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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
#!/usr/bin/env python3
"""
Cron-job that dumps files required in the filesystem to make pggit work.
The job will also generate bare git repositories when new ones are added
to the list, and remove those that are deleted.
This means:
~/.ssh/authorized_keys
"""
import os
import shutil
import io
import psycopg2
import configparser
import urllib.parse
from util.LockFile import LockFile
def replace_file_from_string(fn, s):
if os.path.isfile(fn):
with open(fn) as f:
old = f.read()
if old == s:
# No changes
return False
with open("{}.tmp".format(fn), "w") as f:
f.write(s)
os.chmod("{}.tmp".format(fn), 0o644)
os.rename("{}.tmp".format(fn), fn)
class AuthorizedKeysDumper(object):
def __init__(self, db, conf):
self.db = db
self.conf = conf
def dump(self):
self.dumpkeys()
self.dumprepos()
def dumpkeys(self):
# FIXME: use a trigger to indicate if *anything at all* has changed
curs = self.db.cursor()
curs.execute("SELECT username,sshkey FROM git_users INNER JOIN auth_user on auth_user.id=git_users.user_id ORDER BY username")
f = open("%s/.ssh/authorized_keys.tmp" % self.conf.get("paths", "githome"), "w")
for username, sshkey in curs:
for key in sshkey.split("\n"):
key = key.strip()
if key:
f.write("command=\"%s %s\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s\n" % (self.conf.get("paths", "pggit"), username, key))
f.close()
os.chmod("%s/.ssh/authorized_keys.tmp" % self.conf.get("paths", "githome"), 0o600)
os.rename("%s/.ssh/authorized_keys.tmp" % self.conf.get("paths", "githome"), "%s/.ssh/authorized_keys" % self.conf.get("paths", "githome"))
def dumprepos(self):
# FIXME: use a trigger to indicate if *anything at all* has changed
allrepos = {}
curs = self.db.cursor()
curs.execute("""
SELECT name,anonymous,web,description,initialclone,tabwidth,
COALESCE(
(SELECT min(first_name || ' ' || last_name) FROM repository_permissions AS rp
LEFT JOIN auth_user AS au ON au.id=rp.user_id
WHERE rp.level=2 AND rp.repository=r.repoid),''),
CASE WHEN EXISTS
(SELECT * FROM remoterepositories WHERE remoterepositories.id=r.remoterepository_id)
THEN 1 ELSE 0 END
FROM repositories AS r WHERE approved ORDER BY name""")
s = io.StringIO()
webrepos = []
for name, anon, web, description, initialclone, tabwidth, owner, remoterepo in curs:
allrepos[name] = 1
repopath = "%s/repos/%s.git" % (self.conf.get("paths", "githome"), name)
# If this is a remote repository, don't try to create it if it's not there -
# this is handled by the repository importer.
if remoterepo and not os.path.isdir(repopath):
continue
# Check if this repository exists at all
if not os.path.isdir(repopath):
# Does not exist, let's initialize a new one
# Does the parent directory exist? Needed for things like /user/foo/repo.git
parentpath = os.path.normpath(os.path.join(repopath, os.pardir))
if not os.path.isdir(parentpath):
# Parent does not exist, create it
os.makedirs(parentpath)
os.environ['GIT_DIR'] = repopath
if initialclone:
print("Initializing git into %s (cloned repo %s)" % (name, initialclone))
if initialclone.startswith('git://'):
# Just use the raw URL, expect approver to have validated it
oldrepo = initialclone
else:
# This is a local reference, so rewrite it based on our root
oldrepo = "%s/repos/%s.git" % (self.conf.get("paths", "githome"), initialclone)
os.system("git clone --bare %s %s/repos/%s.git" % (
# Old repo
oldrepo,
# New repo
self.conf.get("paths", "githome"), name,
))
else:
print("Initializing new git repository %s" % name)
os.system("git init --bare --shared")
if os.path.isfile("{}/description".format(repopath)):
os.remove("{}/description".format(repopath))
del os.environ['GIT_DIR']
# Check for publishing options here
if web:
cgitrc = io.StringIO()
s.write("%s.git %s\n" % (urllib.parse.quote_plus(name), urllib.parse.quote_plus(owner)))
replace_file_from_string(
"%s/description" % repopath,
description,
)
# Check if we need to change the tab width (default is 8)
repoconf = configparser.ConfigParser()
repoconf.read("%s/config" % repopath)
tabwidth_mod = False
if repoconf.has_option('gitweb', 'tabwidth'):
if tabwidth != int(repoconf.get('gitweb', 'tabwidth')):
tabwidth_mod = True
# Write to cgitrc, we check the contents for this one later
# For now, we only support 4 space tabs (or the default 8) in cgit
if int(repoconf.get('gitweb', 'tabwidth')) == 4:
cgitrc.write('extra-head-content=<link rel="stylesheet" type="text/css" href="/cgit-css/4space.css" />')
cgitrc.write("\n")
else:
# Not specified, so it's 8...
if tabwidth != 8:
tabwidth_mod = True
if tabwidth_mod:
print("Changing tab width for %s" % name)
if not repoconf.has_section('gitweb'):
repoconf.add_section('gitweb')
repoconf.set('gitweb', 'tabwidth', str(tabwidth))
cf = open("%s/config" % repopath, "w")
repoconf.write(cf)
cf.close()
# If one or more options are in the cgirtc file, create it
if cgitrc.tell():
replace_file_from_string(
"{}/cgitrc".format(repopath),
cgitrc.getvalue(),
)
else:
if os.path.isfile("{}/cgitrc".format(repopath)):
os.remove("{}/cgitrc".format(repopath))
else:
# If repo should not be exposed on the web, remove the description file. We use this
# as a trigger of whether to show it...
if os.path.isfile("{}/description".format(repopath)):
os.remove("{}/description".format(repopath))
anonfile = "%s/git-daemon-export-ok" % repopath
if anon:
if not os.path.isfile(anonfile):
open(anonfile, "w").close()
# When anonymous access is allowed, create an entry so
# we can access it with http git.
webrepos.append((name, repopath))
else:
if os.path.isfile(anonfile):
os.remove(anonfile)
replace_file_from_string(
self.conf.get("paths", "gitweblist"),
s.getvalue(),
)
if webrepos:
changed = False
if self.conf.has_option("paths", "lighttpdconf"):
if replace_file_from_string(
self.conf.get("paths", "lighttpdconf"),
"alias.url += (\n{}\n)\n".format("\n".join([' "/git/{}.git/" => "{}/",'.format(name, path) for name, path in webrepos])),
):
changed = True
if self.conf.has_option("paths", "nginxconf"):
if replace_file_from_string(
self.conf.get("paths", "nginxconf"),
"""if ($args ~ "git-receive-pack") {{
return 403;
}}
if ($uri !~ "^/git/({})\.git") {{
return 404;
}}
""".format("|".join([name for name, path in webrepos])),
):
changed = True
if changed and self.conf.has_option("webserver", "reloadcommand"):
os.system(self.conf.get("webserver", "reloadcommand"))
# Now remove any repositories that have been deleted
self._removerepos("%s/repos/" % self.conf.get("paths", "githome"), '/', allrepos)
def _removerepos(self, rootpath, relativepath, allrepos):
dl = os.listdir(rootpath)
if not dl:
# Nothing in there, perhaps we need to remove it?
if relativepath != '/':
print("Removing container directory %s" % rootpath)
try:
os.rmdir("%s" % rootpath)
except Exception as e:
print("FAIL: unable to remove container directory: %s" % e)
return
for d in dl:
if d.startswith('.'):
continue
if not d.endswith('.git'):
# If it doesn't end in '.git', that means it's a repository container
# and not actually a repository. So we have to recurse.
self._removerepos(os.path.join(rootpath, d),
os.path.join(relativepath, d),
allrepos)
else:
# Ends with '.git', meaning it's a repository. Let's figure out if it should
# be here.
d = d[:-4]
if os.path.join(relativepath, d)[1:] not in allrepos:
print("Removing repository %s" % os.path.join(relativepath, d))
try:
shutil.rmtree("%s.git" % os.path.join(rootpath, d))
except Exception as e:
print("FAIL: unable to remove directory: %s" % e)
if __name__ == "__main__":
c = configparser.ConfigParser()
c.read("pggit.settings")
lock = LockFile("%s/repos/.gitdump_interlock" % c.get("paths", "githome"))
db = psycopg2.connect(c.get('database', 'db'))
AuthorizedKeysDumper(db, c).dump()
|