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