Merge ~ilasc/launchpad:revision-status-submission-api into launchpad:master
- Git
- lp:~ilasc/launchpad
- revision-status-submission-api
- Merge into master
Status: | Merged |
---|---|
Approved by: | Ioana Lasc |
Approved revision: | 52d3757e7b293bad0e038c6e85e96b0b08089e43 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~ilasc/launchpad:revision-status-submission-api |
Merge into: | launchpad:master |
Diff against target: |
953 lines (+625/-3) 11 files modified
lib/lp/_schema_circular_imports.py (+8/-1) lib/lp/code/browser/configure.zcml (+6/-0) lib/lp/code/browser/gitrepository.py (+13/-0) lib/lp/code/configure.zcml (+34/-0) lib/lp/code/enums.py (+50/-0) lib/lp/code/interfaces/gitrepository.py (+180/-0) lib/lp/code/interfaces/webservice.py (+2/-0) lib/lp/code/model/gitrepository.py (+147/-1) lib/lp/code/model/tests/test_gitrepository.py (+137/-0) lib/lp/security.py (+19/-0) lib/lp/testing/factory.py (+29/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+410373@code.launchpad.net |
Commit message
Add revision status submission API
Description of the change
Colin Watson (cjwatson) : | # |
Colin Watson (cjwatson) : | # |
Colin Watson (cjwatson) : | # |
Ioana Lasc (ilasc) wrote : | # |
Colin Watson (cjwatson) wrote : | # |
There is a *lot* of formatting noise in this MP; for example it isn't at all clear what substantive changes have been made to `lib/lp/
Some of the rest of my comments may collide with work you've already done locally; if so, sorry about that, and hopefully you can make sense of them. I think these comments should at least be enough to let you move forward, although they aren't a complete review yet.
Colin Watson (cjwatson) : | # |
Ioana Lasc (ilasc) wrote : | # |
MP now ready for review.
Colin Watson (cjwatson) wrote : | # |
Getting there, despite the large volume of comments here!
I think the main thing is to make sure that the API is the right sort of shape so that we don't paint ourselves into a corner. Consider making the API method that creates a new report check a feature rule for the time being, so that we can limit this to only authorized clients until we're sure that we're ready.
Ioana Lasc (ilasc) wrote : | # |
While a large portion of comments have been updated on this MP there is still below set of things currently being worked on:
For this MP:
1: make the API method that creates a new report check a feature rule for the time being, so that we can limit this to only authorized clients until we're sure that we're ready.
2: export a method that takes the log as bytes, and creates the artifact object internally
3: Unit test for setLog over the API
4: on RevisionStatusA
5: add at least a basic security adapter for the newky added IRevisionStatus
7: Unit Test for the result mutator
Next MP / Jira Issue:
1: putting `IRevisionStatus*` in a separate file (and the same for the implementation) rather than having it in the same module as `IGitRepository*`
Colin Watson (cjwatson) wrote : | # |
I understand this is still work in progress, so I haven't done a full review, but I had a quick look through this today and had a couple of comments that might be useful to you at this stage.
Ioana Lasc (ilasc) wrote : | # |
Indeed the comments were helpful Colin, thanks! This is now ready for review together with the DB patch (https:/
Colin Watson (cjwatson) wrote : | # |
Summary of the main points here, after which I think this will be good to go:
* fix ambiguity in `RevisionStatus
* export `IRevisionStatu
* fix type of `IRevisionStatu
* declare a proper feature rule exception
* fix confusing definition of `EditRevisionSt
Ioana Lasc (ilasc) wrote : | # |
Thanks Colin! Addressed all comments and when moving TestRevisionSta
Added TestRevisionSta
HTTP/1.1 404 Not Found
Content-Length: 112
Content-Type: text/plain;
Object: <GitRepository '~person-
Looks like I need to define traversal / navigation for reports.
Ioana Lasc (ilasc) wrote : | # |
Navigation now works and DB patch changes are included in this version of the MP.
I do have however one last issue in test_setLogOnRe
Colin Watson (cjwatson) wrote : | # |
Iiiiiinteresting.
The problem here is that access tokens are scoped to a particular target, and if the context isn't exactly equal to the target then authenticating with an access token fails. We could work around this by moving the `setLog` method back to `IGitRepository` (with a more precise way to identify the correct revision status report), but I don't think that's really the best approach. Instead, I've proposed https:/
Preview Diff
1 | diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py | |||
2 | index af21a01..e239ac2 100644 | |||
3 | --- a/lib/lp/_schema_circular_imports.py | |||
4 | +++ b/lib/lp/_schema_circular_imports.py | |||
5 | @@ -64,7 +64,10 @@ from lp.code.interfaces.codereviewcomment import ICodeReviewComment | |||
6 | 64 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference | 64 | from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference |
7 | 65 | from lp.code.interfaces.diff import IPreviewDiff | 65 | from lp.code.interfaces.diff import IPreviewDiff |
8 | 66 | from lp.code.interfaces.gitref import IGitRef | 66 | from lp.code.interfaces.gitref import IGitRef |
10 | 67 | from lp.code.interfaces.gitrepository import IGitRepository | 67 | from lp.code.interfaces.gitrepository import ( |
11 | 68 | IGitRepository, | ||
12 | 69 | IRevisionStatusReport, | ||
13 | 70 | ) | ||
14 | 68 | from lp.code.interfaces.gitrule import ( | 71 | from lp.code.interfaces.gitrule import ( |
15 | 69 | IGitNascentRule, | 72 | IGitNascentRule, |
16 | 70 | IGitNascentRuleGrant, | 73 | IGitNascentRuleGrant, |
17 | @@ -528,6 +531,10 @@ patch_collection_return_type( | |||
18 | 528 | patch_list_parameter_type( | 531 | patch_list_parameter_type( |
19 | 529 | IGitRepository, 'setRules', 'rules', InlineObject(schema=IGitNascentRule)) | 532 | IGitRepository, 'setRules', 'rules', InlineObject(schema=IGitNascentRule)) |
20 | 530 | 533 | ||
21 | 534 | # IRevisionStatusReport | ||
22 | 535 | patch_reference_property( | ||
23 | 536 | IRevisionStatusReport, 'git_repository', IGitRepository) | ||
24 | 537 | |||
25 | 531 | # ILiveFSFile | 538 | # ILiveFSFile |
26 | 532 | patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild) | 539 | patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild) |
27 | 533 | 540 | ||
28 | diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml | |||
29 | index f7bab39..31b3938 100644 | |||
30 | --- a/lib/lp/code/browser/configure.zcml | |||
31 | +++ b/lib/lp/code/browser/configure.zcml | |||
32 | @@ -963,6 +963,12 @@ | |||
33 | 963 | factory="lp.code.browser.gitrepository.GitRepositoryBreadcrumb" | 963 | factory="lp.code.browser.gitrepository.GitRepositoryBreadcrumb" |
34 | 964 | permission="zope.Public"/> | 964 | permission="zope.Public"/> |
35 | 965 | 965 | ||
36 | 966 | <browser:url | ||
37 | 967 | for="lp.code.interfaces.gitrepository.IRevisionStatusReport" | ||
38 | 968 | path_expression="string:+status/${id}" | ||
39 | 969 | attribute_to_parent="git_repository" | ||
40 | 970 | rootsite="code"/> | ||
41 | 971 | |||
42 | 966 | <browser:defaultView | 972 | <browser:defaultView |
43 | 967 | for="lp.code.interfaces.gitref.IGitRef" | 973 | for="lp.code.interfaces.gitref.IGitRef" |
44 | 968 | name="+index"/> | 974 | name="+index"/> |
45 | diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py | |||
46 | index 92a1b14..ddd79ca 100644 | |||
47 | --- a/lib/lp/code/browser/gitrepository.py | |||
48 | +++ b/lib/lp/code/browser/gitrepository.py | |||
49 | @@ -106,6 +106,7 @@ from lp.code.interfaces.gitrepository import ( | |||
50 | 106 | ContributorGitIdentity, | 106 | ContributorGitIdentity, |
51 | 107 | IGitRepository, | 107 | IGitRepository, |
52 | 108 | IGitRepositorySet, | 108 | IGitRepositorySet, |
53 | 109 | IRevisionStatusReportSet, | ||
54 | 109 | ) | 110 | ) |
55 | 110 | from lp.code.vocabularies.gitrule import GitPermissionsVocabulary | 111 | from lp.code.vocabularies.gitrule import GitPermissionsVocabulary |
56 | 111 | from lp.registry.interfaces.person import ( | 112 | from lp.registry.interfaces.person import ( |
57 | @@ -190,6 +191,18 @@ class GitRepositoryNavigation(WebhookTargetNavigationMixin, Navigation): | |||
58 | 190 | 191 | ||
59 | 191 | usedfor = IGitRepository | 192 | usedfor = IGitRepository |
60 | 192 | 193 | ||
61 | 194 | @stepthrough('+status') | ||
62 | 195 | def traverse_status(self, id): | ||
63 | 196 | try: | ||
64 | 197 | report_id = int(id) | ||
65 | 198 | except ValueError: | ||
66 | 199 | raise NotFoundError(report_id) | ||
67 | 200 | report = getUtility( | ||
68 | 201 | IRevisionStatusReportSet).getByID(report_id) | ||
69 | 202 | if report is None: | ||
70 | 203 | raise NotFoundError(report_id) | ||
71 | 204 | return report | ||
72 | 205 | |||
73 | 193 | @stepto("+ref") | 206 | @stepto("+ref") |
74 | 194 | def traverse_ref(self): | 207 | def traverse_ref(self): |
75 | 195 | segments = list(self.request.getTraversalStack()) | 208 | segments = list(self.request.getTraversalStack()) |
76 | diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml | |||
77 | index 6417c89..fb8e0d5 100644 | |||
78 | --- a/lib/lp/code/configure.zcml | |||
79 | +++ b/lib/lp/code/configure.zcml | |||
80 | @@ -953,6 +953,40 @@ | |||
81 | 953 | <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" /> | 953 | <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" /> |
82 | 954 | </securedutility> | 954 | </securedutility> |
83 | 955 | 955 | ||
84 | 956 | <!-- RevisionStatusReport --> | ||
85 | 957 | |||
86 | 958 | <class class="lp.code.model.gitrepository.RevisionStatusReport"> | ||
87 | 959 | <require | ||
88 | 960 | permission="launchpad.View" | ||
89 | 961 | interface="lp.code.interfaces.gitrepository.IRevisionStatusReportView | ||
90 | 962 | lp.code.interfaces.gitrepository.IRevisionStatusReportEditableAttributes" /> | ||
91 | 963 | <require | ||
92 | 964 | permission="launchpad.Edit" | ||
93 | 965 | interface="lp.code.interfaces.gitrepository.IRevisionStatusReportEdit" | ||
94 | 966 | set_schema="lp.code.interfaces.gitrepository.IRevisionStatusReportEditableAttributes" /> | ||
95 | 967 | </class> | ||
96 | 968 | <class class="lp.code.model.gitrepository.RevisionStatusArtifact"> | ||
97 | 969 | <require | ||
98 | 970 | permission="launchpad.View" | ||
99 | 971 | interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifact" /> | ||
100 | 972 | </class> | ||
101 | 973 | <class class="lp.code.model.gitrepository.RevisionStatusReportSet"> | ||
102 | 974 | <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusReportSet" /> | ||
103 | 975 | </class> | ||
104 | 976 | <securedutility | ||
105 | 977 | class="lp.code.model.gitrepository.RevisionStatusReportSet" | ||
106 | 978 | provides="lp.code.interfaces.gitrepository.IRevisionStatusReportSet"> | ||
107 | 979 | <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusReportSet" /> | ||
108 | 980 | </securedutility> | ||
109 | 981 | <class class="lp.code.model.gitrepository.RevisionStatusArtifactSet"> | ||
110 | 982 | <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet" /> | ||
111 | 983 | </class> | ||
112 | 984 | <securedutility | ||
113 | 985 | class="lp.code.model.gitrepository.RevisionStatusArtifactSet" | ||
114 | 986 | provides="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet"> | ||
115 | 987 | <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet" /> | ||
116 | 988 | </securedutility> | ||
117 | 989 | |||
118 | 956 | <!-- Git repository access rules --> | 990 | <!-- Git repository access rules --> |
119 | 957 | 991 | ||
120 | 958 | <class class="lp.code.model.gitrule.GitRule"> | 992 | <class class="lp.code.model.gitrule.GitRule"> |
121 | diff --git a/lib/lp/code/enums.py b/lib/lp/code/enums.py | |||
122 | index 3765754..2dbd287 100644 | |||
123 | --- a/lib/lp/code/enums.py | |||
124 | +++ b/lib/lp/code/enums.py | |||
125 | @@ -29,6 +29,8 @@ __all__ = [ | |||
126 | 29 | 'GitRepositoryType', | 29 | 'GitRepositoryType', |
127 | 30 | 'NON_CVS_RCS_TYPES', | 30 | 'NON_CVS_RCS_TYPES', |
128 | 31 | 'RevisionControlSystems', | 31 | 'RevisionControlSystems', |
129 | 32 | 'RevisionStatusArtifactType', | ||
130 | 33 | 'RevisionStatusResult', | ||
131 | 32 | 'TargetRevisionControlSystems', | 34 | 'TargetRevisionControlSystems', |
132 | 33 | ] | 35 | ] |
133 | 34 | 36 | ||
134 | @@ -252,6 +254,54 @@ class GitPermissionType(EnumeratedType): | |||
135 | 252 | CAN_FORCE_PUSH = Item("Can force-push") | 254 | CAN_FORCE_PUSH = Item("Can force-push") |
136 | 253 | 255 | ||
137 | 254 | 256 | ||
138 | 257 | class RevisionStatusArtifactType(DBEnumeratedType): | ||
139 | 258 | LOG = DBItem(0, """ | ||
140 | 259 | Log | ||
141 | 260 | |||
142 | 261 | The log produced by the check job. | ||
143 | 262 | """) | ||
144 | 263 | |||
145 | 264 | |||
146 | 265 | class RevisionStatusResult(DBEnumeratedType): | ||
147 | 266 | """Revision Status Result""" | ||
148 | 267 | |||
149 | 268 | WAITING = DBItem(0, """ | ||
150 | 269 | Waiting | ||
151 | 270 | |||
152 | 271 | The check job is waiting to be run. | ||
153 | 272 | """) | ||
154 | 273 | |||
155 | 274 | RUNNING = DBItem(1, """ | ||
156 | 275 | Running | ||
157 | 276 | |||
158 | 277 | The check job is currently running. | ||
159 | 278 | """) | ||
160 | 279 | |||
161 | 280 | SUCCEEDED = DBItem(2, """ | ||
162 | 281 | Succeeded | ||
163 | 282 | |||
164 | 283 | The check job ran successfully. | ||
165 | 284 | """) | ||
166 | 285 | |||
167 | 286 | FAILED = DBItem(3, """ | ||
168 | 287 | Failed | ||
169 | 288 | |||
170 | 289 | The check job failed. | ||
171 | 290 | """) | ||
172 | 291 | |||
173 | 292 | SKIPPED = DBItem(4, """ | ||
174 | 293 | Skipped | ||
175 | 294 | |||
176 | 295 | The check job was skipped. | ||
177 | 296 | """) | ||
178 | 297 | |||
179 | 298 | CANCELLED = DBItem(5, """ | ||
180 | 299 | Cancelled | ||
181 | 300 | |||
182 | 301 | The check job was cancelled. | ||
183 | 302 | """) | ||
184 | 303 | |||
185 | 304 | |||
186 | 255 | class BranchLifecycleStatusFilter(EnumeratedType): | 305 | class BranchLifecycleStatusFilter(EnumeratedType): |
187 | 256 | """Branch Lifecycle Status Filter | 306 | """Branch Lifecycle Status Filter |
188 | 257 | 307 | ||
189 | diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py | |||
190 | index cb12a5d..47fb9a6 100644 | |||
191 | --- a/lib/lp/code/interfaces/gitrepository.py | |||
192 | +++ b/lib/lp/code/interfaces/gitrepository.py | |||
193 | @@ -13,9 +13,15 @@ __all__ = [ | |||
194 | 13 | 'IGitRepositoryExpensiveRequest', | 13 | 'IGitRepositoryExpensiveRequest', |
195 | 14 | 'IGitRepositorySet', | 14 | 'IGitRepositorySet', |
196 | 15 | 'IHasGitRepositoryURL', | 15 | 'IHasGitRepositoryURL', |
197 | 16 | 'IRevisionStatusArtifact', | ||
198 | 17 | 'IRevisionStatusArtifactSet', | ||
199 | 18 | 'IRevisionStatusReport', | ||
200 | 19 | 'IRevisionStatusReportSet', | ||
201 | 20 | 'RevisionStatusReportsFeatureDisabled', | ||
202 | 16 | 'user_has_special_git_repository_access', | 21 | 'user_has_special_git_repository_access', |
203 | 17 | ] | 22 | ] |
204 | 18 | 23 | ||
205 | 24 | import http.client | ||
206 | 19 | import re | 25 | import re |
207 | 20 | from textwrap import dedent | 26 | from textwrap import dedent |
208 | 21 | 27 | ||
209 | @@ -23,6 +29,7 @@ from lazr.lifecycle.snapshot import doNotSnapshot | |||
210 | 23 | from lazr.restful.declarations import ( | 29 | from lazr.restful.declarations import ( |
211 | 24 | call_with, | 30 | call_with, |
212 | 25 | collection_default_content, | 31 | collection_default_content, |
213 | 32 | error_status, | ||
214 | 26 | export_destructor_operation, | 33 | export_destructor_operation, |
215 | 27 | export_factory_operation, | 34 | export_factory_operation, |
216 | 28 | export_operation_as, | 35 | export_operation_as, |
217 | @@ -37,6 +44,7 @@ from lazr.restful.declarations import ( | |||
218 | 37 | operation_returns_collection_of, | 44 | operation_returns_collection_of, |
219 | 38 | operation_returns_entry, | 45 | operation_returns_entry, |
220 | 39 | REQUEST_USER, | 46 | REQUEST_USER, |
221 | 47 | scoped, | ||
222 | 40 | ) | 48 | ) |
223 | 41 | from lazr.restful.fields import ( | 49 | from lazr.restful.fields import ( |
224 | 42 | CollectionField, | 50 | CollectionField, |
225 | @@ -50,6 +58,7 @@ from zope.interface import ( | |||
226 | 50 | ) | 58 | ) |
227 | 51 | from zope.schema import ( | 59 | from zope.schema import ( |
228 | 52 | Bool, | 60 | Bool, |
229 | 61 | Bytes, | ||
230 | 53 | Choice, | 62 | Choice, |
231 | 54 | Datetime, | 63 | Datetime, |
232 | 55 | Int, | 64 | Int, |
233 | @@ -57,10 +66,12 @@ from zope.schema import ( | |||
234 | 57 | Text, | 66 | Text, |
235 | 58 | TextLine, | 67 | TextLine, |
236 | 59 | ) | 68 | ) |
237 | 69 | from zope.security.interfaces import Unauthorized | ||
238 | 60 | 70 | ||
239 | 61 | from lp import _ | 71 | from lp import _ |
240 | 62 | from lp.app.enums import InformationType | 72 | from lp.app.enums import InformationType |
241 | 63 | from lp.app.validators import LaunchpadValidationError | 73 | from lp.app.validators import LaunchpadValidationError |
242 | 74 | from lp.app.validators.attachment import attachment_size_constraint | ||
243 | 64 | from lp.code.enums import ( | 75 | from lp.code.enums import ( |
244 | 65 | BranchMergeProposalStatus, | 76 | BranchMergeProposalStatus, |
245 | 66 | BranchSubscriptionDiffSize, | 77 | BranchSubscriptionDiffSize, |
246 | @@ -69,6 +80,8 @@ from lp.code.enums import ( | |||
247 | 69 | GitListingSort, | 80 | GitListingSort, |
248 | 70 | GitRepositoryStatus, | 81 | GitRepositoryStatus, |
249 | 71 | GitRepositoryType, | 82 | GitRepositoryType, |
250 | 83 | RevisionStatusArtifactType, | ||
251 | 84 | RevisionStatusResult, | ||
252 | 72 | ) | 85 | ) |
253 | 73 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository | 86 | from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
254 | 74 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories | 87 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
255 | @@ -91,6 +104,7 @@ from lp.services.fields import ( | |||
256 | 91 | InlineObject, | 104 | InlineObject, |
257 | 92 | PersonChoice, | 105 | PersonChoice, |
258 | 93 | PublicPersonChoice, | 106 | PublicPersonChoice, |
259 | 107 | URIField, | ||
260 | 94 | ) | 108 | ) |
261 | 95 | from lp.services.webhooks.interfaces import IWebhookTarget | 109 | from lp.services.webhooks.interfaces import IWebhookTarget |
262 | 96 | 110 | ||
263 | @@ -813,6 +827,152 @@ class IGitRepositoryExpensiveRequest(Interface): | |||
264 | 813 | that is not an admin or a registry expert.""" | 827 | that is not an admin or a registry expert.""" |
265 | 814 | 828 | ||
266 | 815 | 829 | ||
267 | 830 | @error_status(http.client.UNAUTHORIZED) | ||
268 | 831 | class RevisionStatusReportsFeatureDisabled(Unauthorized): | ||
269 | 832 | """Only certain users can access APIs for revision status reports.""" | ||
270 | 833 | |||
271 | 834 | def __init__(self): | ||
272 | 835 | super(RevisionStatusReportsFeatureDisabled, self).__init__( | ||
273 | 836 | "You do not have permission to create revision status reports") | ||
274 | 837 | |||
275 | 838 | |||
276 | 839 | class IRevisionStatusReportView(Interface): | ||
277 | 840 | """`IRevisionStatusReport` attributes that require launchpad.View.""" | ||
278 | 841 | |||
279 | 842 | id = Int(title=_("ID"), required=True, readonly=True) | ||
280 | 843 | |||
281 | 844 | date_created = exported(Datetime( | ||
282 | 845 | title=_("When the report was created."), required=True, readonly=True)) | ||
283 | 846 | date_started = exported(Datetime( | ||
284 | 847 | title=_("When the report was started.")), readonly=False) | ||
285 | 848 | date_finished = exported(Datetime( | ||
286 | 849 | title=_("When the report has finished.")), readonly=False) | ||
287 | 850 | |||
288 | 851 | |||
289 | 852 | class IRevisionStatusReportEditableAttributes(Interface): | ||
290 | 853 | """`IRevisionStatusReport` attributes that can be edited. | ||
291 | 854 | |||
292 | 855 | These attributes need launchpad.View to see, and launchpad.Edit to change. | ||
293 | 856 | """ | ||
294 | 857 | |||
295 | 858 | title = exported(TextLine( | ||
296 | 859 | title=_("A short title for the report."), required=True)) | ||
297 | 860 | |||
298 | 861 | git_repository = exported(Reference( | ||
299 | 862 | title=_("The Git repository for which this report is built."), | ||
300 | 863 | # Really IGitRepository, patched in _schema_circular_imports.py. | ||
301 | 864 | schema=Interface, required=True, readonly=True)) | ||
302 | 865 | |||
303 | 866 | commit_sha1 = exported(TextLine( | ||
304 | 867 | title=_("The Git commit for which this report is built."), | ||
305 | 868 | required=True, readonly=True)) | ||
306 | 869 | |||
307 | 870 | url = exported(URIField(title=_("URL"), required=False, readonly=True, | ||
308 | 871 | description=_("The external url of the report."))) | ||
309 | 872 | |||
310 | 873 | result_summary = exported(TextLine( | ||
311 | 874 | title=_("A short summary of the result."), required=False)) | ||
312 | 875 | |||
313 | 876 | result = exported(Choice( | ||
314 | 877 | title=_('Result of the report'), readonly=True, | ||
315 | 878 | required=False, vocabulary=RevisionStatusResult)) | ||
316 | 879 | |||
317 | 880 | @mutator_for(result) | ||
318 | 881 | @operation_parameters(result=copy_field(result)) | ||
319 | 882 | @export_write_operation() | ||
320 | 883 | @operation_for_version("devel") | ||
321 | 884 | def transitionToNewResult(result): | ||
322 | 885 | """Set the RevisionStatusReport result. | ||
323 | 886 | |||
324 | 887 | Set the revision status report result.""" | ||
325 | 888 | |||
326 | 889 | |||
327 | 890 | class IRevisionStatusReportEdit(Interface): | ||
328 | 891 | """`IRevisionStatusReport` attributes that require launchpad.Edit.""" | ||
329 | 892 | |||
330 | 893 | @operation_parameters( | ||
331 | 894 | log_data=Bytes(title=_("The content of the artifact in bytes."), | ||
332 | 895 | constraint=attachment_size_constraint)) | ||
333 | 896 | @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title) | ||
334 | 897 | @export_write_operation() | ||
335 | 898 | @export_operation_as(name="setLog") | ||
336 | 899 | @operation_for_version("devel") | ||
337 | 900 | def api_setLog(log_data): | ||
338 | 901 | """Set a new log on an existing status report. | ||
339 | 902 | |||
340 | 903 | :param log_data: The contents (in bytes) of the log. | ||
341 | 904 | """ | ||
342 | 905 | |||
343 | 906 | |||
344 | 907 | @exported_as_webservice_entry(as_of="beta") | ||
345 | 908 | class IRevisionStatusReport(IRevisionStatusReportView, | ||
346 | 909 | IRevisionStatusReportEditableAttributes, | ||
347 | 910 | IRevisionStatusReportEdit): | ||
348 | 911 | """An revision status report for a Git commit.""" | ||
349 | 912 | |||
350 | 913 | |||
351 | 914 | class IRevisionStatusReportSet(Interface): | ||
352 | 915 | """The set of all revision status reports.""" | ||
353 | 916 | |||
354 | 917 | def new(creator, title, git_repository, commit_sha1, date_created=None, | ||
355 | 918 | url=None, result_summary=None, result=None, date_started=None, | ||
356 | 919 | date_finished=None, log=None): | ||
357 | 920 | """Return a new revision status report. | ||
358 | 921 | |||
359 | 922 | :param title: A text string. | ||
360 | 923 | :param git_repository: An `IGitRepository` for which the report | ||
361 | 924 | is being created. | ||
362 | 925 | :param commit_sha1: The sha1 of the commit for which the report | ||
363 | 926 | is being created. | ||
364 | 927 | :param date_created: The date when the report is being created. | ||
365 | 928 | :param url: External URL to view result of report. | ||
366 | 929 | :param result_summary: A short summary of the result. | ||
367 | 930 | :param result: The result of the check job for this revision. | ||
368 | 931 | :param date_started: DateTime that report was started. | ||
369 | 932 | :param date_finished: DateTime that report was completed. | ||
370 | 933 | :param log: Stores the content of the artifact for this report. | ||
371 | 934 | """ | ||
372 | 935 | |||
373 | 936 | def getByID(id): | ||
374 | 937 | """Returns the RevisionStatusReport for a given ID.""" | ||
375 | 938 | |||
376 | 939 | def findByRepository(repository): | ||
377 | 940 | """Returns the set of RevisionStatusReport for a repository.""" | ||
378 | 941 | |||
379 | 942 | |||
380 | 943 | class IRevisionStatusArtifactSet(Interface): | ||
381 | 944 | """The set of all revision status artifacts.""" | ||
382 | 945 | |||
383 | 946 | def new(lfa, report): | ||
384 | 947 | """Return a new revision status artifact. | ||
385 | 948 | |||
386 | 949 | :param lfa: An `ILibraryFileAlias`. | ||
387 | 950 | :param report: An `IRevisionStatusReport` for which the | ||
388 | 951 | artifact is being created. | ||
389 | 952 | """ | ||
390 | 953 | |||
391 | 954 | def getByID(id): | ||
392 | 955 | """Returns the RevisionStatusArtifact for a given ID.""" | ||
393 | 956 | |||
394 | 957 | def findByReport(report): | ||
395 | 958 | """Returns the set of artifacts for a given report.""" | ||
396 | 959 | |||
397 | 960 | |||
398 | 961 | class IRevisionStatusArtifact(Interface): | ||
399 | 962 | id = Int(title=_("ID"), required=True, readonly=True) | ||
400 | 963 | |||
401 | 964 | report = Attribute( | ||
402 | 965 | "The `RevisionStatusReport` that this artifact is linked to.") | ||
403 | 966 | |||
404 | 967 | library_file = Attribute( | ||
405 | 968 | "The `LibraryFileAlias` object containing information for " | ||
406 | 969 | "a revision status report.") | ||
407 | 970 | |||
408 | 971 | artifact_type = Choice( | ||
409 | 972 | title=_('The type of artifact, only log for now.'), | ||
410 | 973 | vocabulary=RevisionStatusArtifactType) | ||
411 | 974 | |||
412 | 975 | |||
413 | 816 | class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget): | 976 | class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget): |
414 | 817 | """IGitRepository methods that require launchpad.Edit permission.""" | 977 | """IGitRepository methods that require launchpad.Edit permission.""" |
415 | 818 | 978 | ||
416 | @@ -1015,6 +1175,26 @@ class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget): | |||
417 | 1015 | :raise: CannotDeleteGitRepository if the repository cannot be deleted. | 1175 | :raise: CannotDeleteGitRepository if the repository cannot be deleted. |
418 | 1016 | """ | 1176 | """ |
419 | 1017 | 1177 | ||
420 | 1178 | @operation_parameters( | ||
421 | 1179 | title=copy_field(IRevisionStatusReport["title"]), | ||
422 | 1180 | commit_sha1=copy_field(IRevisionStatusReport["commit_sha1"]), | ||
423 | 1181 | url=copy_field(IRevisionStatusReport["url"]), | ||
424 | 1182 | result_summary=copy_field(IRevisionStatusReport["result_summary"]), | ||
425 | 1183 | result=copy_field(IRevisionStatusReport["result"])) | ||
426 | 1184 | @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title) | ||
427 | 1185 | @call_with(user=REQUEST_USER) | ||
428 | 1186 | @export_factory_operation(IRevisionStatusReport, []) | ||
429 | 1187 | @operation_for_version("devel") | ||
430 | 1188 | def newStatusReport(title, commit_sha1, url, result_summary, result, user): | ||
431 | 1189 | """Create a new status report. | ||
432 | 1190 | |||
433 | 1191 | :param title: The name of the new report. | ||
434 | 1192 | :param commit_sha1: The commit sha1 for the report. | ||
435 | 1193 | :param url: The external link of the status report. | ||
436 | 1194 | :param result_summary: The description of the new report. | ||
437 | 1195 | :param result: The result of the new report. | ||
438 | 1196 | """ | ||
439 | 1197 | |||
440 | 1018 | 1198 | ||
441 | 1019 | # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL | 1199 | # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL |
442 | 1020 | # generation working. Individual attributes must set their version to | 1200 | # generation working. Individual attributes must set their version to |
443 | diff --git a/lib/lp/code/interfaces/webservice.py b/lib/lp/code/interfaces/webservice.py | |||
444 | index 02546e9..bd94298 100644 | |||
445 | --- a/lib/lp/code/interfaces/webservice.py | |||
446 | +++ b/lib/lp/code/interfaces/webservice.py | |||
447 | @@ -31,6 +31,7 @@ __all__ = [ | |||
448 | 31 | 'IGitSubscription', | 31 | 'IGitSubscription', |
449 | 32 | 'IHasGitRepositories', | 32 | 'IHasGitRepositories', |
450 | 33 | 'IPreviewDiff', | 33 | 'IPreviewDiff', |
451 | 34 | 'IRevisionStatusReport', | ||
452 | 34 | 'ISourcePackageRecipe', | 35 | 'ISourcePackageRecipe', |
453 | 35 | 'ISourcePackageRecipeBuild', | 36 | 'ISourcePackageRecipeBuild', |
454 | 36 | ] | 37 | ] |
455 | @@ -66,6 +67,7 @@ from lp.code.interfaces.gitref import IGitRef | |||
456 | 66 | from lp.code.interfaces.gitrepository import ( | 67 | from lp.code.interfaces.gitrepository import ( |
457 | 67 | IGitRepository, | 68 | IGitRepository, |
458 | 68 | IGitRepositorySet, | 69 | IGitRepositorySet, |
459 | 70 | IRevisionStatusReport, | ||
460 | 69 | ) | 71 | ) |
461 | 70 | from lp.code.interfaces.gitsubscription import IGitSubscription | 72 | from lp.code.interfaces.gitsubscription import IGitSubscription |
462 | 71 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories | 73 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
463 | diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py | |||
464 | index 812f1d7..501ef29 100644 | |||
465 | --- a/lib/lp/code/model/gitrepository.py | |||
466 | +++ b/lib/lp/code/model/gitrepository.py | |||
467 | @@ -6,6 +6,7 @@ __all__ = [ | |||
468 | 6 | 'GitRepository', | 6 | 'GitRepository', |
469 | 7 | 'GitRepositorySet', | 7 | 'GitRepositorySet', |
470 | 8 | 'parse_git_commits', | 8 | 'parse_git_commits', |
471 | 9 | 'RevisionStatusReport', | ||
472 | 9 | ] | 10 | ] |
473 | 10 | 11 | ||
474 | 11 | from collections import ( | 12 | from collections import ( |
475 | @@ -19,6 +20,7 @@ from datetime import ( | |||
476 | 19 | import email | 20 | import email |
477 | 20 | from fnmatch import fnmatch | 21 | from fnmatch import fnmatch |
478 | 21 | from functools import partial | 22 | from functools import partial |
479 | 23 | import io | ||
480 | 22 | from itertools import ( | 24 | from itertools import ( |
481 | 23 | chain, | 25 | chain, |
482 | 24 | groupby, | 26 | groupby, |
483 | @@ -102,6 +104,8 @@ from lp.code.enums import ( | |||
484 | 102 | GitPermissionType, | 104 | GitPermissionType, |
485 | 103 | GitRepositoryStatus, | 105 | GitRepositoryStatus, |
486 | 104 | GitRepositoryType, | 106 | GitRepositoryType, |
487 | 107 | RevisionStatusArtifactType, | ||
488 | 108 | RevisionStatusResult, | ||
489 | 105 | ) | 109 | ) |
490 | 106 | from lp.code.errors import ( | 110 | from lp.code.errors import ( |
491 | 107 | CannotDeleteGitRepository, | 111 | CannotDeleteGitRepository, |
492 | @@ -131,6 +135,10 @@ from lp.code.interfaces.gitrepository import ( | |||
493 | 131 | GitIdentityMixin, | 135 | GitIdentityMixin, |
494 | 132 | IGitRepository, | 136 | IGitRepository, |
495 | 133 | IGitRepositorySet, | 137 | IGitRepositorySet, |
496 | 138 | IRevisionStatusArtifactSet, | ||
497 | 139 | IRevisionStatusReport, | ||
498 | 140 | IRevisionStatusReportSet, | ||
499 | 141 | RevisionStatusReportsFeatureDisabled, | ||
500 | 134 | user_has_special_git_repository_access, | 142 | user_has_special_git_repository_access, |
501 | 135 | ) | 143 | ) |
502 | 136 | from lp.code.interfaces.gitrule import ( | 144 | from lp.code.interfaces.gitrule import ( |
503 | @@ -205,6 +213,7 @@ from lp.services.identity.interfaces.account import ( | |||
504 | 205 | ) | 213 | ) |
505 | 206 | from lp.services.job.interfaces.job import JobStatus | 214 | from lp.services.job.interfaces.job import JobStatus |
506 | 207 | from lp.services.job.model.job import Job | 215 | from lp.services.job.model.job import Job |
507 | 216 | from lp.services.librarian.interfaces import ILibraryFileAliasSet | ||
508 | 208 | from lp.services.macaroons.interfaces import IMacaroonIssuer | 217 | from lp.services.macaroons.interfaces import IMacaroonIssuer |
509 | 209 | from lp.services.macaroons.model import MacaroonIssuerBase | 218 | from lp.services.macaroons.model import MacaroonIssuerBase |
510 | 210 | from lp.services.mail.notificationrecipientset import NotificationRecipientSet | 219 | from lp.services.mail.notificationrecipientset import NotificationRecipientSet |
511 | @@ -219,8 +228,9 @@ from lp.services.webhooks.model import WebhookTargetMixin | |||
512 | 219 | from lp.snappy.interfaces.snap import ISnapSet | 228 | from lp.snappy.interfaces.snap import ISnapSet |
513 | 220 | 229 | ||
514 | 221 | 230 | ||
516 | 222 | logger = logging.getLogger(__name__) | 231 | REVISION_STATUS_REPORT_ALLOW_CREATE = 'revision_status_report.allow_create' |
517 | 223 | 232 | ||
518 | 233 | logger = logging.getLogger(__name__) | ||
519 | 224 | 234 | ||
520 | 225 | object_type_map = { | 235 | object_type_map = { |
521 | 226 | "commit": GitObjectType.COMMIT, | 236 | "commit": GitObjectType.COMMIT, |
522 | @@ -292,6 +302,132 @@ def git_repository_modified(repository, event): | |||
523 | 292 | send_git_repository_modified_notifications(repository, event) | 302 | send_git_repository_modified_notifications(repository, event) |
524 | 293 | 303 | ||
525 | 294 | 304 | ||
526 | 305 | @implementer(IRevisionStatusReport) | ||
527 | 306 | class RevisionStatusReport(StormBase): | ||
528 | 307 | __storm_table__ = 'RevisionStatusReport' | ||
529 | 308 | |||
530 | 309 | id = Int(primary=True) | ||
531 | 310 | |||
532 | 311 | creator_id = Int(name="creator", allow_none=False) | ||
533 | 312 | creator = Reference(creator_id, "Person.id") | ||
534 | 313 | |||
535 | 314 | title = Unicode(name='name', allow_none=False) | ||
536 | 315 | |||
537 | 316 | git_repository_id = Int(name='git_repository', allow_none=False) | ||
538 | 317 | git_repository = Reference(git_repository_id, 'GitRepository.id') | ||
539 | 318 | |||
540 | 319 | commit_sha1 = Unicode(name='commit_sha1', allow_none=False) | ||
541 | 320 | |||
542 | 321 | url = Unicode(name='url', allow_none=True) | ||
543 | 322 | |||
544 | 323 | result_summary = Unicode(name='description', allow_none=True) | ||
545 | 324 | |||
546 | 325 | result = DBEnum(name='result', allow_none=True, enum=RevisionStatusResult) | ||
547 | 326 | |||
548 | 327 | date_created = DateTime( | ||
549 | 328 | name='date_created', tzinfo=pytz.UTC, allow_none=False) | ||
550 | 329 | |||
551 | 330 | date_started = DateTime(name='date_started', tzinfo=pytz.UTC, | ||
552 | 331 | allow_none=True) | ||
553 | 332 | date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC, | ||
554 | 333 | allow_none=True) | ||
555 | 334 | |||
556 | 335 | def __init__(self, git_repository, user, title, commit_sha1, | ||
557 | 336 | url, result_summary, result): | ||
558 | 337 | super().__init__() | ||
559 | 338 | self.creator = user | ||
560 | 339 | self.git_repository = git_repository | ||
561 | 340 | self.title = title | ||
562 | 341 | self.commit_sha1 = commit_sha1 | ||
563 | 342 | self.url = url | ||
564 | 343 | self.result_summary = result_summary | ||
565 | 344 | self.result = result | ||
566 | 345 | self.date_created = UTC_NOW | ||
567 | 346 | |||
568 | 347 | def api_setLog(self, log_data): | ||
569 | 348 | filename = '%s-%s.txt' % (self.title, self.commit_sha1) | ||
570 | 349 | |||
571 | 350 | lfa = getUtility(ILibraryFileAliasSet).create( | ||
572 | 351 | name=filename, size=len(log_data), | ||
573 | 352 | file=io.BytesIO(log_data), contentType='text/plain') | ||
574 | 353 | |||
575 | 354 | getUtility(IRevisionStatusArtifactSet).new(lfa, self) | ||
576 | 355 | |||
577 | 356 | def transitionToNewResult(self, result): | ||
578 | 357 | if self.result == RevisionStatusResult.WAITING: | ||
579 | 358 | if result == RevisionStatusResult.RUNNING: | ||
580 | 359 | self.date_started == UTC_NOW | ||
581 | 360 | else: | ||
582 | 361 | self.date_finished = UTC_NOW | ||
583 | 362 | self.result = result | ||
584 | 363 | |||
585 | 364 | |||
586 | 365 | @implementer(IRevisionStatusReportSet) | ||
587 | 366 | class RevisionStatusReportSet: | ||
588 | 367 | |||
589 | 368 | def new(self, creator, title, git_repository, commit_sha1, | ||
590 | 369 | url=None, result_summary=None, result=None, | ||
591 | 370 | date_started=None, date_finished=None, log=None): | ||
592 | 371 | """See `IRevisionStatusReportSet`.""" | ||
593 | 372 | store = IStore(RevisionStatusReport) | ||
594 | 373 | report = RevisionStatusReport(git_repository, creator, title, | ||
595 | 374 | commit_sha1, url, result_summary, | ||
596 | 375 | result) | ||
597 | 376 | store.add(report) | ||
598 | 377 | return report | ||
599 | 378 | |||
600 | 379 | def getByID(self, id): | ||
601 | 380 | return IStore( | ||
602 | 381 | RevisionStatusReport).find(RevisionStatusReport, id=id).one() | ||
603 | 382 | |||
604 | 383 | def findByRepository(self, repository): | ||
605 | 384 | return IStore(RevisionStatusReport).find( | ||
606 | 385 | RevisionStatusReport, | ||
607 | 386 | RevisionStatusReport.git_repository == repository) | ||
608 | 387 | |||
609 | 388 | |||
610 | 389 | class RevisionStatusArtifact(StormBase): | ||
611 | 390 | __storm_table__ = 'RevisionStatusArtifact' | ||
612 | 391 | |||
613 | 392 | id = Int(primary=True) | ||
614 | 393 | |||
615 | 394 | library_file_id = Int(name='library_file', allow_none=False) | ||
616 | 395 | library_file = Reference(library_file_id, 'LibraryFileAlias.id') | ||
617 | 396 | |||
618 | 397 | report_id = Int(name='report', allow_none=False) | ||
619 | 398 | report = Reference(report_id, 'RevisionStatusReport.id') | ||
620 | 399 | |||
621 | 400 | artifact_type = DBEnum(name='type', allow_none=False, | ||
622 | 401 | enum=RevisionStatusArtifactType) | ||
623 | 402 | |||
624 | 403 | def __init__(self, library_file, report): | ||
625 | 404 | super().__init__() | ||
626 | 405 | self.library_file = library_file | ||
627 | 406 | self.report = report | ||
628 | 407 | self.artifact_type = RevisionStatusArtifactType.LOG | ||
629 | 408 | |||
630 | 409 | |||
631 | 410 | @implementer(IRevisionStatusArtifactSet) | ||
632 | 411 | class RevisionStatusArtifactSet: | ||
633 | 412 | |||
634 | 413 | def new(self, lfa, report): | ||
635 | 414 | """See `IRevisionStatusArtifactSet`.""" | ||
636 | 415 | store = IStore(RevisionStatusArtifact) | ||
637 | 416 | artifact = RevisionStatusArtifact(lfa, report) | ||
638 | 417 | store.add(artifact) | ||
639 | 418 | return artifact | ||
640 | 419 | |||
641 | 420 | def getById(self, id): | ||
642 | 421 | return IStore(RevisionStatusArtifact).find( | ||
643 | 422 | RevisionStatusArtifact, | ||
644 | 423 | RevisionStatusArtifact.id == id).one() | ||
645 | 424 | |||
646 | 425 | def findByReport(self, report): | ||
647 | 426 | return IStore(RevisionStatusArtifact).find( | ||
648 | 427 | RevisionStatusArtifact, | ||
649 | 428 | RevisionStatusArtifact.report == report) | ||
650 | 429 | |||
651 | 430 | |||
652 | 295 | @implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType) | 431 | @implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType) |
653 | 296 | class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin, | 432 | class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin, |
654 | 297 | GitIdentityMixin): | 433 | GitIdentityMixin): |
655 | @@ -501,6 +637,16 @@ class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin, | |||
656 | 501 | def collectGarbage(self): | 637 | def collectGarbage(self): |
657 | 502 | getUtility(IGitHostingClient).collectGarbage(self.getInternalPath()) | 638 | getUtility(IGitHostingClient).collectGarbage(self.getInternalPath()) |
658 | 503 | 639 | ||
659 | 640 | def newStatusReport(self, user, title, commit_sha1, url=None, | ||
660 | 641 | result_summary=None, result=None): | ||
661 | 642 | |||
662 | 643 | if not getFeatureFlag(REVISION_STATUS_REPORT_ALLOW_CREATE): | ||
663 | 644 | raise RevisionStatusReportsFeatureDisabled() | ||
664 | 645 | |||
665 | 646 | report = RevisionStatusReport(self, user, title, commit_sha1, | ||
666 | 647 | url, result_summary, result) | ||
667 | 648 | return report | ||
668 | 649 | |||
669 | 504 | @property | 650 | @property |
670 | 505 | def namespace(self): | 651 | def namespace(self): |
671 | 506 | """See `IGitRepository`.""" | 652 | """See `IGitRepository`.""" |
672 | diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py | |||
673 | index 252b535..764d13c 100644 | |||
674 | --- a/lib/lp/code/model/tests/test_gitrepository.py | |||
675 | +++ b/lib/lp/code/model/tests/test_gitrepository.py | |||
676 | @@ -10,6 +10,7 @@ from datetime import ( | |||
677 | 10 | import email | 10 | import email |
678 | 11 | from functools import partial | 11 | from functools import partial |
679 | 12 | import hashlib | 12 | import hashlib |
680 | 13 | import io | ||
681 | 13 | import json | 14 | import json |
682 | 14 | 15 | ||
683 | 15 | from breezy import urlutils | 16 | from breezy import urlutils |
684 | @@ -65,6 +66,7 @@ from lp.code.enums import ( | |||
685 | 65 | GitObjectType, | 66 | GitObjectType, |
686 | 66 | GitRepositoryStatus, | 67 | GitRepositoryStatus, |
687 | 67 | GitRepositoryType, | 68 | GitRepositoryType, |
688 | 69 | RevisionStatusResult, | ||
689 | 68 | TargetRevisionControlSystems, | 70 | TargetRevisionControlSystems, |
690 | 69 | ) | 71 | ) |
691 | 70 | from lp.code.errors import ( | 72 | from lp.code.errors import ( |
692 | @@ -95,6 +97,8 @@ from lp.code.interfaces.gitrepository import ( | |||
693 | 95 | IGitRepository, | 97 | IGitRepository, |
694 | 96 | IGitRepositorySet, | 98 | IGitRepositorySet, |
695 | 97 | IGitRepositoryView, | 99 | IGitRepositoryView, |
696 | 100 | IRevisionStatusArtifactSet, | ||
697 | 101 | IRevisionStatusReportSet, | ||
698 | 98 | ) | 102 | ) |
699 | 99 | from lp.code.interfaces.gitrule import ( | 103 | from lp.code.interfaces.gitrule import ( |
700 | 100 | IGitNascentRule, | 104 | IGitNascentRule, |
701 | @@ -121,6 +125,7 @@ from lp.code.model.gitrepository import ( | |||
702 | 121 | DeletionCallable, | 125 | DeletionCallable, |
703 | 122 | DeletionOperation, | 126 | DeletionOperation, |
704 | 123 | GitRepository, | 127 | GitRepository, |
705 | 128 | REVISION_STATUS_REPORT_ALLOW_CREATE, | ||
706 | 124 | ) | 129 | ) |
707 | 125 | from lp.code.tests.helpers import GitHostingFixture | 130 | from lp.code.tests.helpers import GitHostingFixture |
708 | 126 | from lp.code.xmlrpc.git import GitAPI | 131 | from lp.code.xmlrpc.git import GitAPI |
709 | @@ -571,6 +576,24 @@ class TestGitRepository(TestCaseWithFactory): | |||
710 | 571 | self.assertThat(recorder2, HasQueryCount.byEquality(recorder1)) | 576 | self.assertThat(recorder2, HasQueryCount.byEquality(recorder1)) |
711 | 572 | self.assertEqual(7, recorder1.count) | 577 | self.assertEqual(7, recorder1.count) |
712 | 573 | 578 | ||
713 | 579 | def test_findRevisionStatusReport(self): | ||
714 | 580 | repository = removeSecurityProxy(self.factory.makeGitRepository()) | ||
715 | 581 | title = self.factory.getUniqueUnicode('report-title') | ||
716 | 582 | commit_sha1 = hashlib.sha1(b"Some content").hexdigest() | ||
717 | 583 | result_summary = "120/120 tests passed" | ||
718 | 584 | |||
719 | 585 | report = self.factory.makeRevisionStatusReport( | ||
720 | 586 | user=repository.owner, git_repository=repository, | ||
721 | 587 | title=title, commit_sha1=commit_sha1, | ||
722 | 588 | result_summary=result_summary, | ||
723 | 589 | result=RevisionStatusResult.SUCCEEDED) | ||
724 | 590 | |||
725 | 591 | with person_logged_in(repository.owner): | ||
726 | 592 | result = getUtility( | ||
727 | 593 | IRevisionStatusReportSet).getByID( | ||
728 | 594 | report.id) | ||
729 | 595 | self.assertEqual(report, result) | ||
730 | 596 | |||
731 | 574 | 597 | ||
732 | 575 | class TestGitIdentityMixin(TestCaseWithFactory): | 598 | class TestGitIdentityMixin(TestCaseWithFactory): |
733 | 576 | """Test the defaults and identities provided by GitIdentityMixin.""" | 599 | """Test the defaults and identities provided by GitIdentityMixin.""" |
734 | @@ -4241,6 +4264,66 @@ class TestGitRepositoryWebservice(TestCaseWithFactory): | |||
735 | 4241 | self.assertEqual( | 4264 | self.assertEqual( |
736 | 4242 | InformationType.PUBLIC, repository_db.information_type) | 4265 | InformationType.PUBLIC, repository_db.information_type) |
737 | 4243 | 4266 | ||
738 | 4267 | def test_newRevisionStatusReport_featureFlagDisabled(self): | ||
739 | 4268 | repository = self.factory.makeGitRepository() | ||
740 | 4269 | requester = repository.owner | ||
741 | 4270 | webservice = webservice_for_person(None, default_api_version="devel") | ||
742 | 4271 | with person_logged_in(requester): | ||
743 | 4272 | repository_url = api_url(repository) | ||
744 | 4273 | |||
745 | 4274 | secret, _ = self.factory.makeAccessToken( | ||
746 | 4275 | owner=requester, target=repository, | ||
747 | 4276 | scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) | ||
748 | 4277 | header = {'Authorization': 'Token %s' % secret} | ||
749 | 4278 | |||
750 | 4279 | response = webservice.named_post( | ||
751 | 4280 | repository_url, "newStatusReport", | ||
752 | 4281 | headers=header, title="CI", | ||
753 | 4282 | commit_sha1=hashlib.sha1( | ||
754 | 4283 | self.factory.getUniqueBytes()).hexdigest(), | ||
755 | 4284 | url='https://launchpad.net/', | ||
756 | 4285 | result_summary="120/120 tests passed", | ||
757 | 4286 | result="Succeeded") | ||
758 | 4287 | |||
759 | 4288 | self.assertEqual(401, response.status) | ||
760 | 4289 | self.assertIn( | ||
761 | 4290 | b'You do not have permission to create revision status reports', | ||
762 | 4291 | response.body) | ||
763 | 4292 | |||
764 | 4293 | def test_newRevisionStatusReport(self): | ||
765 | 4294 | repository = self.factory.makeGitRepository() | ||
766 | 4295 | requester = repository.owner | ||
767 | 4296 | webservice = webservice_for_person(None, default_api_version="devel") | ||
768 | 4297 | with person_logged_in(requester): | ||
769 | 4298 | repository_url = api_url(repository) | ||
770 | 4299 | |||
771 | 4300 | secret, _ = self.factory.makeAccessToken( | ||
772 | 4301 | owner=requester, target=repository, | ||
773 | 4302 | scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) | ||
774 | 4303 | header = {'Authorization': 'Token %s' % secret} | ||
775 | 4304 | |||
776 | 4305 | self.useFixture(FeatureFixture( | ||
777 | 4306 | {REVISION_STATUS_REPORT_ALLOW_CREATE: "on"})) | ||
778 | 4307 | |||
779 | 4308 | response = webservice.named_post( | ||
780 | 4309 | repository_url, "newStatusReport", | ||
781 | 4310 | headers=header, title="CI", | ||
782 | 4311 | commit_sha1=hashlib.sha1( | ||
783 | 4312 | self.factory.getUniqueBytes()).hexdigest(), | ||
784 | 4313 | url='https://launchpad.net/', | ||
785 | 4314 | result_summary="120/120 tests passed", | ||
786 | 4315 | result="Succeeded") | ||
787 | 4316 | self.assertEqual(201, response.status) | ||
788 | 4317 | |||
789 | 4318 | with person_logged_in(requester): | ||
790 | 4319 | results = getUtility( | ||
791 | 4320 | IRevisionStatusReportSet).findByRepository(repository) | ||
792 | 4321 | reports = list(results) | ||
793 | 4322 | urls = [webservice.getAbsoluteUrl('%s/+status/%s' % ( | ||
794 | 4323 | api_url(repository), | ||
795 | 4324 | report.id)) for report in reports] | ||
796 | 4325 | self.assertIn(response.getHeader("Location"), urls) | ||
797 | 4326 | |||
798 | 4244 | def test_set_target(self): | 4327 | def test_set_target(self): |
799 | 4245 | # The repository owner can move the repository to another target; | 4328 | # The repository owner can move the repository to another target; |
800 | 4246 | # this redirects to the new location. | 4329 | # this redirects to the new location. |
801 | @@ -4831,6 +4914,60 @@ class TestGitRepositoryWebservice(TestCaseWithFactory): | |||
802 | 4831 | response.body) | 4914 | response.body) |
803 | 4832 | 4915 | ||
804 | 4833 | 4916 | ||
805 | 4917 | class TestRevisionStatusReportWebservice(TestCaseWithFactory): | ||
806 | 4918 | layer = LaunchpadFunctionalLayer | ||
807 | 4919 | |||
808 | 4920 | def setUp(self): | ||
809 | 4921 | super(TestRevisionStatusReportWebservice, self).setUp() | ||
810 | 4922 | self.repository = self.factory.makeGitRepository() | ||
811 | 4923 | self.requester = self.repository.owner | ||
812 | 4924 | title = self.factory.getUniqueUnicode('report-title') | ||
813 | 4925 | commit_sha1 = hashlib.sha1(b"Some content").hexdigest() | ||
814 | 4926 | result_summary = "120/120 tests passed" | ||
815 | 4927 | |||
816 | 4928 | self.report = self.factory.makeRevisionStatusReport( | ||
817 | 4929 | user=self.repository.owner, git_repository=self.repository, | ||
818 | 4930 | title=title, commit_sha1=commit_sha1, | ||
819 | 4931 | result_summary=result_summary, | ||
820 | 4932 | result=RevisionStatusResult.SUCCEEDED) | ||
821 | 4933 | |||
822 | 4934 | self.webservice = webservice_for_person( | ||
823 | 4935 | None, default_api_version="devel") | ||
824 | 4936 | with person_logged_in(self.requester): | ||
825 | 4937 | self.report_url = api_url(self.report) | ||
826 | 4938 | |||
827 | 4939 | secret, _ = self.factory.makeAccessToken( | ||
828 | 4940 | owner=self.requester, target=self.repository, | ||
829 | 4941 | scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS]) | ||
830 | 4942 | self.header = {'Authorization': 'Token %s' % secret} | ||
831 | 4943 | |||
832 | 4944 | def test_setLogOnRevisionStatusReport(self): | ||
833 | 4945 | content = b'log_content_data' | ||
834 | 4946 | filesize = len(content) | ||
835 | 4947 | sha1 = hashlib.sha1(content).hexdigest() | ||
836 | 4948 | md5 = hashlib.md5(content).hexdigest() | ||
837 | 4949 | response = self.webservice.named_post( | ||
838 | 4950 | self.report_url, "setLog", | ||
839 | 4951 | headers=self.header, | ||
840 | 4952 | log_data=io.BytesIO(content)) | ||
841 | 4953 | self.assertEqual(200, response.status) | ||
842 | 4954 | |||
843 | 4955 | # A report may have multiple artifacts. | ||
844 | 4956 | # We verify that the content we just submitted via API now | ||
845 | 4957 | # matches one of the artifacts in the DB for the report. | ||
846 | 4958 | with person_logged_in(self.requester): | ||
847 | 4959 | artifacts = list(getUtility( | ||
848 | 4960 | IRevisionStatusArtifactSet).findByReport(self.report)) | ||
849 | 4961 | lfcs = [artifact.library_file.content for artifact in artifacts] | ||
850 | 4962 | sha1_of_all_artifacts = [lfc.sha1 for lfc in lfcs] | ||
851 | 4963 | md5_of_all_artifacts = [lfc.md5 for lfc in lfcs] | ||
852 | 4964 | filesizes_of_all_artifacts = [lfc.filesize for lfc in lfcs] | ||
853 | 4965 | |||
854 | 4966 | self.assertIn(sha1, sha1_of_all_artifacts) | ||
855 | 4967 | self.assertIn(md5, md5_of_all_artifacts) | ||
856 | 4968 | self.assertIn(filesize, filesizes_of_all_artifacts) | ||
857 | 4969 | |||
858 | 4970 | |||
859 | 4834 | class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory): | 4971 | class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory): |
860 | 4835 | """Test GitRepository macaroon issuing and verification.""" | 4972 | """Test GitRepository macaroon issuing and verification.""" |
861 | 4836 | 4973 | ||
862 | diff --git a/lib/lp/security.py b/lib/lp/security.py | |||
863 | index 8f66fbb..8435167 100644 | |||
864 | --- a/lib/lp/security.py | |||
865 | +++ b/lib/lp/security.py | |||
866 | @@ -99,6 +99,8 @@ from lp.code.interfaces.gitcollection import IGitCollection | |||
867 | 99 | from lp.code.interfaces.gitref import IGitRef | 99 | from lp.code.interfaces.gitref import IGitRef |
868 | 100 | from lp.code.interfaces.gitrepository import ( | 100 | from lp.code.interfaces.gitrepository import ( |
869 | 101 | IGitRepository, | 101 | IGitRepository, |
870 | 102 | IRevisionStatusArtifact, | ||
871 | 103 | IRevisionStatusReport, | ||
872 | 102 | user_has_special_git_repository_access, | 104 | user_has_special_git_repository_access, |
873 | 103 | ) | 105 | ) |
874 | 104 | from lp.code.interfaces.gitrule import ( | 106 | from lp.code.interfaces.gitrule import ( |
875 | @@ -683,6 +685,23 @@ class EditSpecificationByRelatedPeople(AuthorizationBase): | |||
876 | 683 | self.obj, ['owner', 'drafter', 'assignee', 'approver'])) | 685 | self.obj, ['owner', 'drafter', 'assignee', 'approver'])) |
877 | 684 | 686 | ||
878 | 685 | 687 | ||
879 | 688 | class EditRevisionStatusReport(AuthorizationBase): | ||
880 | 689 | """The owner of a Git repository can edit its status reports.""" | ||
881 | 690 | permission = 'launchpad.Edit' | ||
882 | 691 | usedfor = IRevisionStatusReport | ||
883 | 692 | |||
884 | 693 | def checkAuthenticated(self, user): | ||
885 | 694 | return user.isOwner(self.obj.git_repository) | ||
886 | 695 | |||
887 | 696 | |||
888 | 697 | class EditRevisionStatusArtifact(DelegatedAuthorization): | ||
889 | 698 | permission = 'launchpad.Edit' | ||
890 | 699 | usedfor = IRevisionStatusArtifact | ||
891 | 700 | |||
892 | 701 | def __init__(self, obj): | ||
893 | 702 | super().__init__(obj, obj.report, 'launchpad.Edit') | ||
894 | 703 | |||
895 | 704 | |||
896 | 686 | class AdminSpecification(AuthorizationBase): | 705 | class AdminSpecification(AuthorizationBase): |
897 | 687 | permission = 'launchpad.Admin' | 706 | permission = 'launchpad.Admin' |
898 | 688 | usedfor = ISpecification | 707 | usedfor = ISpecification |
899 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py | |||
900 | index 9d59ba8..db48ed3 100644 | |||
901 | --- a/lib/lp/testing/factory.py | |||
902 | +++ b/lib/lp/testing/factory.py | |||
903 | @@ -131,7 +131,10 @@ from lp.code.interfaces.gitref import ( | |||
904 | 131 | IGitRef, | 131 | IGitRef, |
905 | 132 | IGitRefRemoteSet, | 132 | IGitRefRemoteSet, |
906 | 133 | ) | 133 | ) |
908 | 134 | from lp.code.interfaces.gitrepository import IGitRepository | 134 | from lp.code.interfaces.gitrepository import ( |
909 | 135 | IGitRepository, | ||
910 | 136 | IRevisionStatusArtifactSet, | ||
911 | 137 | ) | ||
912 | 135 | from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch | 138 | from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch |
913 | 136 | from lp.code.interfaces.revision import IRevisionSet | 139 | from lp.code.interfaces.revision import IRevisionSet |
914 | 137 | from lp.code.interfaces.sourcepackagerecipe import ( | 140 | from lp.code.interfaces.sourcepackagerecipe import ( |
915 | @@ -146,6 +149,7 @@ from lp.code.model.diff import ( | |||
916 | 146 | Diff, | 149 | Diff, |
917 | 147 | PreviewDiff, | 150 | PreviewDiff, |
918 | 148 | ) | 151 | ) |
919 | 152 | from lp.code.model.gitrepository import IRevisionStatusReportSet | ||
920 | 149 | from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet | 153 | from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet |
921 | 150 | from lp.oci.interfaces.ocirecipe import IOCIRecipeSet | 154 | from lp.oci.interfaces.ocirecipe import IOCIRecipeSet |
922 | 151 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet | 155 | from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet |
923 | @@ -1843,6 +1847,30 @@ class BareLaunchpadObjectFactory(ObjectFactory): | |||
924 | 1843 | grantee, grantor, can_create=can_create, can_push=can_push, | 1847 | grantee, grantor, can_create=can_create, can_push=can_push, |
925 | 1844 | can_force_push=can_force_push) | 1848 | can_force_push=can_force_push) |
926 | 1845 | 1849 | ||
927 | 1850 | def makeRevisionStatusReport(self, user=None, title=None, | ||
928 | 1851 | git_repository=None, commit_sha1=None, | ||
929 | 1852 | result_summary=None, url=None, result=None): | ||
930 | 1853 | """Create a new RevisionStatusReport.""" | ||
931 | 1854 | if title is None: | ||
932 | 1855 | title = self.getUniqueUnicode() | ||
933 | 1856 | if git_repository is None: | ||
934 | 1857 | git_repository = self.makeGitRepository() | ||
935 | 1858 | if user is None: | ||
936 | 1859 | user = git_repository.owner | ||
937 | 1860 | if commit_sha1 is None: | ||
938 | 1861 | commit_sha1 = hashlib.sha1(self.getUniqueBytes()).hexdigest() | ||
939 | 1862 | return getUtility(IRevisionStatusReportSet).new( | ||
940 | 1863 | user, title, git_repository, commit_sha1, result_summary, | ||
941 | 1864 | url, result) | ||
942 | 1865 | |||
943 | 1866 | def makeRevisionStatusArtifact(self, lfa=None, report=None): | ||
944 | 1867 | """Create a new RevisionStatusArtifact.""" | ||
945 | 1868 | if lfa is None: | ||
946 | 1869 | lfa = self.makeLibraryFileAlias() | ||
947 | 1870 | if report is None: | ||
948 | 1871 | report = self.makeRevisionStatusReport() | ||
949 | 1872 | return getUtility(IRevisionStatusArtifactSet).new(lfa, report) | ||
950 | 1873 | |||
951 | 1846 | def makeBug(self, target=None, owner=None, bug_watch_url=None, | 1874 | def makeBug(self, target=None, owner=None, bug_watch_url=None, |
952 | 1847 | information_type=None, date_closed=None, title=None, | 1875 | information_type=None, date_closed=None, title=None, |
953 | 1848 | date_created=None, description=None, comment=None, | 1876 | date_created=None, description=None, comment=None, |
This is the branch in progress with the API test (test_newRevisi onStatusReport) failing and the model test (test_findRevis ionStatusReport ) passing. There are quite a few pre-commit long lines truncated as part of the MP as well in order to be able to commit.
In parallel I will be working on further changes on the model side + Unit Tests (to cover updates and deletes and the RevisionStatusA rtifact) - I won't be updating the MP so that input in current direction and failing unit test (test_newRevisi onStatusReport) can be provided.