1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 """
17 Support for changes in the database
18 """
19
20 from buildbot.util import json
21 import sqlalchemy as sa
22 from twisted.internet import defer, reactor
23 from buildbot.db import base
24 from buildbot.util import epoch2datetime, datetime2epoch
28
30 """
31 A DBConnectorComponent to handle getting changes into and out of the
32 database. An instance is available at C{master.db.changes}.
33
34 Changes are represented as dictionaries with the following keys:
35
36 - changeid: the ID of this change
37 - author: the author of the change (unicode string)
38 - files: list of source-code filenames changed (unicode strings)
39 - comments: user comments (unicode string)
40 - is_dir: deprecated
41 - links: list of links for this change, e.g., to web views, review
42 (unicode strings)
43 - revision: revision for this change (unicode string), or None if unknown
44 - when_timestamp: time of the commit (datetime instance)
45 - branch: branch on which the change took place (unicode string), or None
46 for the "default branch", whatever that might mean
47 - category: user-defined category of this change (unicode string or None)
48 - revlink: link to a web view of this change (unicode string or None)
49 - properties: user-specified properties for this change, represented as a
50 dictionary mapping keys to (value, source)
51 - repository: repository where this change occurred (unicode string)
52 - project: user-defined project to which this change corresponds (unicode
53 string)
54 """
55
56 - def addChange(self, author=None, files=None, comments=None, is_dir=0,
57 links=None, revision=None, when_timestamp=None, branch=None,
58 category=None, revlink='', properties={}, repository='',
59 project='', _reactor=reactor):
60 """Add a Change with the given attributes to the database; returns
61 a Change instance via a deferred. All arguments are keyword arguments.
62
63 @param author: the author of this change
64 @type author: unicode string
65
66 @param files: a list of filenames that were changed
67 @type branch: list of unicode strings
68
69 @param comments: user comments on the change
70 @type branch: unicode string
71
72 @param is_dir: deprecated
73
74 @param links: a list of links related to this change, e.g., to web
75 viewers or review pages
76 @type links: list of unicode strings
77
78 @param revision: the revision identifier for this change
79 @type revision: unicode string
80
81 @param when_timestamp: when this change occurred, or the current time
82 if None
83 @type when_timestamp: datetime instance or None
84
85 @param branch: the branch on which this change took place
86 @type branch: unicode string
87
88 @param category: category for this change (arbitrary use by Buildbot
89 users)
90 @type category: unicode string
91
92 @param revlink: link to a web view of this revision
93 @type revlink: unicode string
94
95 @param properties: properties to set on this change
96 @type properties: dictionary, where values are tuples of (value,
97 source). At the moment, the source must be C{'Change'}, although
98 this may be relaxed in later versions.
99
100 @param repository: the repository in which this change took place
101 @type repository: unicode string
102
103 @param project: the project this change is a part of
104 @type project: unicode string
105
106 @param _reactor: for testing
107
108 @returns: new change's ID via Deferred
109 """
110 assert project is not None, "project must be a string, not None"
111 assert repository is not None, "repository must be a string, not None"
112
113 if when_timestamp is None:
114 when_timestamp = epoch2datetime(_reactor.seconds())
115
116
117 for pv in properties.values():
118 assert pv[1] == 'Change', ("properties must be qualified with"
119 "source 'Change'")
120
121 def thd(conn):
122
123
124
125
126
127
128 transaction = conn.begin()
129
130 ins = self.db.model.changes.insert()
131 r = conn.execute(ins, dict(
132 author=author,
133 comments=comments,
134 is_dir=is_dir,
135 branch=branch,
136 revision=revision,
137 revlink=revlink,
138 when_timestamp=datetime2epoch(when_timestamp),
139 category=category,
140 repository=repository,
141 project=project))
142 changeid = r.inserted_primary_key[0]
143 if links:
144 ins = self.db.model.change_links.insert()
145 conn.execute(ins, [
146 dict(changeid=changeid, link=l)
147 for l in links
148 ])
149 if files:
150 ins = self.db.model.change_files.insert()
151 conn.execute(ins, [
152 dict(changeid=changeid, filename=f)
153 for f in files
154 ])
155 if properties:
156 ins = self.db.model.change_properties.insert()
157 conn.execute(ins, [
158 dict(changeid=changeid,
159 property_name=k,
160 property_value=json.dumps(v))
161 for k,v in properties.iteritems()
162 ])
163
164 transaction.commit()
165
166 return changeid
167 d = self.db.pool.do(thd)
168 return d
169
170 @base.cached("chdicts")
172 """
173 Get a change dictionary for the given changeid, or None if no such
174 change exists.
175
176 @param changeid: the id of the change instance to fetch
177
178 @param no_cache: bypass cache and always fetch from database
179 @type no_cache: boolean
180
181 @returns: Change dictionary via Deferred
182 """
183 assert changeid >= 0
184 def thd(conn):
185
186 changes_tbl = self.db.model.changes
187 q = changes_tbl.select(whereclause=(changes_tbl.c.changeid == changeid))
188 rp = conn.execute(q)
189 row = rp.fetchone()
190 if not row:
191 return None
192
193 return self._chdict_from_change_row_thd(conn, row)
194 d = self.db.pool.do(thd)
195 return d
196
198 """
199 Get a list of the C{count} most recent changes, represented as
200 dictionaies; returns fewer if that many do not exist.
201
202 @param count: maximum number of instances to return
203
204 @returns: list of dictionaries via Deferred, ordered by changeid
205 """
206 def thd(conn):
207
208 changes_tbl = self.db.model.changes
209 q = sa.select([changes_tbl.c.changeid],
210 order_by=[sa.desc(changes_tbl.c.changeid)],
211 limit=count)
212 rp = conn.execute(q)
213 changeids = [ row.changeid for row in rp ]
214 rp.close()
215 return list(reversed(changeids))
216 d = self.db.pool.do(thd)
217
218
219 def get_changes(changeids):
220 return defer.gatherResults([ self.getChange(changeid)
221 for changeid in changeids ])
222 d.addCallback(get_changes)
223 return d
224
226 """
227 Get the most-recently-assigned changeid, or None if there are no
228 changes at all.
229
230 @returns: changeid via Deferred
231 """
232 def thd(conn):
233 changes_tbl = self.db.model.changes
234 q = sa.select([ changes_tbl.c.changeid ],
235 order_by=sa.desc(changes_tbl.c.changeid),
236 limit=1)
237 return conn.scalar(q)
238 d = self.db.pool.do(thd)
239 return d
240
241
242
244 if not changeHorizon:
245 return defer.succeed(None)
246 def thd(conn):
247 changes_tbl = self.db.model.changes
248
249
250
251
252
253
254 q = sa.select([changes_tbl.c.changeid],
255 order_by=[sa.desc(changes_tbl.c.changeid)],
256 offset=changeHorizon)
257 res = conn.execute(q)
258 ids_to_delete = [ r.changeid for r in res ]
259
260
261 for table_name in ('scheduler_changes', 'sourcestamp_changes',
262 'change_files', 'change_links',
263 'change_properties', 'changes'):
264 table = self.db.model.metadata.tables[table_name]
265 conn.execute(
266 table.delete(table.c.changeid.in_(ids_to_delete)))
267 return self.db.pool.do(thd)
268
279
280 chdict = ChDict(
281 changeid=ch_row.changeid,
282 author=ch_row.author,
283 files=[],
284 comments=ch_row.comments,
285 is_dir=ch_row.is_dir,
286 links=[],
287 revision=ch_row.revision,
288 when_timestamp=mkdt(ch_row.when_timestamp),
289 branch=ch_row.branch,
290 category=ch_row.category,
291 revlink=ch_row.revlink,
292 properties={},
293 repository=ch_row.repository,
294 project=ch_row.project)
295
296 query = change_links_tbl.select(
297 whereclause=(change_links_tbl.c.changeid == ch_row.changeid))
298 rows = conn.execute(query)
299 for r in rows:
300 chdict['links'].append(r.link)
301
302 query = change_files_tbl.select(
303 whereclause=(change_files_tbl.c.changeid == ch_row.changeid))
304 rows = conn.execute(query)
305 for r in rows:
306 chdict['files'].append(r.filename)
307
308
309
310
311 def split_vs(vs):
312 try:
313 v,s = vs
314 if s != "Change":
315 v,s = vs, "Change"
316 except:
317 v,s = vs, "Change"
318 return v, s
319
320 query = change_properties_tbl.select(
321 whereclause=(change_properties_tbl.c.changeid == ch_row.changeid))
322 rows = conn.execute(query)
323 for r in rows:
324 v, s = split_vs(json.loads(r.property_value))
325 chdict['properties'][r.property_name] = (v,s)
326
327 return chdict
328