Merge ~ilasc/launchpad:revision-status-submission-api into launchpad:master

Proposed by Ioana Lasc
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)
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

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Ioana Lasc (ilasc) wrote :

This is the branch in progress with the API test (test_newRevisionStatusReport) failing and the model test (test_findRevisionStatusReport) 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 RevisionStatusArtifact) - I won't be updating the MP so that input in current direction and failing unit test (test_newRevisionStatusReport) can be provided.

Revision history for this message
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/testing/factory.py` because they're buried under a pile of indentation changes and similar. I expect to see all of these unrelated changes reverted before I take another look at this, because it's difficult to review otherwise. It's probably a good idea to run `git diff master...` and look over the output (effectively doing a self-review) before you push anything.

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.

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Ioana Lasc (ilasc) wrote :

MP now ready for review.

Revision history for this message
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.

review: Needs Information
Revision history for this message
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 RevisionStatusArtifact while `log` needs to be in the interface for internal use, perhaps the exported API should be a `log_url` property

5: add at least a basic security adapter for the newky added IRevisionStatusArtifact

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*`

Revision history for this message
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.

Revision history for this message
Ioana Lasc (ilasc) wrote :

Indeed the comments were helpful Colin, thanks! This is now ready for review together with the DB patch (https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/409900).

Revision history for this message
Colin Watson (cjwatson) wrote :

Summary of the main points here, after which I think this will be good to go:

 * fix ambiguity in `RevisionStatusReport` items
 * export `IRevisionStatusReport.setLog` (or a variant of it that takes bytes rather than an artifact object) instead of `IGitRepository.setLogForStatusReport`
 * fix type of `IRevisionStatusReport.commit_sha1`
 * declare a proper feature rule exception
 * fix confusing definition of `EditRevisionStatusReport` security adapter

review: Needs Fixing
Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin! Addressed all comments and when moving  TestRevisionStatusReportWebserviceFunctionalLayer started failing.

Added TestRevisionStatusReport to try to figure out what is wrong in the URL formatting but they are both failing with:

HTTP/1.1 404 Not Found
Content-Length: 112
Content-Type: text/plain;charset=utf-8

Object: <GitRepository '~person-name-100000/product-name-100005/+git/gitrepository-100002' (1)>, name: '+status'

Looks like I need to define traversal / navigation for reports.

Revision history for this message
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_setLogOnRevisionStatusReport failing with 401 "Current authentication does not allow access to this object." I need to investigate probably on Monday AM.

Revision history for this message
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://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/412524, which allows access token targets to also be valid for objects subordinate to the target; that fixes this.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/_schema_circular_imports.py b/lib/lp/_schema_circular_imports.py
index af21a01..e239ac2 100644
--- a/lib/lp/_schema_circular_imports.py
+++ b/lib/lp/_schema_circular_imports.py
@@ -64,7 +64,10 @@ from lp.code.interfaces.codereviewcomment import ICodeReviewComment
64from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference64from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
65from lp.code.interfaces.diff import IPreviewDiff65from lp.code.interfaces.diff import IPreviewDiff
66from lp.code.interfaces.gitref import IGitRef66from lp.code.interfaces.gitref import IGitRef
67from lp.code.interfaces.gitrepository import IGitRepository67from lp.code.interfaces.gitrepository import (
68 IGitRepository,
69 IRevisionStatusReport,
70 )
68from lp.code.interfaces.gitrule import (71from lp.code.interfaces.gitrule import (
69 IGitNascentRule,72 IGitNascentRule,
70 IGitNascentRuleGrant,73 IGitNascentRuleGrant,
@@ -528,6 +531,10 @@ patch_collection_return_type(
528patch_list_parameter_type(531patch_list_parameter_type(
529 IGitRepository, 'setRules', 'rules', InlineObject(schema=IGitNascentRule))532 IGitRepository, 'setRules', 'rules', InlineObject(schema=IGitNascentRule))
530533
534# IRevisionStatusReport
535patch_reference_property(
536 IRevisionStatusReport, 'git_repository', IGitRepository)
537
531# ILiveFSFile538# ILiveFSFile
532patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild)539patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild)
533540
diff --git a/lib/lp/code/browser/configure.zcml b/lib/lp/code/browser/configure.zcml
index f7bab39..31b3938 100644
--- a/lib/lp/code/browser/configure.zcml
+++ b/lib/lp/code/browser/configure.zcml
@@ -963,6 +963,12 @@
963 factory="lp.code.browser.gitrepository.GitRepositoryBreadcrumb"963 factory="lp.code.browser.gitrepository.GitRepositoryBreadcrumb"
964 permission="zope.Public"/>964 permission="zope.Public"/>
965965
966 <browser:url
967 for="lp.code.interfaces.gitrepository.IRevisionStatusReport"
968 path_expression="string:+status/${id}"
969 attribute_to_parent="git_repository"
970 rootsite="code"/>
971
966 <browser:defaultView972 <browser:defaultView
967 for="lp.code.interfaces.gitref.IGitRef"973 for="lp.code.interfaces.gitref.IGitRef"
968 name="+index"/>974 name="+index"/>
diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
index 92a1b14..ddd79ca 100644
--- a/lib/lp/code/browser/gitrepository.py
+++ b/lib/lp/code/browser/gitrepository.py
@@ -106,6 +106,7 @@ from lp.code.interfaces.gitrepository import (
106 ContributorGitIdentity,106 ContributorGitIdentity,
107 IGitRepository,107 IGitRepository,
108 IGitRepositorySet,108 IGitRepositorySet,
109 IRevisionStatusReportSet,
109 )110 )
110from lp.code.vocabularies.gitrule import GitPermissionsVocabulary111from lp.code.vocabularies.gitrule import GitPermissionsVocabulary
111from lp.registry.interfaces.person import (112from lp.registry.interfaces.person import (
@@ -190,6 +191,18 @@ class GitRepositoryNavigation(WebhookTargetNavigationMixin, Navigation):
190191
191 usedfor = IGitRepository192 usedfor = IGitRepository
192193
194 @stepthrough('+status')
195 def traverse_status(self, id):
196 try:
197 report_id = int(id)
198 except ValueError:
199 raise NotFoundError(report_id)
200 report = getUtility(
201 IRevisionStatusReportSet).getByID(report_id)
202 if report is None:
203 raise NotFoundError(report_id)
204 return report
205
193 @stepto("+ref")206 @stepto("+ref")
194 def traverse_ref(self):207 def traverse_ref(self):
195 segments = list(self.request.getTraversalStack())208 segments = list(self.request.getTraversalStack())
diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
index 6417c89..fb8e0d5 100644
--- a/lib/lp/code/configure.zcml
+++ b/lib/lp/code/configure.zcml
@@ -953,6 +953,40 @@
953 <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" />953 <allow interface="lp.code.interfaces.gitref.IGitRefRemoteSet" />
954 </securedutility>954 </securedutility>
955955
956 <!-- RevisionStatusReport -->
957
958 <class class="lp.code.model.gitrepository.RevisionStatusReport">
959 <require
960 permission="launchpad.View"
961 interface="lp.code.interfaces.gitrepository.IRevisionStatusReportView
962 lp.code.interfaces.gitrepository.IRevisionStatusReportEditableAttributes" />
963 <require
964 permission="launchpad.Edit"
965 interface="lp.code.interfaces.gitrepository.IRevisionStatusReportEdit"
966 set_schema="lp.code.interfaces.gitrepository.IRevisionStatusReportEditableAttributes" />
967 </class>
968 <class class="lp.code.model.gitrepository.RevisionStatusArtifact">
969 <require
970 permission="launchpad.View"
971 interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifact" />
972 </class>
973 <class class="lp.code.model.gitrepository.RevisionStatusReportSet">
974 <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusReportSet" />
975 </class>
976 <securedutility
977 class="lp.code.model.gitrepository.RevisionStatusReportSet"
978 provides="lp.code.interfaces.gitrepository.IRevisionStatusReportSet">
979 <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusReportSet" />
980 </securedutility>
981 <class class="lp.code.model.gitrepository.RevisionStatusArtifactSet">
982 <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet" />
983 </class>
984 <securedutility
985 class="lp.code.model.gitrepository.RevisionStatusArtifactSet"
986 provides="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet">
987 <allow interface="lp.code.interfaces.gitrepository.IRevisionStatusArtifactSet" />
988 </securedutility>
989
956 <!-- Git repository access rules -->990 <!-- Git repository access rules -->
957991
958 <class class="lp.code.model.gitrule.GitRule">992 <class class="lp.code.model.gitrule.GitRule">
diff --git a/lib/lp/code/enums.py b/lib/lp/code/enums.py
index 3765754..2dbd287 100644
--- a/lib/lp/code/enums.py
+++ b/lib/lp/code/enums.py
@@ -29,6 +29,8 @@ __all__ = [
29 'GitRepositoryType',29 'GitRepositoryType',
30 'NON_CVS_RCS_TYPES',30 'NON_CVS_RCS_TYPES',
31 'RevisionControlSystems',31 'RevisionControlSystems',
32 'RevisionStatusArtifactType',
33 'RevisionStatusResult',
32 'TargetRevisionControlSystems',34 'TargetRevisionControlSystems',
33 ]35 ]
3436
@@ -252,6 +254,54 @@ class GitPermissionType(EnumeratedType):
252 CAN_FORCE_PUSH = Item("Can force-push")254 CAN_FORCE_PUSH = Item("Can force-push")
253255
254256
257class RevisionStatusArtifactType(DBEnumeratedType):
258 LOG = DBItem(0, """
259 Log
260
261 The log produced by the check job.
262 """)
263
264
265class RevisionStatusResult(DBEnumeratedType):
266 """Revision Status Result"""
267
268 WAITING = DBItem(0, """
269 Waiting
270
271 The check job is waiting to be run.
272 """)
273
274 RUNNING = DBItem(1, """
275 Running
276
277 The check job is currently running.
278 """)
279
280 SUCCEEDED = DBItem(2, """
281 Succeeded
282
283 The check job ran successfully.
284 """)
285
286 FAILED = DBItem(3, """
287 Failed
288
289 The check job failed.
290 """)
291
292 SKIPPED = DBItem(4, """
293 Skipped
294
295 The check job was skipped.
296 """)
297
298 CANCELLED = DBItem(5, """
299 Cancelled
300
301 The check job was cancelled.
302 """)
303
304
255class BranchLifecycleStatusFilter(EnumeratedType):305class BranchLifecycleStatusFilter(EnumeratedType):
256 """Branch Lifecycle Status Filter306 """Branch Lifecycle Status Filter
257307
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index cb12a5d..47fb9a6 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -13,9 +13,15 @@ __all__ = [
13 'IGitRepositoryExpensiveRequest',13 'IGitRepositoryExpensiveRequest',
14 'IGitRepositorySet',14 'IGitRepositorySet',
15 'IHasGitRepositoryURL',15 'IHasGitRepositoryURL',
16 'IRevisionStatusArtifact',
17 'IRevisionStatusArtifactSet',
18 'IRevisionStatusReport',
19 'IRevisionStatusReportSet',
20 'RevisionStatusReportsFeatureDisabled',
16 'user_has_special_git_repository_access',21 'user_has_special_git_repository_access',
17 ]22 ]
1823
24import http.client
19import re25import re
20from textwrap import dedent26from textwrap import dedent
2127
@@ -23,6 +29,7 @@ from lazr.lifecycle.snapshot import doNotSnapshot
23from lazr.restful.declarations import (29from lazr.restful.declarations import (
24 call_with,30 call_with,
25 collection_default_content,31 collection_default_content,
32 error_status,
26 export_destructor_operation,33 export_destructor_operation,
27 export_factory_operation,34 export_factory_operation,
28 export_operation_as,35 export_operation_as,
@@ -37,6 +44,7 @@ from lazr.restful.declarations import (
37 operation_returns_collection_of,44 operation_returns_collection_of,
38 operation_returns_entry,45 operation_returns_entry,
39 REQUEST_USER,46 REQUEST_USER,
47 scoped,
40 )48 )
41from lazr.restful.fields import (49from lazr.restful.fields import (
42 CollectionField,50 CollectionField,
@@ -50,6 +58,7 @@ from zope.interface import (
50 )58 )
51from zope.schema import (59from zope.schema import (
52 Bool,60 Bool,
61 Bytes,
53 Choice,62 Choice,
54 Datetime,63 Datetime,
55 Int,64 Int,
@@ -57,10 +66,12 @@ from zope.schema import (
57 Text,66 Text,
58 TextLine,67 TextLine,
59 )68 )
69from zope.security.interfaces import Unauthorized
6070
61from lp import _71from lp import _
62from lp.app.enums import InformationType72from lp.app.enums import InformationType
63from lp.app.validators import LaunchpadValidationError73from lp.app.validators import LaunchpadValidationError
74from lp.app.validators.attachment import attachment_size_constraint
64from lp.code.enums import (75from lp.code.enums import (
65 BranchMergeProposalStatus,76 BranchMergeProposalStatus,
66 BranchSubscriptionDiffSize,77 BranchSubscriptionDiffSize,
@@ -69,6 +80,8 @@ from lp.code.enums import (
69 GitListingSort,80 GitListingSort,
70 GitRepositoryStatus,81 GitRepositoryStatus,
71 GitRepositoryType,82 GitRepositoryType,
83 RevisionStatusArtifactType,
84 RevisionStatusResult,
72 )85 )
73from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository86from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
74from lp.code.interfaces.hasgitrepositories import IHasGitRepositories87from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
@@ -91,6 +104,7 @@ from lp.services.fields import (
91 InlineObject,104 InlineObject,
92 PersonChoice,105 PersonChoice,
93 PublicPersonChoice,106 PublicPersonChoice,
107 URIField,
94 )108 )
95from lp.services.webhooks.interfaces import IWebhookTarget109from lp.services.webhooks.interfaces import IWebhookTarget
96110
@@ -813,6 +827,152 @@ class IGitRepositoryExpensiveRequest(Interface):
813 that is not an admin or a registry expert."""827 that is not an admin or a registry expert."""
814828
815829
830@error_status(http.client.UNAUTHORIZED)
831class RevisionStatusReportsFeatureDisabled(Unauthorized):
832 """Only certain users can access APIs for revision status reports."""
833
834 def __init__(self):
835 super(RevisionStatusReportsFeatureDisabled, self).__init__(
836 "You do not have permission to create revision status reports")
837
838
839class IRevisionStatusReportView(Interface):
840 """`IRevisionStatusReport` attributes that require launchpad.View."""
841
842 id = Int(title=_("ID"), required=True, readonly=True)
843
844 date_created = exported(Datetime(
845 title=_("When the report was created."), required=True, readonly=True))
846 date_started = exported(Datetime(
847 title=_("When the report was started.")), readonly=False)
848 date_finished = exported(Datetime(
849 title=_("When the report has finished.")), readonly=False)
850
851
852class IRevisionStatusReportEditableAttributes(Interface):
853 """`IRevisionStatusReport` attributes that can be edited.
854
855 These attributes need launchpad.View to see, and launchpad.Edit to change.
856 """
857
858 title = exported(TextLine(
859 title=_("A short title for the report."), required=True))
860
861 git_repository = exported(Reference(
862 title=_("The Git repository for which this report is built."),
863 # Really IGitRepository, patched in _schema_circular_imports.py.
864 schema=Interface, required=True, readonly=True))
865
866 commit_sha1 = exported(TextLine(
867 title=_("The Git commit for which this report is built."),
868 required=True, readonly=True))
869
870 url = exported(URIField(title=_("URL"), required=False, readonly=True,
871 description=_("The external url of the report.")))
872
873 result_summary = exported(TextLine(
874 title=_("A short summary of the result."), required=False))
875
876 result = exported(Choice(
877 title=_('Result of the report'), readonly=True,
878 required=False, vocabulary=RevisionStatusResult))
879
880 @mutator_for(result)
881 @operation_parameters(result=copy_field(result))
882 @export_write_operation()
883 @operation_for_version("devel")
884 def transitionToNewResult(result):
885 """Set the RevisionStatusReport result.
886
887 Set the revision status report result."""
888
889
890class IRevisionStatusReportEdit(Interface):
891 """`IRevisionStatusReport` attributes that require launchpad.Edit."""
892
893 @operation_parameters(
894 log_data=Bytes(title=_("The content of the artifact in bytes."),
895 constraint=attachment_size_constraint))
896 @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title)
897 @export_write_operation()
898 @export_operation_as(name="setLog")
899 @operation_for_version("devel")
900 def api_setLog(log_data):
901 """Set a new log on an existing status report.
902
903 :param log_data: The contents (in bytes) of the log.
904 """
905
906
907@exported_as_webservice_entry(as_of="beta")
908class IRevisionStatusReport(IRevisionStatusReportView,
909 IRevisionStatusReportEditableAttributes,
910 IRevisionStatusReportEdit):
911 """An revision status report for a Git commit."""
912
913
914class IRevisionStatusReportSet(Interface):
915 """The set of all revision status reports."""
916
917 def new(creator, title, git_repository, commit_sha1, date_created=None,
918 url=None, result_summary=None, result=None, date_started=None,
919 date_finished=None, log=None):
920 """Return a new revision status report.
921
922 :param title: A text string.
923 :param git_repository: An `IGitRepository` for which the report
924 is being created.
925 :param commit_sha1: The sha1 of the commit for which the report
926 is being created.
927 :param date_created: The date when the report is being created.
928 :param url: External URL to view result of report.
929 :param result_summary: A short summary of the result.
930 :param result: The result of the check job for this revision.
931 :param date_started: DateTime that report was started.
932 :param date_finished: DateTime that report was completed.
933 :param log: Stores the content of the artifact for this report.
934 """
935
936 def getByID(id):
937 """Returns the RevisionStatusReport for a given ID."""
938
939 def findByRepository(repository):
940 """Returns the set of RevisionStatusReport for a repository."""
941
942
943class IRevisionStatusArtifactSet(Interface):
944 """The set of all revision status artifacts."""
945
946 def new(lfa, report):
947 """Return a new revision status artifact.
948
949 :param lfa: An `ILibraryFileAlias`.
950 :param report: An `IRevisionStatusReport` for which the
951 artifact is being created.
952 """
953
954 def getByID(id):
955 """Returns the RevisionStatusArtifact for a given ID."""
956
957 def findByReport(report):
958 """Returns the set of artifacts for a given report."""
959
960
961class IRevisionStatusArtifact(Interface):
962 id = Int(title=_("ID"), required=True, readonly=True)
963
964 report = Attribute(
965 "The `RevisionStatusReport` that this artifact is linked to.")
966
967 library_file = Attribute(
968 "The `LibraryFileAlias` object containing information for "
969 "a revision status report.")
970
971 artifact_type = Choice(
972 title=_('The type of artifact, only log for now.'),
973 vocabulary=RevisionStatusArtifactType)
974
975
816class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget):976class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget):
817 """IGitRepository methods that require launchpad.Edit permission."""977 """IGitRepository methods that require launchpad.Edit permission."""
818978
@@ -1015,6 +1175,26 @@ class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget):
1015 :raise: CannotDeleteGitRepository if the repository cannot be deleted.1175 :raise: CannotDeleteGitRepository if the repository cannot be deleted.
1016 """1176 """
10171177
1178 @operation_parameters(
1179 title=copy_field(IRevisionStatusReport["title"]),
1180 commit_sha1=copy_field(IRevisionStatusReport["commit_sha1"]),
1181 url=copy_field(IRevisionStatusReport["url"]),
1182 result_summary=copy_field(IRevisionStatusReport["result_summary"]),
1183 result=copy_field(IRevisionStatusReport["result"]))
1184 @scoped(AccessTokenScope.REPOSITORY_BUILD_STATUS.title)
1185 @call_with(user=REQUEST_USER)
1186 @export_factory_operation(IRevisionStatusReport, [])
1187 @operation_for_version("devel")
1188 def newStatusReport(title, commit_sha1, url, result_summary, result, user):
1189 """Create a new status report.
1190
1191 :param title: The name of the new report.
1192 :param commit_sha1: The commit sha1 for the report.
1193 :param url: The external link of the status report.
1194 :param result_summary: The description of the new report.
1195 :param result: The result of the new report.
1196 """
1197
10181198
1019# XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL1199# XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
1020# generation working. Individual attributes must set their version to1200# generation working. Individual attributes must set their version to
diff --git a/lib/lp/code/interfaces/webservice.py b/lib/lp/code/interfaces/webservice.py
index 02546e9..bd94298 100644
--- a/lib/lp/code/interfaces/webservice.py
+++ b/lib/lp/code/interfaces/webservice.py
@@ -31,6 +31,7 @@ __all__ = [
31 'IGitSubscription',31 'IGitSubscription',
32 'IHasGitRepositories',32 'IHasGitRepositories',
33 'IPreviewDiff',33 'IPreviewDiff',
34 'IRevisionStatusReport',
34 'ISourcePackageRecipe',35 'ISourcePackageRecipe',
35 'ISourcePackageRecipeBuild',36 'ISourcePackageRecipeBuild',
36 ]37 ]
@@ -66,6 +67,7 @@ from lp.code.interfaces.gitref import IGitRef
66from lp.code.interfaces.gitrepository import (67from lp.code.interfaces.gitrepository import (
67 IGitRepository,68 IGitRepository,
68 IGitRepositorySet,69 IGitRepositorySet,
70 IRevisionStatusReport,
69 )71 )
70from lp.code.interfaces.gitsubscription import IGitSubscription72from lp.code.interfaces.gitsubscription import IGitSubscription
71from lp.code.interfaces.hasgitrepositories import IHasGitRepositories73from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 812f1d7..501ef29 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -6,6 +6,7 @@ __all__ = [
6 'GitRepository',6 'GitRepository',
7 'GitRepositorySet',7 'GitRepositorySet',
8 'parse_git_commits',8 'parse_git_commits',
9 'RevisionStatusReport',
9 ]10 ]
1011
11from collections import (12from collections import (
@@ -19,6 +20,7 @@ from datetime import (
19import email20import email
20from fnmatch import fnmatch21from fnmatch import fnmatch
21from functools import partial22from functools import partial
23import io
22from itertools import (24from itertools import (
23 chain,25 chain,
24 groupby,26 groupby,
@@ -102,6 +104,8 @@ from lp.code.enums import (
102 GitPermissionType,104 GitPermissionType,
103 GitRepositoryStatus,105 GitRepositoryStatus,
104 GitRepositoryType,106 GitRepositoryType,
107 RevisionStatusArtifactType,
108 RevisionStatusResult,
105 )109 )
106from lp.code.errors import (110from lp.code.errors import (
107 CannotDeleteGitRepository,111 CannotDeleteGitRepository,
@@ -131,6 +135,10 @@ from lp.code.interfaces.gitrepository import (
131 GitIdentityMixin,135 GitIdentityMixin,
132 IGitRepository,136 IGitRepository,
133 IGitRepositorySet,137 IGitRepositorySet,
138 IRevisionStatusArtifactSet,
139 IRevisionStatusReport,
140 IRevisionStatusReportSet,
141 RevisionStatusReportsFeatureDisabled,
134 user_has_special_git_repository_access,142 user_has_special_git_repository_access,
135 )143 )
136from lp.code.interfaces.gitrule import (144from lp.code.interfaces.gitrule import (
@@ -205,6 +213,7 @@ from lp.services.identity.interfaces.account import (
205 )213 )
206from lp.services.job.interfaces.job import JobStatus214from lp.services.job.interfaces.job import JobStatus
207from lp.services.job.model.job import Job215from lp.services.job.model.job import Job
216from lp.services.librarian.interfaces import ILibraryFileAliasSet
208from lp.services.macaroons.interfaces import IMacaroonIssuer217from lp.services.macaroons.interfaces import IMacaroonIssuer
209from lp.services.macaroons.model import MacaroonIssuerBase218from lp.services.macaroons.model import MacaroonIssuerBase
210from lp.services.mail.notificationrecipientset import NotificationRecipientSet219from lp.services.mail.notificationrecipientset import NotificationRecipientSet
@@ -219,8 +228,9 @@ from lp.services.webhooks.model import WebhookTargetMixin
219from lp.snappy.interfaces.snap import ISnapSet228from lp.snappy.interfaces.snap import ISnapSet
220229
221230
222logger = logging.getLogger(__name__)231REVISION_STATUS_REPORT_ALLOW_CREATE = 'revision_status_report.allow_create'
223232
233logger = logging.getLogger(__name__)
224234
225object_type_map = {235object_type_map = {
226 "commit": GitObjectType.COMMIT,236 "commit": GitObjectType.COMMIT,
@@ -292,6 +302,132 @@ def git_repository_modified(repository, event):
292 send_git_repository_modified_notifications(repository, event)302 send_git_repository_modified_notifications(repository, event)
293303
294304
305@implementer(IRevisionStatusReport)
306class RevisionStatusReport(StormBase):
307 __storm_table__ = 'RevisionStatusReport'
308
309 id = Int(primary=True)
310
311 creator_id = Int(name="creator", allow_none=False)
312 creator = Reference(creator_id, "Person.id")
313
314 title = Unicode(name='name', allow_none=False)
315
316 git_repository_id = Int(name='git_repository', allow_none=False)
317 git_repository = Reference(git_repository_id, 'GitRepository.id')
318
319 commit_sha1 = Unicode(name='commit_sha1', allow_none=False)
320
321 url = Unicode(name='url', allow_none=True)
322
323 result_summary = Unicode(name='description', allow_none=True)
324
325 result = DBEnum(name='result', allow_none=True, enum=RevisionStatusResult)
326
327 date_created = DateTime(
328 name='date_created', tzinfo=pytz.UTC, allow_none=False)
329
330 date_started = DateTime(name='date_started', tzinfo=pytz.UTC,
331 allow_none=True)
332 date_finished = DateTime(name='date_finished', tzinfo=pytz.UTC,
333 allow_none=True)
334
335 def __init__(self, git_repository, user, title, commit_sha1,
336 url, result_summary, result):
337 super().__init__()
338 self.creator = user
339 self.git_repository = git_repository
340 self.title = title
341 self.commit_sha1 = commit_sha1
342 self.url = url
343 self.result_summary = result_summary
344 self.result = result
345 self.date_created = UTC_NOW
346
347 def api_setLog(self, log_data):
348 filename = '%s-%s.txt' % (self.title, self.commit_sha1)
349
350 lfa = getUtility(ILibraryFileAliasSet).create(
351 name=filename, size=len(log_data),
352 file=io.BytesIO(log_data), contentType='text/plain')
353
354 getUtility(IRevisionStatusArtifactSet).new(lfa, self)
355
356 def transitionToNewResult(self, result):
357 if self.result == RevisionStatusResult.WAITING:
358 if result == RevisionStatusResult.RUNNING:
359 self.date_started == UTC_NOW
360 else:
361 self.date_finished = UTC_NOW
362 self.result = result
363
364
365@implementer(IRevisionStatusReportSet)
366class RevisionStatusReportSet:
367
368 def new(self, creator, title, git_repository, commit_sha1,
369 url=None, result_summary=None, result=None,
370 date_started=None, date_finished=None, log=None):
371 """See `IRevisionStatusReportSet`."""
372 store = IStore(RevisionStatusReport)
373 report = RevisionStatusReport(git_repository, creator, title,
374 commit_sha1, url, result_summary,
375 result)
376 store.add(report)
377 return report
378
379 def getByID(self, id):
380 return IStore(
381 RevisionStatusReport).find(RevisionStatusReport, id=id).one()
382
383 def findByRepository(self, repository):
384 return IStore(RevisionStatusReport).find(
385 RevisionStatusReport,
386 RevisionStatusReport.git_repository == repository)
387
388
389class RevisionStatusArtifact(StormBase):
390 __storm_table__ = 'RevisionStatusArtifact'
391
392 id = Int(primary=True)
393
394 library_file_id = Int(name='library_file', allow_none=False)
395 library_file = Reference(library_file_id, 'LibraryFileAlias.id')
396
397 report_id = Int(name='report', allow_none=False)
398 report = Reference(report_id, 'RevisionStatusReport.id')
399
400 artifact_type = DBEnum(name='type', allow_none=False,
401 enum=RevisionStatusArtifactType)
402
403 def __init__(self, library_file, report):
404 super().__init__()
405 self.library_file = library_file
406 self.report = report
407 self.artifact_type = RevisionStatusArtifactType.LOG
408
409
410@implementer(IRevisionStatusArtifactSet)
411class RevisionStatusArtifactSet:
412
413 def new(self, lfa, report):
414 """See `IRevisionStatusArtifactSet`."""
415 store = IStore(RevisionStatusArtifact)
416 artifact = RevisionStatusArtifact(lfa, report)
417 store.add(artifact)
418 return artifact
419
420 def getById(self, id):
421 return IStore(RevisionStatusArtifact).find(
422 RevisionStatusArtifact,
423 RevisionStatusArtifact.id == id).one()
424
425 def findByReport(self, report):
426 return IStore(RevisionStatusArtifact).find(
427 RevisionStatusArtifact,
428 RevisionStatusArtifact.report == report)
429
430
295@implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType)431@implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType)
296class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin,432class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin,
297 GitIdentityMixin):433 GitIdentityMixin):
@@ -501,6 +637,16 @@ class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin,
501 def collectGarbage(self):637 def collectGarbage(self):
502 getUtility(IGitHostingClient).collectGarbage(self.getInternalPath())638 getUtility(IGitHostingClient).collectGarbage(self.getInternalPath())
503639
640 def newStatusReport(self, user, title, commit_sha1, url=None,
641 result_summary=None, result=None):
642
643 if not getFeatureFlag(REVISION_STATUS_REPORT_ALLOW_CREATE):
644 raise RevisionStatusReportsFeatureDisabled()
645
646 report = RevisionStatusReport(self, user, title, commit_sha1,
647 url, result_summary, result)
648 return report
649
504 @property650 @property
505 def namespace(self):651 def namespace(self):
506 """See `IGitRepository`."""652 """See `IGitRepository`."""
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 252b535..764d13c 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -10,6 +10,7 @@ from datetime import (
10import email10import email
11from functools import partial11from functools import partial
12import hashlib12import hashlib
13import io
13import json14import json
1415
15from breezy import urlutils16from breezy import urlutils
@@ -65,6 +66,7 @@ from lp.code.enums import (
65 GitObjectType,66 GitObjectType,
66 GitRepositoryStatus,67 GitRepositoryStatus,
67 GitRepositoryType,68 GitRepositoryType,
69 RevisionStatusResult,
68 TargetRevisionControlSystems,70 TargetRevisionControlSystems,
69 )71 )
70from lp.code.errors import (72from lp.code.errors import (
@@ -95,6 +97,8 @@ from lp.code.interfaces.gitrepository import (
95 IGitRepository,97 IGitRepository,
96 IGitRepositorySet,98 IGitRepositorySet,
97 IGitRepositoryView,99 IGitRepositoryView,
100 IRevisionStatusArtifactSet,
101 IRevisionStatusReportSet,
98 )102 )
99from lp.code.interfaces.gitrule import (103from lp.code.interfaces.gitrule import (
100 IGitNascentRule,104 IGitNascentRule,
@@ -121,6 +125,7 @@ from lp.code.model.gitrepository import (
121 DeletionCallable,125 DeletionCallable,
122 DeletionOperation,126 DeletionOperation,
123 GitRepository,127 GitRepository,
128 REVISION_STATUS_REPORT_ALLOW_CREATE,
124 )129 )
125from lp.code.tests.helpers import GitHostingFixture130from lp.code.tests.helpers import GitHostingFixture
126from lp.code.xmlrpc.git import GitAPI131from lp.code.xmlrpc.git import GitAPI
@@ -571,6 +576,24 @@ class TestGitRepository(TestCaseWithFactory):
571 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))576 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
572 self.assertEqual(7, recorder1.count)577 self.assertEqual(7, recorder1.count)
573578
579 def test_findRevisionStatusReport(self):
580 repository = removeSecurityProxy(self.factory.makeGitRepository())
581 title = self.factory.getUniqueUnicode('report-title')
582 commit_sha1 = hashlib.sha1(b"Some content").hexdigest()
583 result_summary = "120/120 tests passed"
584
585 report = self.factory.makeRevisionStatusReport(
586 user=repository.owner, git_repository=repository,
587 title=title, commit_sha1=commit_sha1,
588 result_summary=result_summary,
589 result=RevisionStatusResult.SUCCEEDED)
590
591 with person_logged_in(repository.owner):
592 result = getUtility(
593 IRevisionStatusReportSet).getByID(
594 report.id)
595 self.assertEqual(report, result)
596
574597
575class TestGitIdentityMixin(TestCaseWithFactory):598class TestGitIdentityMixin(TestCaseWithFactory):
576 """Test the defaults and identities provided by GitIdentityMixin."""599 """Test the defaults and identities provided by GitIdentityMixin."""
@@ -4241,6 +4264,66 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
4241 self.assertEqual(4264 self.assertEqual(
4242 InformationType.PUBLIC, repository_db.information_type)4265 InformationType.PUBLIC, repository_db.information_type)
42434266
4267 def test_newRevisionStatusReport_featureFlagDisabled(self):
4268 repository = self.factory.makeGitRepository()
4269 requester = repository.owner
4270 webservice = webservice_for_person(None, default_api_version="devel")
4271 with person_logged_in(requester):
4272 repository_url = api_url(repository)
4273
4274 secret, _ = self.factory.makeAccessToken(
4275 owner=requester, target=repository,
4276 scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS])
4277 header = {'Authorization': 'Token %s' % secret}
4278
4279 response = webservice.named_post(
4280 repository_url, "newStatusReport",
4281 headers=header, title="CI",
4282 commit_sha1=hashlib.sha1(
4283 self.factory.getUniqueBytes()).hexdigest(),
4284 url='https://launchpad.net/',
4285 result_summary="120/120 tests passed",
4286 result="Succeeded")
4287
4288 self.assertEqual(401, response.status)
4289 self.assertIn(
4290 b'You do not have permission to create revision status reports',
4291 response.body)
4292
4293 def test_newRevisionStatusReport(self):
4294 repository = self.factory.makeGitRepository()
4295 requester = repository.owner
4296 webservice = webservice_for_person(None, default_api_version="devel")
4297 with person_logged_in(requester):
4298 repository_url = api_url(repository)
4299
4300 secret, _ = self.factory.makeAccessToken(
4301 owner=requester, target=repository,
4302 scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS])
4303 header = {'Authorization': 'Token %s' % secret}
4304
4305 self.useFixture(FeatureFixture(
4306 {REVISION_STATUS_REPORT_ALLOW_CREATE: "on"}))
4307
4308 response = webservice.named_post(
4309 repository_url, "newStatusReport",
4310 headers=header, title="CI",
4311 commit_sha1=hashlib.sha1(
4312 self.factory.getUniqueBytes()).hexdigest(),
4313 url='https://launchpad.net/',
4314 result_summary="120/120 tests passed",
4315 result="Succeeded")
4316 self.assertEqual(201, response.status)
4317
4318 with person_logged_in(requester):
4319 results = getUtility(
4320 IRevisionStatusReportSet).findByRepository(repository)
4321 reports = list(results)
4322 urls = [webservice.getAbsoluteUrl('%s/+status/%s' % (
4323 api_url(repository),
4324 report.id)) for report in reports]
4325 self.assertIn(response.getHeader("Location"), urls)
4326
4244 def test_set_target(self):4327 def test_set_target(self):
4245 # The repository owner can move the repository to another target;4328 # The repository owner can move the repository to another target;
4246 # this redirects to the new location.4329 # this redirects to the new location.
@@ -4831,6 +4914,60 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
4831 response.body)4914 response.body)
48324915
48334916
4917class TestRevisionStatusReportWebservice(TestCaseWithFactory):
4918 layer = LaunchpadFunctionalLayer
4919
4920 def setUp(self):
4921 super(TestRevisionStatusReportWebservice, self).setUp()
4922 self.repository = self.factory.makeGitRepository()
4923 self.requester = self.repository.owner
4924 title = self.factory.getUniqueUnicode('report-title')
4925 commit_sha1 = hashlib.sha1(b"Some content").hexdigest()
4926 result_summary = "120/120 tests passed"
4927
4928 self.report = self.factory.makeRevisionStatusReport(
4929 user=self.repository.owner, git_repository=self.repository,
4930 title=title, commit_sha1=commit_sha1,
4931 result_summary=result_summary,
4932 result=RevisionStatusResult.SUCCEEDED)
4933
4934 self.webservice = webservice_for_person(
4935 None, default_api_version="devel")
4936 with person_logged_in(self.requester):
4937 self.report_url = api_url(self.report)
4938
4939 secret, _ = self.factory.makeAccessToken(
4940 owner=self.requester, target=self.repository,
4941 scopes=[AccessTokenScope.REPOSITORY_BUILD_STATUS])
4942 self.header = {'Authorization': 'Token %s' % secret}
4943
4944 def test_setLogOnRevisionStatusReport(self):
4945 content = b'log_content_data'
4946 filesize = len(content)
4947 sha1 = hashlib.sha1(content).hexdigest()
4948 md5 = hashlib.md5(content).hexdigest()
4949 response = self.webservice.named_post(
4950 self.report_url, "setLog",
4951 headers=self.header,
4952 log_data=io.BytesIO(content))
4953 self.assertEqual(200, response.status)
4954
4955 # A report may have multiple artifacts.
4956 # We verify that the content we just submitted via API now
4957 # matches one of the artifacts in the DB for the report.
4958 with person_logged_in(self.requester):
4959 artifacts = list(getUtility(
4960 IRevisionStatusArtifactSet).findByReport(self.report))
4961 lfcs = [artifact.library_file.content for artifact in artifacts]
4962 sha1_of_all_artifacts = [lfc.sha1 for lfc in lfcs]
4963 md5_of_all_artifacts = [lfc.md5 for lfc in lfcs]
4964 filesizes_of_all_artifacts = [lfc.filesize for lfc in lfcs]
4965
4966 self.assertIn(sha1, sha1_of_all_artifacts)
4967 self.assertIn(md5, md5_of_all_artifacts)
4968 self.assertIn(filesize, filesizes_of_all_artifacts)
4969
4970
4834class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):4971class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
4835 """Test GitRepository macaroon issuing and verification."""4972 """Test GitRepository macaroon issuing and verification."""
48364973
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 8f66fbb..8435167 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -99,6 +99,8 @@ from lp.code.interfaces.gitcollection import IGitCollection
99from lp.code.interfaces.gitref import IGitRef99from lp.code.interfaces.gitref import IGitRef
100from lp.code.interfaces.gitrepository import (100from lp.code.interfaces.gitrepository import (
101 IGitRepository,101 IGitRepository,
102 IRevisionStatusArtifact,
103 IRevisionStatusReport,
102 user_has_special_git_repository_access,104 user_has_special_git_repository_access,
103 )105 )
104from lp.code.interfaces.gitrule import (106from lp.code.interfaces.gitrule import (
@@ -683,6 +685,23 @@ class EditSpecificationByRelatedPeople(AuthorizationBase):
683 self.obj, ['owner', 'drafter', 'assignee', 'approver']))685 self.obj, ['owner', 'drafter', 'assignee', 'approver']))
684686
685687
688class EditRevisionStatusReport(AuthorizationBase):
689 """The owner of a Git repository can edit its status reports."""
690 permission = 'launchpad.Edit'
691 usedfor = IRevisionStatusReport
692
693 def checkAuthenticated(self, user):
694 return user.isOwner(self.obj.git_repository)
695
696
697class EditRevisionStatusArtifact(DelegatedAuthorization):
698 permission = 'launchpad.Edit'
699 usedfor = IRevisionStatusArtifact
700
701 def __init__(self, obj):
702 super().__init__(obj, obj.report, 'launchpad.Edit')
703
704
686class AdminSpecification(AuthorizationBase):705class AdminSpecification(AuthorizationBase):
687 permission = 'launchpad.Admin'706 permission = 'launchpad.Admin'
688 usedfor = ISpecification707 usedfor = ISpecification
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 9d59ba8..db48ed3 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -131,7 +131,10 @@ from lp.code.interfaces.gitref import (
131 IGitRef,131 IGitRef,
132 IGitRefRemoteSet,132 IGitRefRemoteSet,
133 )133 )
134from lp.code.interfaces.gitrepository import IGitRepository134from lp.code.interfaces.gitrepository import (
135 IGitRepository,
136 IRevisionStatusArtifactSet,
137 )
135from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch138from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
136from lp.code.interfaces.revision import IRevisionSet139from lp.code.interfaces.revision import IRevisionSet
137from lp.code.interfaces.sourcepackagerecipe import (140from lp.code.interfaces.sourcepackagerecipe import (
@@ -146,6 +149,7 @@ from lp.code.model.diff import (
146 Diff,149 Diff,
147 PreviewDiff,150 PreviewDiff,
148 )151 )
152from lp.code.model.gitrepository import IRevisionStatusReportSet
149from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet153from lp.oci.interfaces.ocipushrule import IOCIPushRuleSet
150from lp.oci.interfaces.ocirecipe import IOCIRecipeSet154from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
151from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet155from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
@@ -1843,6 +1847,30 @@ class BareLaunchpadObjectFactory(ObjectFactory):
1843 grantee, grantor, can_create=can_create, can_push=can_push,1847 grantee, grantor, can_create=can_create, can_push=can_push,
1844 can_force_push=can_force_push)1848 can_force_push=can_force_push)
18451849
1850 def makeRevisionStatusReport(self, user=None, title=None,
1851 git_repository=None, commit_sha1=None,
1852 result_summary=None, url=None, result=None):
1853 """Create a new RevisionStatusReport."""
1854 if title is None:
1855 title = self.getUniqueUnicode()
1856 if git_repository is None:
1857 git_repository = self.makeGitRepository()
1858 if user is None:
1859 user = git_repository.owner
1860 if commit_sha1 is None:
1861 commit_sha1 = hashlib.sha1(self.getUniqueBytes()).hexdigest()
1862 return getUtility(IRevisionStatusReportSet).new(
1863 user, title, git_repository, commit_sha1, result_summary,
1864 url, result)
1865
1866 def makeRevisionStatusArtifact(self, lfa=None, report=None):
1867 """Create a new RevisionStatusArtifact."""
1868 if lfa is None:
1869 lfa = self.makeLibraryFileAlias()
1870 if report is None:
1871 report = self.makeRevisionStatusReport()
1872 return getUtility(IRevisionStatusArtifactSet).new(lfa, report)
1873
1846 def makeBug(self, target=None, owner=None, bug_watch_url=None,1874 def makeBug(self, target=None, owner=None, bug_watch_url=None,
1847 information_type=None, date_closed=None, title=None,1875 information_type=None, date_closed=None, title=None,
1848 date_created=None, description=None, comment=None,1876 date_created=None, description=None, comment=None,

Subscribers

People subscribed via source and target branches

to status/vote changes: