Reorganize tests
[aur.git] / git-interface / git-serve.py
blob5f3b26ddbfa8b008d7fb3f8b5733e56f5d5e6b09
1 #!/usr/bin/python3
3 import os
4 import re
5 import shlex
6 import subprocess
7 import sys
8 import time
10 import aurweb.config
11 import aurweb.db
13 notify_cmd = aurweb.config.get('notifications', 'notify-cmd')
15 repo_path = aurweb.config.get('serve', 'repo-path')
16 repo_regex = aurweb.config.get('serve', 'repo-regex')
17 git_shell_cmd = aurweb.config.get('serve', 'git-shell-cmd')
18 git_update_cmd = aurweb.config.get('serve', 'git-update-cmd')
19 ssh_cmdline = aurweb.config.get('serve', 'ssh-cmdline')
21 enable_maintenance = aurweb.config.getboolean('options', 'enable-maintenance')
22 maintenance_exc = aurweb.config.get('options', 'maintenance-exceptions').split()
25 def pkgbase_from_name(pkgbase):
26 conn = aurweb.db.Connection()
27 cur = conn.execute("SELECT ID FROM PackageBases WHERE Name = ?", [pkgbase])
29 row = cur.fetchone()
30 return row[0] if row else None
33 def pkgbase_exists(pkgbase):
34 return pkgbase_from_name(pkgbase) is not None
37 def list_repos(user):
38 conn = aurweb.db.Connection()
40 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
41 userid = cur.fetchone()[0]
42 if userid == 0:
43 die('{:s}: unknown user: {:s}'.format(action, user))
45 cur = conn.execute("SELECT Name, PackagerUID FROM PackageBases " +
46 "WHERE MaintainerUID = ?", [userid])
47 for row in cur:
48 print((' ' if row[1] else '*') + row[0])
49 conn.close()
52 def create_pkgbase(pkgbase, user):
53 if not re.match(repo_regex, pkgbase):
54 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
55 if pkgbase_exists(pkgbase):
56 die('{:s}: package base already exists: {:s}'.format(action, pkgbase))
58 conn = aurweb.db.Connection()
60 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
61 userid = cur.fetchone()[0]
62 if userid == 0:
63 die('{:s}: unknown user: {:s}'.format(action, user))
65 now = int(time.time())
66 cur = conn.execute("INSERT INTO PackageBases (Name, SubmittedTS, " +
67 "ModifiedTS, SubmitterUID, MaintainerUID) VALUES " +
68 "(?, ?, ?, ?, ?)", [pkgbase, now, now, userid, userid])
69 pkgbase_id = cur.lastrowid
71 cur = conn.execute("INSERT INTO PackageNotifications " +
72 "(PackageBaseID, UserID) VALUES (?, ?)",
73 [pkgbase_id, userid])
75 conn.commit()
76 conn.close()
79 def pkgbase_adopt(pkgbase, user, privileged):
80 pkgbase_id = pkgbase_from_name(pkgbase)
81 if not pkgbase_id:
82 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
84 conn = aurweb.db.Connection()
86 cur = conn.execute("SELECT ID FROM PackageBases WHERE ID = ? AND " +
87 "MaintainerUID IS NULL", [pkgbase_id])
88 if not privileged and not cur.fetchone():
89 die('{:s}: permission denied: {:s}'.format(action, user))
91 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
92 userid = cur.fetchone()[0]
93 if userid == 0:
94 die('{:s}: unknown user: {:s}'.format(action, user))
96 cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
97 "WHERE ID = ?", [userid, pkgbase_id])
99 cur = conn.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " +
100 "PackageBaseID = ? AND UserID = ?",
101 [pkgbase_id, userid])
102 if cur.fetchone()[0] == 0:
103 cur = conn.execute("INSERT INTO PackageNotifications " +
104 "(PackageBaseID, UserID) VALUES (?, ?)",
105 [pkgbase_id, userid])
106 conn.commit()
108 subprocess.Popen((notify_cmd, 'adopt', str(pkgbase_id), str(userid)))
110 conn.close()
113 def pkgbase_get_comaintainers(pkgbase):
114 conn = aurweb.db.Connection()
116 cur = conn.execute("SELECT UserName FROM PackageComaintainers " +
117 "INNER JOIN Users " +
118 "ON Users.ID = PackageComaintainers.UsersID " +
119 "INNER JOIN PackageBases " +
120 "ON PackageBases.ID = PackageComaintainers.PackageBaseID " +
121 "WHERE PackageBases.Name = ? " +
122 "ORDER BY Priority ASC", [pkgbase])
124 return [row[0] for row in cur.fetchall()]
127 def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged):
128 pkgbase_id = pkgbase_from_name(pkgbase)
129 if not pkgbase_id:
130 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
132 if not privileged and not pkgbase_has_full_access(pkgbase, user):
133 die('{:s}: permission denied: {:s}'.format(action, user))
135 conn = aurweb.db.Connection()
137 userlist_old = set(pkgbase_get_comaintainers(pkgbase))
139 uids_old = set()
140 for olduser in userlist_old:
141 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
142 [olduser])
143 userid = cur.fetchone()[0]
144 if userid == 0:
145 die('{:s}: unknown user: {:s}'.format(action, user))
146 uids_old.add(userid)
148 uids_new = set()
149 for newuser in userlist:
150 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
151 [newuser])
152 userid = cur.fetchone()[0]
153 if userid == 0:
154 die('{:s}: unknown user: {:s}'.format(action, user))
155 uids_new.add(userid)
157 uids_add = uids_new - uids_old
158 uids_rem = uids_old - uids_new
160 i = 1
161 for userid in uids_new:
162 if userid in uids_add:
163 cur = conn.execute("INSERT INTO PackageComaintainers " +
164 "(PackageBaseID, UsersID, Priority) " +
165 "VALUES (?, ?, ?)", [pkgbase_id, userid, i])
166 subprocess.Popen((notify_cmd, 'comaintainer-add', str(pkgbase_id),
167 str(userid)))
168 else:
169 cur = conn.execute("UPDATE PackageComaintainers " +
170 "SET Priority = ? " +
171 "WHERE PackageBaseID = ? AND UsersID = ?",
172 [i, pkgbase_id, userid])
173 i += 1
175 for userid in uids_rem:
176 cur = conn.execute("DELETE FROM PackageComaintainers " +
177 "WHERE PackageBaseID = ? AND UsersID = ?",
178 [pkgbase_id, userid])
179 subprocess.Popen((notify_cmd, 'comaintainer-remove',
180 str(pkgbase_id), str(userid)))
182 conn.commit()
183 conn.close()
186 def pkgbase_disown(pkgbase, user, privileged):
187 pkgbase_id = pkgbase_from_name(pkgbase)
188 if not pkgbase_id:
189 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
191 initialized_by_owner = pkgbase_has_full_access(pkgbase, user)
192 if not privileged and not initialized_by_owner:
193 die('{:s}: permission denied: {:s}'.format(action, user))
195 # TODO: Support disowning package bases via package request.
196 # TODO: Scan through pending orphan requests and close them.
198 comaintainers = []
199 new_maintainer_userid = None
201 conn = aurweb.db.Connection()
203 # Make the first co-maintainer the new maintainer, unless the action was
204 # enforced by a Trusted User.
205 if initialized_by_owner:
206 comaintainers = pkgbase_get_comaintainers(pkgbase)
207 if len(comaintainers) > 0:
208 new_maintainer = comaintainers[0]
209 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
210 [new_maintainer])
211 new_maintainer_userid = cur.fetchone()[0]
212 comaintainers.remove(new_maintainer)
214 pkgbase_set_comaintainers(pkgbase, comaintainers, user, privileged)
215 cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
216 "WHERE ID = ?", [new_maintainer_userid, pkgbase_id])
218 conn.commit()
220 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
221 userid = cur.fetchone()[0]
222 if userid == 0:
223 die('{:s}: unknown user: {:s}'.format(action, user))
225 subprocess.Popen((notify_cmd, 'disown', str(pkgbase_id), str(userid)))
227 conn.close()
230 def pkgbase_set_keywords(pkgbase, keywords):
231 pkgbase_id = pkgbase_from_name(pkgbase)
232 if not pkgbase_id:
233 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
235 conn = aurweb.db.Connection()
237 conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?",
238 [pkgbase_id])
239 for keyword in keywords:
240 conn.execute("INSERT INTO PackageKeywords (PackageBaseID, Keyword) " +
241 "VALUES (?, ?)", [pkgbase_id, keyword])
243 conn.commit()
244 conn.close()
247 def pkgbase_has_write_access(pkgbase, user):
248 conn = aurweb.db.Connection()
250 cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
251 "LEFT JOIN PackageComaintainers " +
252 "ON PackageComaintainers.PackageBaseID = PackageBases.ID " +
253 "INNER JOIN Users " +
254 "ON Users.ID = PackageBases.MaintainerUID " +
255 "OR PackageBases.MaintainerUID IS NULL " +
256 "OR Users.ID = PackageComaintainers.UsersID " +
257 "WHERE Name = ? AND Username = ?", [pkgbase, user])
258 return cur.fetchone()[0] > 0
261 def pkgbase_has_full_access(pkgbase, user):
262 conn = aurweb.db.Connection()
264 cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
265 "INNER JOIN Users " +
266 "ON Users.ID = PackageBases.MaintainerUID " +
267 "WHERE Name = ? AND Username = ?", [pkgbase, user])
268 return cur.fetchone()[0] > 0
271 def die(msg):
272 sys.stderr.write("{:s}\n".format(msg))
273 exit(1)
276 def die_with_help(msg):
277 die(msg + "\nTry `{:s} help` for a list of commands.".format(ssh_cmdline))
280 def warn(msg):
281 sys.stderr.write("warning: {:s}\n".format(msg))
284 def usage(cmds):
285 sys.stderr.write("Commands:\n")
286 colwidth = max([len(cmd) for cmd in cmds.keys()]) + 4
287 for key in sorted(cmds):
288 sys.stderr.write(" " + key.ljust(colwidth) + cmds[key] + "\n")
289 exit(0)
292 def main():
293 user = os.environ.get('AUR_USER')
294 privileged = (os.environ.get('AUR_PRIVILEGED', '0') == '1')
295 ssh_cmd = os.environ.get('SSH_ORIGINAL_COMMAND')
296 ssh_client = os.environ.get('SSH_CLIENT')
298 if not ssh_cmd:
299 die_with_help("Interactive shell is disabled.")
300 cmdargv = shlex.split(ssh_cmd)
301 action = cmdargv[0]
302 remote_addr = ssh_client.split(' ')[0] if ssh_client else None
304 if enable_maintenance:
305 if remote_addr not in maintenance_exc:
306 die("The AUR is down due to maintenance. We will be back soon.")
308 if action == 'git-upload-pack' or action == 'git-receive-pack':
309 if len(cmdargv) < 2:
310 die_with_help("{:s}: missing path".format(action))
312 path = cmdargv[1].rstrip('/')
313 if not path.startswith('/'):
314 path = '/' + path
315 if not path.endswith('.git'):
316 path = path + '.git'
317 pkgbase = path[1:-4]
318 if not re.match(repo_regex, pkgbase):
319 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
321 if action == 'git-receive-pack' and pkgbase_exists(pkgbase):
322 if not privileged and not pkgbase_has_write_access(pkgbase, user):
323 die('{:s}: permission denied: {:s}'.format(action, user))
325 os.environ["AUR_USER"] = user
326 os.environ["AUR_PKGBASE"] = pkgbase
327 os.environ["GIT_NAMESPACE"] = pkgbase
328 cmd = action + " '" + repo_path + "'"
329 os.execl(git_shell_cmd, git_shell_cmd, '-c', cmd)
330 elif action == 'set-keywords':
331 if len(cmdargv) < 2:
332 die_with_help("{:s}: missing repository name".format(action))
333 pkgbase_set_keywords(cmdargv[1], cmdargv[2:])
334 elif action == 'list-repos':
335 if len(cmdargv) > 1:
336 die_with_help("{:s}: too many arguments".format(action))
337 list_repos(user)
338 elif action == 'setup-repo':
339 if len(cmdargv) < 2:
340 die_with_help("{:s}: missing repository name".format(action))
341 if len(cmdargv) > 2:
342 die_with_help("{:s}: too many arguments".format(action))
343 warn('{:s} is deprecated. '
344 'Use `git push` to create new repositories.'.format(action))
345 create_pkgbase(cmdargv[1], user)
346 elif action == 'restore':
347 if len(cmdargv) < 2:
348 die_with_help("{:s}: missing repository name".format(action))
349 if len(cmdargv) > 2:
350 die_with_help("{:s}: too many arguments".format(action))
352 pkgbase = cmdargv[1]
353 if not re.match(repo_regex, pkgbase):
354 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
356 if pkgbase_exists(pkgbase):
357 die('{:s}: package base exists: {:s}'.format(action, pkgbase))
358 create_pkgbase(pkgbase, user)
360 os.environ["AUR_USER"] = user
361 os.environ["AUR_PKGBASE"] = pkgbase
362 os.execl(git_update_cmd, git_update_cmd, 'restore')
363 elif action == 'adopt':
364 if len(cmdargv) < 2:
365 die_with_help("{:s}: missing repository name".format(action))
366 if len(cmdargv) > 2:
367 die_with_help("{:s}: too many arguments".format(action))
369 pkgbase = cmdargv[1]
370 pkgbase_adopt(pkgbase, user, privileged)
371 elif action == 'disown':
372 if len(cmdargv) < 2:
373 die_with_help("{:s}: missing repository name".format(action))
374 if len(cmdargv) > 2:
375 die_with_help("{:s}: too many arguments".format(action))
377 pkgbase = cmdargv[1]
378 pkgbase_disown(pkgbase, user, privileged)
379 elif action == 'set-comaintainers':
380 if len(cmdargv) < 2:
381 die_with_help("{:s}: missing repository name".format(action))
383 pkgbase = cmdargv[1]
384 userlist = cmdargv[2:]
385 pkgbase_set_comaintainers(pkgbase, userlist, user, privileged)
386 elif action == 'help':
387 cmds = {
388 "adopt <name>": "Adopt a package base.",
389 "disown <name>": "Disown a package base.",
390 "help": "Show this help message and exit.",
391 "list-repos": "List all your repositories.",
392 "restore <name>": "Restore a deleted package base.",
393 "set-comaintainers <name> [...]": "Set package base co-maintainers.",
394 "set-keywords <name> [...]": "Change package base keywords.",
395 "setup-repo <name>": "Create a repository (deprecated).",
396 "git-receive-pack": "Internal command used with Git.",
397 "git-upload-pack": "Internal command used with Git.",
399 usage(cmds)
400 else:
401 die_with_help("invalid command: {:s}".format(action))
404 if __name__ == '__main__':
405 main()