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
|