1# This file is part of Buildbot. Buildbot is free software: you can 2# redistribute it and/or modify it under the terms of the GNU General Public 3# License as published by the Free Software Foundation, version 2. 4# 5# This program is distributed in the hope that it will be useful, but WITHOUT 6# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 7# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 8# details. 9# 10# You should have received a copy of the GNU General Public License along with 11# this program; if not, write to the Free Software Foundation, Inc., 51 12# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 13# 14# Copyright Buildbot Team Members 15 16import copy 17import json 18 19from twisted.internet import defer 20from twisted.python import log 21 22from buildbot.data import base 23from buildbot.data import sourcestamps 24from buildbot.data import types 25from buildbot.process import metrics 26from buildbot.process.users import users 27from buildbot.util import datetime2epoch 28from buildbot.util import epoch2datetime 29 30 31class FixerMixin: 32 33 @defer.inlineCallbacks 34 def _fixChange(self, change, is_graphql): 35 # TODO: make these mods in the DB API 36 if change: 37 change = change.copy() 38 change['when_timestamp'] = datetime2epoch(change['when_timestamp']) 39 if is_graphql: 40 props = change['properties'] 41 change['properties'] = [ 42 {'name': k, 'source': v[1], 'value': json.dumps(v[0])} 43 for k, v in props.items() 44 ] 45 else: 46 sskey = ('sourcestamps', str(change['sourcestampid'])) 47 change['sourcestamp'] = yield self.master.data.get(sskey) 48 del change['sourcestampid'] 49 return change 50 fieldMapping = { 51 'changeid': 'changes.changeid', 52 } 53 54 55class ChangeEndpoint(FixerMixin, base.Endpoint): 56 57 isCollection = False 58 pathPatterns = """ 59 /changes/n:changeid 60 """ 61 62 def get(self, resultSpec, kwargs): 63 d = self.master.db.changes.getChange(kwargs['changeid']) 64 d.addCallback(self._fixChange, is_graphql='graphql' in kwargs) 65 return d 66 67 68class ChangesEndpoint(FixerMixin, base.BuildNestingMixin, base.Endpoint): 69 70 isCollection = True 71 pathPatterns = """ 72 /changes 73 /builders/n:builderid/builds/n:build_number/changes 74 /builds/n:buildid/changes 75 /sourcestamps/n:ssid/changes 76 """ 77 rootLinkName = 'changes' 78 79 @defer.inlineCallbacks 80 def get(self, resultSpec, kwargs): 81 buildid = kwargs.get('buildid') 82 if 'build_number' in kwargs: 83 buildid = yield self.getBuildid(kwargs) 84 ssid = kwargs.get('ssid') 85 if buildid is not None: 86 changes = yield self.master.db.changes.getChangesForBuild(buildid) 87 elif ssid is not None: 88 change = yield self.master.db.changes.getChangeFromSSid(ssid) 89 if change is not None: 90 changes = [change] 91 else: 92 changes = [] 93 else: 94 if resultSpec is not None: 95 resultSpec.fieldMapping = self.fieldMapping 96 changes = yield self.master.db.changes.getChanges(resultSpec=resultSpec) 97 results = [] 98 for ch in changes: 99 results.append((yield self._fixChange(ch, is_graphql='graphql' in kwargs))) 100 return results 101 102 103class Change(base.ResourceType): 104 105 name = "change" 106 plural = "changes" 107 endpoints = [ChangeEndpoint, ChangesEndpoint] 108 eventPathPatterns = """ 109 /changes/:changeid 110 """ 111 keyField = "changeid" 112 subresources = ["Build", "Property"] 113 114 class EntityType(types.Entity): 115 changeid = types.Integer() 116 parent_changeids = types.List(of=types.Integer()) 117 author = types.String() 118 committer = types.String() 119 files = types.List(of=types.String()) 120 comments = types.String() 121 revision = types.NoneOk(types.String()) 122 when_timestamp = types.Integer() 123 branch = types.NoneOk(types.String()) 124 category = types.NoneOk(types.String()) 125 revlink = types.NoneOk(types.String()) 126 properties = types.SourcedProperties() 127 repository = types.String() 128 project = types.String() 129 codebase = types.String() 130 sourcestamp = sourcestamps.SourceStamp.entityType 131 entityType = EntityType(name, 'Change') 132 133 @base.updateMethod 134 @defer.inlineCallbacks 135 def addChange(self, files=None, comments=None, author=None, committer=None, revision=None, 136 when_timestamp=None, branch=None, category=None, revlink='', 137 properties=None, repository='', codebase=None, project='', 138 src=None): 139 metrics.MetricCountEvent.log("added_changes", 1) 140 141 if properties is None: 142 properties = {} 143 # add the source to the properties 144 for k in properties: 145 properties[k] = (properties[k], 'Change') 146 147 # get a user id 148 if src: 149 # create user object, returning a corresponding uid 150 uid = yield users.createUserObject(self.master, 151 author, src) 152 else: 153 uid = None 154 155 if not revlink and revision and repository and callable(self.master.config.revlink): 156 # generate revlink from revision and repository using the configured callable 157 revlink = self.master.config.revlink(revision, repository) or '' 158 159 if callable(category): 160 pre_change = self.master.config.preChangeGenerator(author=author, 161 committer=committer, 162 files=files, 163 comments=comments, 164 revision=revision, 165 when_timestamp=when_timestamp, 166 branch=branch, 167 revlink=revlink, 168 properties=properties, 169 repository=repository, 170 project=project) 171 category = category(pre_change) 172 173 # set the codebase, either the default, supplied, or generated 174 if codebase is None \ 175 and self.master.config.codebaseGenerator is not None: 176 pre_change = self.master.config.preChangeGenerator(author=author, 177 committer=committer, 178 files=files, 179 comments=comments, 180 revision=revision, 181 when_timestamp=when_timestamp, 182 branch=branch, 183 category=category, 184 revlink=revlink, 185 properties=properties, 186 repository=repository, 187 project=project) 188 codebase = self.master.config.codebaseGenerator(pre_change) 189 codebase = str(codebase) 190 else: 191 codebase = codebase or '' 192 193 # add the Change to the database 194 changeid = yield self.master.db.changes.addChange( 195 author=author, 196 committer=committer, 197 files=files, 198 comments=comments, 199 revision=revision, 200 when_timestamp=epoch2datetime(when_timestamp), 201 branch=branch, 202 category=category, 203 revlink=revlink, 204 properties=properties, 205 repository=repository, 206 codebase=codebase, 207 project=project, 208 uid=uid) 209 210 # get the change and munge the result for the notification 211 change = yield self.master.data.get(('changes', str(changeid))) 212 change = copy.deepcopy(change) 213 self.produceEvent(change, 'new') 214 215 # log, being careful to handle funny characters 216 msg = "added change with revision {} to database".format(revision) 217 log.msg(msg.encode('utf-8', 'replace')) 218 219 return changeid 220