# Copyright (c) 2021 Egor Tensin # This file is part of the "cgitize" project. # For details, see https://github.com/egor-tensin/cgitize. # Distributed under the MIT License. from contextlib import contextmanager import os import cgitize.utils as utils GIT_ENV = os.environ.copy() GIT_ENV['GIT_SSH_COMMAND'] = 'ssh -oBatchMode=yes -oLogLevel=QUIET -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null' class Config: def __init__(self, path): self.path = path def exists(self): return os.path.exists(self.path) def open(self, mode='r'): return open(self.path, mode=mode, encoding='utf-8') def read(self): with self.open(mode='r') as fd: return fd.read() def write(self, contents): with self.open(mode='w') as fd: fd.write(contents) @contextmanager def backup(self): old_contents = self.read() try: yield old_contents finally: self.write(old_contents) # What follows is an exteremely loose interpretation of what the .gitconfig # syntax is. The source was git-config(1). class Section: def __init__(self, name, variables): Config.Section.validate_name(name) self.name = name self.variables = variables @staticmethod def validate_name(name): if not name: raise RuntimeError('section names cannot be empty') for c in name: if c.isalnum() or c == '-' or c == '.': continue raise RuntimeError(f'section names must only contain alphanumeric characters, . or -: {name}') @staticmethod def format_name(name): return name def format(self): result = '[self.format_name(self.name)]\n' result += ''.join((var.format() for var in self.variables)) return result class Subsection: def __init__(self, section, name, variables): Config.Section.validate_name(section) Config.Subsection.validate_name(name) self.section = section self.name = name self.variables = variables @staticmethod def validate_name(name): if '\n' in name: raise RuntimeError(f'subsection names cannot contain newlines: {name}') def format_name(self): name = self.name # Escape the backslashes: name = name.replace('\\', '\\\\') # Escape the quotes: name = name.replace('"', '\\"') # Put in quotes: return f'"{name}"' def format(self): result = f'[{Config.Section.format_name(self.section)} {self.format_name()}]\n' result += ''.join((var.format() for var in self.variables)) return result class Variable: def __init__(self, name, value): Config.Variable.validate_name(name) Config.Variable.validate_value(value) self.name = name self.value = value @staticmethod def validate_name(name): if not name: raise RuntimeError('variable names cannot be empty') for c in name: if c.isalnum() or c == '-': continue raise RuntimeError(f'variable name can only contain alphanumeric characters or -: {name}') if not name[0].isalnum(): raise RuntimeError(f'variable name must start with an alphanumeric character: {name}') @staticmethod def validate_value(value): pass def format_name(self): return self.name def format_value(self): value = self.value # Escape the backslashes: value = value.replace('\\', '\\\\') # Escape the supported escape sequences (\n, \t and \b): value = value.replace('\n', '\\n') value = value.replace('\t', '\\t') value = value.replace('\b', '\\b') # Escape the quotes: value = value.replace('"', '\\"') # Put in quotes: value = f'"{value}"' return value def format(self): return f' {self.format_name()} = {self.format_value()}\n' class Git: EXE = 'git' @staticmethod def check(*args, **kwargs): return utils.try_run(Git.EXE, *args, env=GIT_ENV, **kwargs) @staticmethod def capture(*args, **kwargs): return utils.try_run_capture(Git.EXE, *args, env=GIT_ENV, **kwargs) @staticmethod def get_global_config(): return Config(os.path.expanduser('~/.gitconfig')) @staticmethod @contextmanager def setup_auth(repo): if not repo.url_auth: yield return config = Git.get_global_config() with utils.protected_file(config.path): with config.backup() as old_contents: variables = [Config.Variable('insteadOf', repo.clone_url)] subsection = Config.Subsection('url', repo.clone_url_with_auth, variables) new_contents = f'{old_contents}\n{subsection.format()}' config.write(new_contents) yield