aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cgitize/git.py
blob: 769b624c4dbffb016f71b0d044357db6f826b30f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# Copyright (c) 2021 Egor Tensin <Egor.Tensin@gmail.com>
# 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

from cgitize import 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 = f'[{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('\\', r'\\')
            # Escape the quotes:
            name = name.replace('"', r'\"')
            # 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('\\', r'\\')
            # Escape the supported escape sequences (\n, \t and \b):
            value = value.replace('\n', r'\n')
            value = value.replace('\t', r'\t')
            value = value.replace('\b', r'\b')
            # Escape the quotes:
            value = value.replace('"', r'\"')
            # 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_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