Jump to latest comments after adding a comment
[aur.git] / git-interface / git-update.py
blob935fa5bbd17ee2af173c6a6abbdf1b56cecbe2f6
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 if len(sys.argv) != 4:
180 die("invalid arguments")
182 refname = sys.argv[1]
183 sha1_old = sys.argv[2]
184 sha1_new = sys.argv[3]
186 user = os.environ.get("AUR_USER")
187 pkgbase = os.environ.get("AUR_PKGBASE")
188 privileged = (os.environ.get("AUR_PRIVILEGED", '0') == '1')
190 if refname != "refs/heads/master":
191 die("pushing to a branch other than master is restricted")
193 repo = pygit2.Repository(repo_path)
195 db = mysql.connector.connect(host=aur_db_host, user=aur_db_user,
196 passwd=aur_db_pass, db=aur_db_name,
197 unix_socket=aur_db_socket, buffered=True)
198 cur = db.cursor()
200 # Detect and deny non-fast-forwards.
201 if sha1_old != "0000000000000000000000000000000000000000":
202 walker = repo.walk(sha1_old, pygit2.GIT_SORT_TOPOLOGICAL)
203 walker.hide(sha1_new)
204 if next(walker, None) != None:
205 cur.execute("SELECT AccountTypeID FROM Users WHERE UserName = %s ",
206 [user])
207 if cur.fetchone()[0] == 1:
208 die("denying non-fast-forward (you should pull first)")
210 # Prepare the walker that validates new commits.
211 walker = repo.walk(sha1_new, pygit2.GIT_SORT_TOPOLOGICAL)
212 if sha1_old != "0000000000000000000000000000000000000000":
213 walker.hide(sha1_old)
215 cur.execute("SELECT Name FROM PackageBlacklist")
216 blacklist = [row[0] for row in cur.fetchall()]
218 for commit in walker:
219 for fname in ('.SRCINFO', 'PKGBUILD'):
220 if not fname in commit.tree:
221 die_commit("missing {:s}".format(fname), str(commit.id))
223 for treeobj in commit.tree:
224 blob = repo[treeobj.id]
226 if isinstance(blob, pygit2.Tree):
227 die_commit("the repository must not contain subdirectories",
228 str(commit.id))
230 if not isinstance(blob, pygit2.Blob):
231 die_commit("not a blob object: {:s}".format(treeobj), str(commit.id))
233 if blob.size > 250000:
234 die_commit("maximum blob size (250kB) exceeded", str(commit.id))
236 srcinfo_raw = repo[commit.tree['.SRCINFO'].id].data.decode()
237 srcinfo_raw = srcinfo_raw.split('\n')
238 ecatcher = aurinfo.CollectionECatcher()
239 srcinfo = aurinfo.ParseAurinfoFromIterable(srcinfo_raw, ecatcher)
240 errors = ecatcher.Errors()
241 if errors:
242 sys.stderr.write("error: The following errors occurred "
243 "when parsing .SRCINFO in commit\n")
244 sys.stderr.write("error: {:s}:\n".format(str(commit.id)))
245 for error in errors:
246 sys.stderr.write("error: line {:d}: {:s}\n".format(*error))
247 exit(1)
249 srcinfo_pkgbase = srcinfo._pkgbase['pkgname']
250 if not re.match(repo_regex, srcinfo_pkgbase):
251 die_commit('invalid pkgbase: {:s}'.format(srcinfo_pkgbase), str(commit.id))
253 for pkgname in srcinfo.GetPackageNames():
254 pkginfo = srcinfo.GetMergedPackage(pkgname)
256 for field in ('pkgver', 'pkgrel', 'pkgname'):
257 if not field in pkginfo:
258 die_commit('missing mandatory field: {:s}'.format(field), str(commit.id))
260 if 'epoch' in pkginfo and not pkginfo['epoch'].isdigit():
261 die_commit('invalid epoch: {:s}'.format(pkginfo['epoch']), str(commit.id))
263 if not re.match(r'[a-z0-9][a-z0-9\.+_-]*$', pkginfo['pkgname']):
264 die_commit('invalid package name: {:s}'.format(pkginfo['pkgname']),
265 str(commit.id))
267 for field in ('pkgname', 'pkgdesc', 'url'):
268 if field in pkginfo and len(pkginfo[field]) > 255:
269 die_commit('{:s} field too long: {:s}'.format(field, pkginfo[field]),
270 str(commit.id))
272 for field in ('install', 'changelog'):
273 if field in pkginfo and not pkginfo[field] in commit.tree:
274 die_commit('missing {:s} file: {:s}'.format(field, pkginfo[field]),
275 str(commit.id))
277 for field in extract_arch_fields(pkginfo, 'source'):
278 fname = field['value']
279 if "://" in fname or "lp:" in fname:
280 continue
281 if not fname in commit.tree:
282 die_commit('missing source file: {:s}'.format(fname), str(commit.id))
284 srcinfo_raw = repo[repo[sha1_new].tree['.SRCINFO'].id].data.decode()
285 srcinfo_raw = srcinfo_raw.split('\n')
286 srcinfo = aurinfo.ParseAurinfoFromIterable(srcinfo_raw)
288 srcinfo_pkgbase = srcinfo._pkgbase['pkgname']
289 if srcinfo_pkgbase != pkgbase:
290 die('invalid pkgbase: {:s}, expected {:s}'.format(srcinfo_pkgbase, pkgbase))
292 pkgbase = srcinfo._pkgbase['pkgname']
293 cur.execute("SELECT ID FROM PackageBases WHERE Name = %s", [pkgbase])
294 pkgbase_id = cur.fetchone()[0]
296 for pkgname in srcinfo.GetPackageNames():
297 pkginfo = srcinfo.GetMergedPackage(pkgname)
298 pkgname = pkginfo['pkgname']
300 if pkgname in blacklist and not privileged:
301 die('package is blacklisted: {:s}'.format(pkginfo['pkgname']))
303 cur.execute("SELECT COUNT(*) FROM Packages WHERE Name = %s AND " +
304 "PackageBaseID <> %s", [pkgname, pkgbase_id])
305 if cur.fetchone()[0] > 0:
306 die('cannot overwrite package: {:s}'.format(pkgname))
308 save_srcinfo(srcinfo, db, cur, user)
310 db.close()
312 # Create (or update) a branch with the name of the package base for better
313 # accessibility.
314 repo.create_reference('refs/heads/' + pkgbase, sha1_new, True)
316 # Work around a Git bug: The HEAD ref is not updated when using gitnamespaces.
317 # This can be removed once the bug fix is included in Git mainline. See
318 # http://git.661346.n2.nabble.com/PATCH-receive-pack-Create-a-HEAD-ref-for-ref-namespace-td7632149.html
319 # for details.
320 repo.create_reference('refs/namespaces/' + pkgbase + '/HEAD', sha1_new, True)