Fix type of FlaggerUID in table PackageBases
[aur.git] / git-interface / git-update.py
blob4698382d29f917beb8825642ff805114a430b8f0
1 #!/usr/bin/python3
3 from copy import copy, deepcopy
4 import configparser
5 import mysql.connector
6 import os
7 import pygit2
8 import re
9 import sys
11 import aurinfo
13 config = configparser.RawConfigParser()
14 config.read(os.path.dirname(os.path.realpath(__file__)) + "/../conf/config")
16 aur_db_host = config.get('database', 'host')
17 aur_db_name = config.get('database', 'name')
18 aur_db_user = config.get('database', 'user')
19 aur_db_pass = config.get('database', 'password')
20 aur_db_socket = config.get('database', 'socket')
22 repo_path = config.get('serve', 'repo-path')
23 repo_regex = config.get('serve', 'repo-regex')
25 def extract_arch_fields(pkginfo, field):
26 values = []
28 if field in pkginfo:
29 for val in pkginfo[field]:
30 values.append({"value": val, "arch": None})
32 for arch in ['i686', 'x86_64']:
33 if field + '_' + arch in pkginfo:
34 for val in pkginfo[field + '_' + arch]:
35 values.append({"value": val, "arch": arch})
37 return values
39 def parse_dep(depstring):
40 dep, _, desc = depstring.partition(': ')
41 depname = re.sub(r'(<|=|>).*', '', dep)
42 depcond = dep[len(depname):]
44 if (desc):
45 return (depname + ': ' + desc, depcond)
46 else:
47 return (depname, depcond)
49 def save_srcinfo(srcinfo, db, cur, user):
50 # Obtain package base ID and previous maintainer.
51 pkgbase = srcinfo._pkgbase['pkgname']
52 cur.execute("SELECT ID, MaintainerUID FROM PackageBases "
53 "WHERE Name = %s", [pkgbase])
54 (pkgbase_id, maintainer_uid) = cur.fetchone()
55 was_orphan = not maintainer_uid
57 # Obtain the user ID of the new maintainer.
58 cur.execute("SELECT ID FROM Users WHERE Username = %s", [user])
59 user_id = int(cur.fetchone()[0])
61 # Update package base details and delete current packages.
62 cur.execute("UPDATE PackageBases SET ModifiedTS = UNIX_TIMESTAMP(), " +
63 "PackagerUID = %s, OutOfDateTS = NULL WHERE ID = %s",
64 [user_id, pkgbase_id])
65 cur.execute("UPDATE PackageBases SET MaintainerUID = %s " +
66 "WHERE ID = %s AND MaintainerUID IS NULL",
67 [user_id, pkgbase_id])
68 cur.execute("DELETE FROM Packages WHERE PackageBaseID = %s",
69 [pkgbase_id])
71 for pkgname in srcinfo.GetPackageNames():
72 pkginfo = srcinfo.GetMergedPackage(pkgname)
74 if 'epoch' in pkginfo and int(pkginfo['epoch']) > 0:
75 ver = '{:d}:{:s}-{:s}'.format(int(pkginfo['epoch']), pkginfo['pkgver'],
76 pkginfo['pkgrel'])
77 else:
78 ver = '{:s}-{:s}'.format(pkginfo['pkgver'], pkginfo['pkgrel'])
80 for field in ('pkgdesc', 'url'):
81 if not field in pkginfo:
82 pkginfo[field] = None
84 # Create a new package.
85 cur.execute("INSERT INTO Packages (PackageBaseID, Name, " +
86 "Version, Description, URL) " +
87 "VALUES (%s, %s, %s, %s, %s)",
88 [pkgbase_id, pkginfo['pkgname'], ver,
89 pkginfo['pkgdesc'], pkginfo['url']])
90 db.commit()
91 pkgid = cur.lastrowid
93 # Add package sources.
94 for source_info in extract_arch_fields(pkginfo, 'source'):
95 cur.execute("INSERT INTO PackageSources (PackageID, Source, " +
96 "SourceArch) VALUES (%s, %s, %s)",
97 [pkgid, source_info['value'], source_info['arch']])
99 # Add package dependencies.
100 for deptype in ('depends', 'makedepends',
101 'checkdepends', 'optdepends'):
102 cur.execute("SELECT ID FROM DependencyTypes WHERE Name = %s",
103 [deptype])
104 deptypeid = cur.fetchone()[0]
105 for dep_info in extract_arch_fields(pkginfo, deptype):
106 depname, depcond = parse_dep(dep_info['value'])
107 deparch = dep_info['arch']
108 cur.execute("INSERT INTO PackageDepends (PackageID, " +
109 "DepTypeID, DepName, DepCondition, DepArch) " +
110 "VALUES (%s, %s, %s, %s, %s)",
111 [pkgid, deptypeid, depname, depcond, deparch])
113 # Add package relations (conflicts, provides, replaces).
114 for reltype in ('conflicts', 'provides', 'replaces'):
115 cur.execute("SELECT ID FROM RelationTypes WHERE Name = %s",
116 [reltype])
117 reltypeid = cur.fetchone()[0]
118 for rel_info in extract_arch_fields(pkginfo, reltype):
119 relname, relcond = parse_dep(rel_info['value'])
120 relarch = rel_info['arch']
121 cur.execute("INSERT INTO PackageRelations (PackageID, " +
122 "RelTypeID, RelName, RelCondition, RelArch) " +
123 "VALUES (%s, %s, %s, %s, %s)",
124 [pkgid, reltypeid, relname, relcond, relarch])
126 # Add package licenses.
127 if 'license' in pkginfo:
128 for license in pkginfo['license']:
129 cur.execute("SELECT ID FROM Licenses WHERE Name = %s",
130 [license])
131 if cur.rowcount == 1:
132 licenseid = cur.fetchone()[0]
133 else:
134 cur.execute("INSERT INTO Licenses (Name) VALUES (%s)",
135 [license])
136 db.commit()
137 licenseid = cur.lastrowid
138 cur.execute("INSERT INTO PackageLicenses (PackageID, " +
139 "LicenseID) VALUES (%s, %s)",
140 [pkgid, licenseid])
142 # Add package groups.
143 if 'groups' in pkginfo:
144 for group in pkginfo['groups']:
145 cur.execute("SELECT ID FROM Groups WHERE Name = %s",
146 [group])
147 if cur.rowcount == 1:
148 groupid = cur.fetchone()[0]
149 else:
150 cur.execute("INSERT INTO Groups (Name) VALUES (%s)",
151 [group])
152 db.commit()
153 groupid = cur.lastrowid
154 cur.execute("INSERT INTO PackageGroups (PackageID, "
155 "GroupID) VALUES (%s, %s)", [pkgid, groupid])
157 # Add user to notification list on adoption.
158 if was_orphan:
159 cur.execute("SELECT COUNT(*) FROM CommentNotify WHERE " +
160 "PackageBaseID = %s AND UserID = %s",
161 [pkgbase_id, user_id])
162 if cur.fetchone()[0] == 0:
163 cur.execute("INSERT INTO CommentNotify (PackageBaseID, UserID) " +
164 "VALUES (%s, %s)", [pkgbase_id, user_id])
166 db.commit()
168 def die(msg):
169 sys.stderr.write("error: {:s}\n".format(msg))
170 exit(1)
172 def die_commit(msg, commit):
173 sys.stderr.write("error: The following error " +
174 "occurred when parsing commit\n")
175 sys.stderr.write("error: {:s}:\n".format(commit))
176 sys.stderr.write("error: {:s}\n".format(msg))
177 exit(1)
179 repo = pygit2.Repository(repo_path)
181 user = os.environ.get("AUR_USER")
182 pkgbase = os.environ.get("AUR_PKGBASE")
183 privileged = (os.environ.get("AUR_PRIVILEGED", '0') == '1')
185 if len(sys.argv) == 2 and sys.argv[1] == "restore":
186 if 'refs/heads/' + pkgbase not in repo.listall_references():
187 die('{:s}: repository not found: {:s}'.format(sys.argv[1], pkgbase))
188 refname = "refs/heads/master"
189 sha1_old = sha1_new = repo.lookup_reference('refs/heads/' + pkgbase).target
190 elif len(sys.argv) == 4:
191 refname, sha1_old, sha1_new = sys.argv[1:4]
192 else:
193 die("invalid arguments")
195 if refname != "refs/heads/master":
196 die("pushing to a branch other than master is restricted")
198 db = mysql.connector.connect(host=aur_db_host, user=aur_db_user,
199 passwd=aur_db_pass, db=aur_db_name,
200 unix_socket=aur_db_socket, buffered=True)
201 cur = db.cursor()
203 # Detect and deny non-fast-forwards.
204 if sha1_old != "0000000000000000000000000000000000000000":
205 walker = repo.walk(sha1_old, pygit2.GIT_SORT_TOPOLOGICAL)
206 walker.hide(sha1_new)
207 if next(walker, None) != None:
208 cur.execute("SELECT AccountTypeID FROM Users WHERE UserName = %s ",
209 [user])
210 if cur.fetchone()[0] == 1:
211 die("denying non-fast-forward (you should pull first)")
213 # Prepare the walker that validates new commits.
214 walker = repo.walk(sha1_new, pygit2.GIT_SORT_TOPOLOGICAL)
215 if sha1_old != "0000000000000000000000000000000000000000":
216 walker.hide(sha1_old)
218 # Validate all new commits.
219 for commit in walker:
220 for fname in ('.SRCINFO', 'PKGBUILD'):
221 if not fname in commit.tree:
222 die_commit("missing {:s}".format(fname), str(commit.id))
224 for treeobj in commit.tree:
225 blob = repo[treeobj.id]
227 if isinstance(blob, pygit2.Tree):
228 die_commit("the repository must not contain subdirectories",
229 str(commit.id))
231 if not isinstance(blob, pygit2.Blob):
232 die_commit("not a blob object: {:s}".format(treeobj), str(commit.id))
234 if blob.size > 250000:
235 die_commit("maximum blob size (250kB) exceeded", str(commit.id))
237 srcinfo_raw = repo[commit.tree['.SRCINFO'].id].data.decode()
238 srcinfo_raw = srcinfo_raw.split('\n')
239 ecatcher = aurinfo.CollectionECatcher()
240 srcinfo = aurinfo.ParseAurinfoFromIterable(srcinfo_raw, ecatcher)
241 errors = ecatcher.Errors()
242 if errors:
243 sys.stderr.write("error: The following errors occurred "
244 "when parsing .SRCINFO in commit\n")
245 sys.stderr.write("error: {:s}:\n".format(str(commit.id)))
246 for error in errors:
247 sys.stderr.write("error: line {:d}: {:s}\n".format(*error))
248 exit(1)
250 srcinfo_pkgbase = srcinfo._pkgbase['pkgname']
251 if not re.match(repo_regex, srcinfo_pkgbase):
252 die_commit('invalid pkgbase: {:s}'.format(srcinfo_pkgbase), str(commit.id))
254 for pkgname in srcinfo.GetPackageNames():
255 pkginfo = srcinfo.GetMergedPackage(pkgname)
257 for field in ('pkgver', 'pkgrel', 'pkgname'):
258 if not field in pkginfo:
259 die_commit('missing mandatory field: {:s}'.format(field), str(commit.id))
261 if 'epoch' in pkginfo and not pkginfo['epoch'].isdigit():
262 die_commit('invalid epoch: {:s}'.format(pkginfo['epoch']), str(commit.id))
264 if not re.match(r'[a-z0-9][a-z0-9\.+_-]*$', pkginfo['pkgname']):
265 die_commit('invalid package name: {:s}'.format(pkginfo['pkgname']),
266 str(commit.id))
268 for field in ('pkgname', 'pkgdesc', 'url'):
269 if field in pkginfo and len(pkginfo[field]) > 255:
270 die_commit('{:s} field too long: {:s}'.format(field, pkginfo[field]),
271 str(commit.id))
273 for field in ('install', 'changelog'):
274 if field in pkginfo and not pkginfo[field] in commit.tree:
275 die_commit('missing {:s} file: {:s}'.format(field, pkginfo[field]),
276 str(commit.id))
278 for field in extract_arch_fields(pkginfo, 'source'):
279 fname = field['value']
280 if "://" in fname or "lp:" in fname:
281 continue
282 if not fname in commit.tree:
283 die_commit('missing source file: {:s}'.format(fname), str(commit.id))
285 # Read .SRCINFO from the HEAD commit.
286 srcinfo_raw = repo[repo[sha1_new].tree['.SRCINFO'].id].data.decode()
287 srcinfo_raw = srcinfo_raw.split('\n')
288 srcinfo = aurinfo.ParseAurinfoFromIterable(srcinfo_raw)
290 # Ensure that the package base name matches the repository name.
291 srcinfo_pkgbase = srcinfo._pkgbase['pkgname']
292 if srcinfo_pkgbase != pkgbase:
293 die('invalid pkgbase: {:s}, expected {:s}'.format(srcinfo_pkgbase, pkgbase))
295 # Ensure that packages are neither blacklisted nor overwritten.
296 cur.execute("SELECT ID FROM PackageBases WHERE Name = %s", [pkgbase])
297 pkgbase_id = cur.fetchone()[0] if cur.rowcount == 1 else 0
299 cur.execute("SELECT Name FROM PackageBlacklist")
300 blacklist = [row[0] for row in cur.fetchall()]
302 for pkgname in srcinfo.GetPackageNames():
303 pkginfo = srcinfo.GetMergedPackage(pkgname)
304 pkgname = pkginfo['pkgname']
306 if pkgname in blacklist and not privileged:
307 die('package is blacklisted: {:s}'.format(pkginfo['pkgname']))
309 cur.execute("SELECT COUNT(*) FROM Packages WHERE Name = %s AND " +
310 "PackageBaseID <> %s", [pkgname, pkgbase_id])
311 if cur.fetchone()[0] > 0:
312 die('cannot overwrite package: {:s}'.format(pkgname))
314 # Store package base details in the database.
315 save_srcinfo(srcinfo, db, cur, user)
317 db.close()
319 # Create (or update) a branch with the name of the package base for better
320 # accessibility.
321 repo.create_reference('refs/heads/' + pkgbase, sha1_new, True)
323 # Work around a Git bug: The HEAD ref is not updated when using gitnamespaces.
324 # This can be removed once the bug fix is included in Git mainline. See
325 # http://git.661346.n2.nabble.com/PATCH-receive-pack-Create-a-HEAD-ref-for-ref-namespace-td7632149.html
326 # for details.
327 repo.create_reference('refs/namespaces/' + pkgbase + '/HEAD', sha1_new, True)