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