git-serve: Add support for (un-)flagging packages
[aur.git] / aurweb / git / serve.py
blob08c6541799e53c8ed6e16be455f418f4d6c547de
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 pkgreq_by_pkgbase(pkgbase_id, reqtype):
187 conn = aurweb.db.Connection()
189 cur = conn.execute("SELECT PackageRequests.ID FROM PackageRequests " +
190 "INNER JOIN RequestTypes ON " +
191 "RequestTypes.ID = PackageRequests.ReqTypeID " +
192 "WHERE PackageRequests.Status = 0 " +
193 "AND PackageRequests.PackageBaseID = ?" +
194 "AND RequestTypes.Name = ?", [pkgbase_id, reqtype])
196 return [row[0] for row in cur.fetchall()]
199 def pkgreq_close(reqid, reason, comments, autoclose=False):
200 statusmap = {'accepted': 2, 'rejected': 3}
201 if reason not in statusmap:
202 die('{:s}: invalid reason: {:s}'.format(action, reason))
203 status = statusmap[reason]
205 conn = aurweb.db.Connection()
207 if autoclose:
208 userid = 0
209 else:
210 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
211 userid = cur.fetchone()[0]
212 if userid == 0:
213 die('{:s}: unknown user: {:s}'.format(action, user))
215 conn.execute("UPDATE PackageRequests SET Status = ?, ClosureComment = ? " +
216 "WHERE ID = ?", [status, comments, reqid])
217 conn.commit()
218 conn.close()
220 subprocess.Popen((notify_cmd, 'request-close', str(userid), str(reqid),
221 reason)).wait()
224 def pkgbase_disown(pkgbase, user, privileged):
225 pkgbase_id = pkgbase_from_name(pkgbase)
226 if not pkgbase_id:
227 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
229 initialized_by_owner = pkgbase_has_full_access(pkgbase, user)
230 if not privileged and not initialized_by_owner:
231 die('{:s}: permission denied: {:s}'.format(action, user))
233 # TODO: Support disowning package bases via package request.
235 # Scan through pending orphan requests and close them.
236 comment = 'The user {:s} disowned the package.'.format(user)
237 for reqid in pkgreq_by_pkgbase(pkgbase_id, 'orphan'):
238 pkgreq_close(reqid, 'accepted', comment, True)
240 comaintainers = []
241 new_maintainer_userid = None
243 conn = aurweb.db.Connection()
245 # Make the first co-maintainer the new maintainer, unless the action was
246 # enforced by a Trusted User.
247 if initialized_by_owner:
248 comaintainers = pkgbase_get_comaintainers(pkgbase)
249 if len(comaintainers) > 0:
250 new_maintainer = comaintainers[0]
251 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?",
252 [new_maintainer])
253 new_maintainer_userid = cur.fetchone()[0]
254 comaintainers.remove(new_maintainer)
256 pkgbase_set_comaintainers(pkgbase, comaintainers, user, privileged)
257 cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " +
258 "WHERE ID = ?", [new_maintainer_userid, pkgbase_id])
260 conn.commit()
262 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
263 userid = cur.fetchone()[0]
264 if userid == 0:
265 die('{:s}: unknown user: {:s}'.format(action, user))
267 subprocess.Popen((notify_cmd, 'disown', str(pkgbase_id), str(userid)))
269 conn.close()
272 def pkgbase_flag(pkgbase, user, comment):
273 pkgbase_id = pkgbase_from_name(pkgbase)
274 if not pkgbase_id:
275 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
277 conn = aurweb.db.Connection()
279 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
280 userid = cur.fetchone()[0]
281 if userid == 0:
282 die('{:s}: unknown user: {:s}'.format(action, user))
284 now = int(time.time())
285 conn.execute("UPDATE PackageBases SET " +
286 "OutOfDateTS = ?, FlaggerUID = ?, FlaggerComment = ? " +
287 "WHERE ID = ? AND OutOfDateTS IS NULL",
288 [now, userid, comment, pkgbase_id])
290 conn.commit()
292 subprocess.Popen((notify_cmd, 'flag', str(userid), str(pkgbase_id)))
295 def pkgbase_unflag(pkgbase, user):
296 pkgbase_id = pkgbase_from_name(pkgbase)
297 if not pkgbase_id:
298 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
300 conn = aurweb.db.Connection()
302 cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user])
303 userid = cur.fetchone()[0]
304 if userid == 0:
305 die('{:s}: unknown user: {:s}'.format(action, user))
307 if user in pkgbase_get_comaintainers(pkgbase):
308 conn.execute("UPDATE PackageBases SET OutOfDateTS = NULL " +
309 "WHERE ID = ?", [pkgbase_id])
310 else:
311 conn.execute("UPDATE PackageBases SET OutOfDateTS = NULL " +
312 "WHERE ID = ? AND (MaintainerUID = ? OR FlaggerUID = ?)",
313 [pkgbase_id, userid, userid])
315 conn.commit()
318 def pkgbase_set_keywords(pkgbase, keywords):
319 pkgbase_id = pkgbase_from_name(pkgbase)
320 if not pkgbase_id:
321 die('{:s}: package base not found: {:s}'.format(action, pkgbase))
323 conn = aurweb.db.Connection()
325 conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?",
326 [pkgbase_id])
327 for keyword in keywords:
328 conn.execute("INSERT INTO PackageKeywords (PackageBaseID, Keyword) " +
329 "VALUES (?, ?)", [pkgbase_id, keyword])
331 conn.commit()
332 conn.close()
335 def pkgbase_has_write_access(pkgbase, user):
336 conn = aurweb.db.Connection()
338 cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
339 "LEFT JOIN PackageComaintainers " +
340 "ON PackageComaintainers.PackageBaseID = PackageBases.ID " +
341 "INNER JOIN Users " +
342 "ON Users.ID = PackageBases.MaintainerUID " +
343 "OR PackageBases.MaintainerUID IS NULL " +
344 "OR Users.ID = PackageComaintainers.UsersID " +
345 "WHERE Name = ? AND Username = ?", [pkgbase, user])
346 return cur.fetchone()[0] > 0
349 def pkgbase_has_full_access(pkgbase, user):
350 conn = aurweb.db.Connection()
352 cur = conn.execute("SELECT COUNT(*) FROM PackageBases " +
353 "INNER JOIN Users " +
354 "ON Users.ID = PackageBases.MaintainerUID " +
355 "WHERE Name = ? AND Username = ?", [pkgbase, user])
356 return cur.fetchone()[0] > 0
359 def die(msg):
360 sys.stderr.write("{:s}\n".format(msg))
361 exit(1)
364 def die_with_help(msg):
365 die(msg + "\nTry `{:s} help` for a list of commands.".format(ssh_cmdline))
368 def warn(msg):
369 sys.stderr.write("warning: {:s}\n".format(msg))
372 def usage(cmds):
373 sys.stderr.write("Commands:\n")
374 colwidth = max([len(cmd) for cmd in cmds.keys()]) + 4
375 for key in sorted(cmds):
376 sys.stderr.write(" " + key.ljust(colwidth) + cmds[key] + "\n")
377 exit(0)
380 def main():
381 user = os.environ.get('AUR_USER')
382 privileged = (os.environ.get('AUR_PRIVILEGED', '0') == '1')
383 ssh_cmd = os.environ.get('SSH_ORIGINAL_COMMAND')
384 ssh_client = os.environ.get('SSH_CLIENT')
386 if not ssh_cmd:
387 die_with_help("Interactive shell is disabled.")
388 cmdargv = shlex.split(ssh_cmd)
389 action = cmdargv[0]
390 remote_addr = ssh_client.split(' ')[0] if ssh_client else None
392 if enable_maintenance:
393 if remote_addr not in maintenance_exc:
394 die("The AUR is down due to maintenance. We will be back soon.")
396 if action == 'git' and cmdargv[1] in ('upload-pack', 'receive-pack'):
397 action = action + '-' + cmdargv[1]
398 del cmdargv[1]
400 if action == 'git-upload-pack' or action == 'git-receive-pack':
401 if len(cmdargv) < 2:
402 die_with_help("{:s}: missing path".format(action))
404 path = cmdargv[1].rstrip('/')
405 if not path.startswith('/'):
406 path = '/' + path
407 if not path.endswith('.git'):
408 path = path + '.git'
409 pkgbase = path[1:-4]
410 if not re.match(repo_regex, pkgbase):
411 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
413 if action == 'git-receive-pack' and pkgbase_exists(pkgbase):
414 if not privileged and not pkgbase_has_write_access(pkgbase, user):
415 die('{:s}: permission denied: {:s}'.format(action, user))
417 os.environ["AUR_USER"] = user
418 os.environ["AUR_PKGBASE"] = pkgbase
419 os.environ["GIT_NAMESPACE"] = pkgbase
420 cmd = action + " '" + repo_path + "'"
421 os.execl(git_shell_cmd, git_shell_cmd, '-c', cmd)
422 elif action == 'set-keywords':
423 if len(cmdargv) < 2:
424 die_with_help("{:s}: missing repository name".format(action))
425 pkgbase_set_keywords(cmdargv[1], cmdargv[2:])
426 elif action == 'list-repos':
427 if len(cmdargv) > 1:
428 die_with_help("{:s}: too many arguments".format(action))
429 list_repos(user)
430 elif action == 'setup-repo':
431 if len(cmdargv) < 2:
432 die_with_help("{:s}: missing repository name".format(action))
433 if len(cmdargv) > 2:
434 die_with_help("{:s}: too many arguments".format(action))
435 warn('{:s} is deprecated. '
436 'Use `git push` to create new repositories.'.format(action))
437 create_pkgbase(cmdargv[1], user)
438 elif action == 'restore':
439 if len(cmdargv) < 2:
440 die_with_help("{:s}: missing repository name".format(action))
441 if len(cmdargv) > 2:
442 die_with_help("{:s}: too many arguments".format(action))
444 pkgbase = cmdargv[1]
445 if not re.match(repo_regex, pkgbase):
446 die('{:s}: invalid repository name: {:s}'.format(action, pkgbase))
448 if pkgbase_exists(pkgbase):
449 die('{:s}: package base exists: {:s}'.format(action, pkgbase))
450 create_pkgbase(pkgbase, user)
452 os.environ["AUR_USER"] = user
453 os.environ["AUR_PKGBASE"] = pkgbase
454 os.execl(git_update_cmd, git_update_cmd, 'restore')
455 elif action == 'adopt':
456 if len(cmdargv) < 2:
457 die_with_help("{:s}: missing repository name".format(action))
458 if len(cmdargv) > 2:
459 die_with_help("{:s}: too many arguments".format(action))
461 pkgbase = cmdargv[1]
462 pkgbase_adopt(pkgbase, user, privileged)
463 elif action == 'disown':
464 if len(cmdargv) < 2:
465 die_with_help("{:s}: missing repository name".format(action))
466 if len(cmdargv) > 2:
467 die_with_help("{:s}: too many arguments".format(action))
469 pkgbase = cmdargv[1]
470 pkgbase_disown(pkgbase, user, privileged)
471 elif action == 'flag':
472 if len(cmdargv) < 2:
473 die_with_help("{:s}: missing repository name".format(action))
474 if len(cmdargv) < 3:
475 die_with_help("{:s}: missing comment".format(action))
476 if len(cmdargv) > 3:
477 die_with_help("{:s}: too many arguments".format(action))
479 pkgbase = cmdargv[1]
480 comment = cmdargv[2]
481 if len(comment) < 3:
482 die_with_help("{:s}: comment is too short".format(action))
484 pkgbase_flag(pkgbase, user, comment)
485 elif action == 'unflag':
486 if len(cmdargv) < 2:
487 die_with_help("{:s}: missing repository name".format(action))
488 if len(cmdargv) > 2:
489 die_with_help("{:s}: too many arguments".format(action))
491 pkgbase = cmdargv[1]
492 pkgbase_unflag(pkgbase, user)
493 elif action == 'set-comaintainers':
494 if len(cmdargv) < 2:
495 die_with_help("{:s}: missing repository name".format(action))
497 pkgbase = cmdargv[1]
498 userlist = cmdargv[2:]
499 pkgbase_set_comaintainers(pkgbase, userlist, user, privileged)
500 elif action == 'help':
501 cmds = {
502 "adopt <name>": "Adopt a package base.",
503 "disown <name>": "Disown a package base.",
504 "flag <name> <comment>": "Flag a package base out-of-date.",
505 "help": "Show this help message and exit.",
506 "list-repos": "List all your repositories.",
507 "restore <name>": "Restore a deleted package base.",
508 "set-comaintainers <name> [...]": "Set package base co-maintainers.",
509 "set-keywords <name> [...]": "Change package base keywords.",
510 "setup-repo <name>": "Create a repository (deprecated).",
511 "unflag <name>": "Remove out-of-date flag from a package base.",
512 "git-receive-pack": "Internal command used with Git.",
513 "git-upload-pack": "Internal command used with Git.",
515 usage(cmds)
516 else:
517 die_with_help("invalid command: {:s}".format(action))
520 if __name__ == '__main__':
521 main()