← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Ioana Lasc has proposed merging ~ilasc/launchpad:revision-status-submission-api into launchpad:master.

Commit message:
Add revision status submission API

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ilasc/launchpad/+git/launchpad/+merge/410373
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/launchpad:revision-status-submission-api into launchpad:master.
diff --git a/lib/lp/code/enums.py b/lib/lp/code/enums.py
index 3765754..7f40e69 100644
--- a/lib/lp/code/enums.py
+++ b/lib/lp/code/enums.py
@@ -252,6 +252,32 @@ class GitPermissionType(EnumeratedType):
     CAN_FORCE_PUSH = Item("Can force-push")
 
 
+class RevisionStatus(EnumeratedType):
+    """Revision Status
+
+    The status that a RevisionStatusReport can have.
+    """
+    QUEUED = Item("Queued")
+
+    STARTED = Item("Started")
+
+    COMPLETED = Item("Completed")
+
+    FAILED_TO_START = Item("FailedToStart")
+
+
+class RevisionStatusResult(EnumeratedType):
+    """Revision Status Result"""
+
+    SUCCESS = Item("Success")
+
+    FAILED = Item("Failed")
+
+    SKIPPED = Item("Skipped")
+
+    CANCELLED = Item("Cancelled")
+
+
 class BranchLifecycleStatusFilter(EnumeratedType):
     """Branch Lifecycle Status Filter
 
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index 363356d..75c088a 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -36,8 +36,8 @@ from lazr.restful.declarations import (
     operation_parameters,
     operation_returns_collection_of,
     operation_returns_entry,
-    REQUEST_USER,
-    )
+    REQUEST_USER, scoped,
+)
 from lazr.restful.fields import (
     CollectionField,
     Reference,
@@ -68,7 +68,7 @@ from lp.code.enums import (
     CodeReviewNotificationLevel,
     GitListingSort,
     GitRepositoryStatus,
-    GitRepositoryType,
+    GitRepositoryType, RevisionStatus, RevisionStatusResult,
     )
 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
 from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
@@ -749,6 +749,33 @@ class IGitRepositoryModerateAttributes(Interface):
         description=_("A short description of this repository.")))
 
 
+class IGitRepositoryBuildStatus(Interface):
+
+    @operation_parameters(
+        name=TextLine(title=_("The name of the status report.")),
+        status=List(
+            title=_("A list of report statuses to filter by."),
+            value_type=Choice(vocabulary=RevisionStatus)),
+        description=TextLine(title=_("The description of the status report.")),
+        commit_sha1=TextLine(title=_("The commit sha1 of the status report.")),
+        result=List(
+            title=_("A list of report result statuses to filter by."),
+            value_type=Choice(vocabulary=RevisionStatusResult)))
+    @scoped('repository:build_status')
+    @call_with(user=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version("devel")
+    def newRevisionStatusReport(name, status, description, commit_sha1, result, user):
+        """Create a New Status Report and return its ID.
+
+        :param name: The name of the new report.
+        :param status: The `RevisionStatus` of the new report.
+        :param description: The description of the new report.
+        :param commit_sha1: The commit sha1 for the report.
+        :param result: The result of the new report.
+        """
+
+
 class IGitRepositoryModerate(Interface):
     """IGitRepository methods that can be called by more than one community."""
 
@@ -1014,7 +1041,8 @@ class IGitRepositoryEdit(IWebhookTarget, IAccessTokenTarget):
 @exported_as_webservice_entry(plural_name="git_repositories", as_of="beta")
 class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes,
                      IGitRepositoryModerate, IGitRepositoryEditableAttributes,
-                     IGitRepositoryEdit, IGitRepositoryExpensiveRequest):
+                     IGitRepositoryEdit, IGitRepositoryExpensiveRequest,
+                     IGitRepositoryBuildStatus):
     """A Git repository."""
 
     private = exported(Bool(
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 99d451b..63ec4e8 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -30,6 +30,7 @@ from breezy import urlutils
 from lazr.enum import DBItem
 from lazr.lifecycle.event import ObjectModifiedEvent
 from lazr.lifecycle.snapshot import Snapshot
+from lazr.restful.declarations import scoped
 import pytz
 import six
 from six.moves.urllib.parse import (
@@ -177,6 +178,7 @@ from lp.registry.model.accesspolicy import (
     )
 from lp.registry.model.person import Person
 from lp.registry.model.teammembership import TeamParticipation
+from lp.services.auth.enums import AccessTokenScope
 from lp.services.auth.interfaces import IAccessTokenSet
 from lp.services.auth.model import AccessTokenTargetMixin
 from lp.services.auth.utils import create_access_token_secret
@@ -501,6 +503,11 @@ class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin,
     def collectGarbage(self):
         getUtility(IGitHostingClient).collectGarbage(self.getInternalPath())
 
+    @scoped('repository:build_status')
+    def newRevisionStatusReport(self, name, status, description, commit_sha1, result, user):
+        """See `IGitRepositoryBuildStatus`."""
+        print('test')
+
     @property
     def namespace(self):
         """See `IGitRepository`."""
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index a832554..8887a46 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -6,15 +6,18 @@
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for Git repositories."""
-
+import re
+from contextlib import contextmanager
 from datetime import (
     datetime,
     timedelta,
-    )
+)
 import email
 from functools import partial
 import hashlib
 import json
+from http.client import responses
+from zope.security.management import newInteraction
 
 from breezy import urlutils
 from fixtures import MockPatch
@@ -22,6 +25,11 @@ from lazr.lifecycle.event import ObjectModifiedEvent
 from pymacaroons import Macaroon
 import pytz
 import six
+
+from lp.services.auth.enums import AccessTokenScope
+from lp.services.auth.interfaces import IAccessTokenSet
+from lp.services.timeout import get_default_timeout_function, set_default_timeout_function
+from lp.testing.publication import get_request_and_publication
 from sqlobject import SQLObjectNotFound
 from storm.exceptions import LostObjectError
 from storm.store import Store
@@ -38,14 +46,14 @@ from testtools.matchers import (
     MatchesSetwise,
     MatchesStructure,
     StartsWith,
-    )
+)
 import transaction
 from zope.component import getUtility
 from zope.publisher.xmlrpc import TestRequest
 from zope.security.interfaces import (
     ForbiddenAttribute,
     Unauthorized,
-    )
+)
 from zope.security.proxy import removeSecurityProxy
 
 from lp import _
@@ -53,7 +61,7 @@ from lp.app.enums import (
     InformationType,
     PRIVATE_INFORMATION_TYPES,
     PUBLIC_INFORMATION_TYPES,
-    )
+)
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
@@ -68,7 +76,7 @@ from lp.code.enums import (
     GitRepositoryStatus,
     GitRepositoryType,
     TargetRevisionControlSystems,
-    )
+)
 from lp.code.errors import (
     CannotDeleteGitRepository,
     CannotModifyNonHostedGitRepository,
@@ -77,38 +85,38 @@ from lp.code.errors import (
     GitRepositoryExists,
     GitTargetError,
     NoSuchGitReference,
-    )
+)
 from lp.code.event.git import GitRefsUpdatedEvent
 from lp.code.interfaces.branchmergeproposal import (
     BRANCH_MERGE_PROPOSAL_FINAL_STATES as FINAL_STATES,
-    )
+)
 from lp.code.interfaces.codeimport import ICodeImportSet
 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
 from lp.code.interfaces.gitjob import (
     IGitRefScanJobSource,
     IGitRepositoryModifiedMailJobSource,
-    )
+)
 from lp.code.interfaces.gitlookup import IGitLookup
 from lp.code.interfaces.gitnamespace import (
     IGitNamespacePolicy,
     IGitNamespaceSet,
-    )
+)
 from lp.code.interfaces.gitrepository import (
     IGitRepository,
     IGitRepositorySet,
     IGitRepositoryView,
-    )
+)
 from lp.code.interfaces.gitrule import (
     IGitNascentRule,
     IGitNascentRuleGrant,
-    )
+)
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.model.branchmergeproposal import BranchMergeProposal
 from lp.code.model.branchmergeproposaljob import (
     BranchMergeProposalJob,
     BranchMergeProposalJobType,
     UpdatePreviewDiffJob,
-    )
+)
 from lp.code.model.codereviewcomment import CodeReviewComment
 from lp.code.model.gitactivity import GitActivity
 from lp.code.model.gitjob import (
@@ -116,30 +124,30 @@ from lp.code.model.gitjob import (
     GitJobType,
     GitRefScanJob,
     ReclaimGitRepositorySpaceJob,
-    )
+)
 from lp.code.model.gitrepository import (
     ClearPrerequisiteRepository,
     DeleteCodeImport,
     DeletionCallable,
     DeletionOperation,
     GitRepository,
-    )
+)
 from lp.code.tests.helpers import GitHostingFixture
 from lp.code.xmlrpc.git import GitAPI
 from lp.registry.enums import (
     BranchSharingPolicy,
     PersonVisibility,
     TeamMembershipPolicy,
-    )
+)
 from lp.registry.interfaces.accesspolicy import (
     IAccessArtifactSource,
     IAccessPolicyArtifactSource,
     IAccessPolicySource,
-    )
+)
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.persondistributionsourcepackage import (
     IPersonDistributionSourcePackageFactory,
-    )
+)
 from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
 from lp.registry.interfaces.personproduct import IPersonProductFactory
 from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
@@ -159,7 +167,7 @@ from lp.services.macaroons.interfaces import IMacaroonIssuer
 from lp.services.macaroons.testing import (
     find_caveats_by_name,
     MacaroonTestMixin,
-    )
+)
 from lp.services.mail import stub
 from lp.services.openid.model.openididentifier import OpenIdIdentifier
 from lp.services.propertycache import clear_property_cache
@@ -178,19 +186,19 @@ from lp.testing import (
     record_two_runs,
     StormStatementRecorder,
     TestCaseWithFactory,
-    verifyObject,
-    )
+    verifyObject, logout,
+)
 from lp.testing.dbuser import dbuser
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
     ZopelessDatabaseLayer,
-    )
+)
 from lp.testing.mail_helpers import pop_notifications
 from lp.testing.matchers import (
     DoesNotSnapshot,
     HasQueryCount,
-    )
+)
 from lp.testing.pages import webservice_for_person
 from lp.xmlrpc import faults
 from lp.xmlrpc.interfaces import IPrivateApplication
@@ -212,7 +220,7 @@ class TestGitRepository(TestCaseWithFactory):
             '_api_landing_targets',
             '_api_landing_candidates',
             'dependent_landings',
-            ]
+        ]
         self.assertThat(
             self.factory.makeGitRepository(),
             DoesNotSnapshot(large_properties, IGitRepositoryView))
@@ -850,7 +858,7 @@ class TestGitRepositoryDeletion(TestCaseWithFactory):
         merge_source.addLandingTarget(self.user, self.ref)
         self.assertFalse(
             self.repository.canBeDeleted(),
-             "A repository with a landing candidate is not deletable.")
+            "A repository with a landing candidate is not deletable.")
         self.assertRaises(
             CannotDeleteGitRepository, self.repository.destroySelf)
 
@@ -871,8 +879,8 @@ class TestGitRepositoryDeletion(TestCaseWithFactory):
         # A repository with an associated job will delete those jobs.
         with person_logged_in(self.repository.owner):
             GitAPI(None, None).notify(self.repository.getInternalPath(),
-                                  {'loose_object_count': 5, 'pack_count': 2},
-                                  {'uid': self.repository.owner.id})
+                                      {'loose_object_count': 5, 'pack_count': 2},
+                                      {'uid': self.repository.owner.id})
         store = Store.of(self.repository)
         self.repository.destroySelf()
         # Need to commit the transaction to fire off the constraint checks.
@@ -1020,40 +1028,40 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
         self.assertEqual(
             {
                 merge_proposal1:
-                ("delete",
-                 _("This repository is the source repository of this merge "
-                   "proposal.")),
+                    ("delete",
+                     _("This repository is the source repository of this merge "
+                       "proposal.")),
                 merge_proposal2:
-                ("delete",
-                 _("This repository is the source repository of this merge "
-                   "proposal.")),
-                },
+                    ("delete",
+                     _("This repository is the source repository of this merge "
+                       "proposal.")),
+            },
             self.repository.getDeletionRequirements())
         target = merge_proposal1.target_git_repository
         self.assertEqual(
             {
                 merge_proposal1:
-                ("delete",
-                 _("This repository is the target repository of this merge "
-                   "proposal.")),
+                    ("delete",
+                     _("This repository is the target repository of this merge "
+                       "proposal.")),
                 merge_proposal2:
-                ("delete",
-                 _("This repository is the target repository of this merge "
-                   "proposal.")),
-                },
+                    ("delete",
+                     _("This repository is the target repository of this merge "
+                       "proposal.")),
+            },
             target.getDeletionRequirements())
         prerequisite = merge_proposal1.prerequisite_git_repository
         self.assertEqual(
             {
                 merge_proposal1:
-                ("alter",
-                 _("This repository is the prerequisite repository of this "
-                   "merge proposal.")),
+                    ("alter",
+                     _("This repository is the prerequisite repository of this "
+                       "merge proposal.")),
                 merge_proposal2:
-                ("alter",
-                 _("This repository is the prerequisite repository of this "
-                   "merge proposal.")),
-                },
+                    ("alter",
+                     _("This repository is the prerequisite repository of this "
+                       "merge proposal.")),
+            },
             prerequisite.getDeletionRequirements())
 
     def test_delete_merge_proposal_source(self):
@@ -1075,7 +1083,7 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
         merge_proposal1.target_git_repository.destroySelf(
             break_references=True)
         self.assertRaises(SQLObjectNotFound,
-            BranchMergeProposal.get, merge_proposal1_id)
+                          BranchMergeProposal.get, merge_proposal1_id)
 
     def test_delete_merge_proposal_prerequisite(self):
         # Merge proposal prerequisite repositories can be deleted with
@@ -1145,7 +1153,7 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
         self.factory.makeSnap(git_ref=ref)
         self.assertEqual(
             {None:
-             ("alter", _("Some snap packages build from this repository."))},
+                 ("alter", _("Some snap packages build from this repository."))},
             ref.repository.getDeletionRequirements())
 
     def test_snap_deletion(self):
@@ -1170,7 +1178,7 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
         self.factory.makeCharmRecipe(git_ref=ref)
         self.assertEqual(
             {None:
-             ("alter", _("Some charm recipes build from this repository."))},
+                 ("alter", _("Some charm recipes build from this repository."))},
             ref.repository.getDeletionRequirements())
 
     def test_charm_recipe_deletion(self):
@@ -1261,8 +1269,8 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
             ref.path: {
                 "sha1": "0000000000000000000000000000000000000000",
                 "type": ref.object_type,
-                },
-            }
+            },
+        }
         repository.createOrUpdateRefs(new_refs_info)
         self.assertSqlAttributeEqualsDate(
             repository, "date_last_modified", UTC_NOW)
@@ -1275,8 +1283,8 @@ class TestGitRepositoryModifications(TestCaseWithFactory):
             "refs/heads/new": {
                 "sha1": ref.commit_sha1,
                 "type": ref.object_type,
-                },
-            }
+            },
+        }
         repository.createOrUpdateRefs(new_refs_info)
         self.assertSqlAttributeEqualsDate(
             repository, "date_last_modified", UTC_NOW)
@@ -1593,7 +1601,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
     def test__convertRefInfo_requires_object_type(self):
         info = {
             "object": {"sha1": "0000000000000000000000000000000000000000"},
-            }
+        }
         self.assertRaisesWithContent(
             ValueError, 'ref info object does not contain "type" key',
             GitRepository._convertRefInfo, info)
@@ -1609,8 +1617,8 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             "object": {
                 "sha1": "0000000000000000000000000000000000000000",
                 "type": "nonsense",
-                },
-            }
+            },
+        }
         self.assertRaisesWithContent(
             ValueError, 'ref info type is not a recognised object type',
             GitRepository._convertRefInfo, info)
@@ -1637,8 +1645,8 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             "refs/tags/1.1": {
                 "sha1": master_ref.commit_sha1,
                 "type": master_ref.object_type,
-                },
-            }
+            },
+        }
         repository.createOrUpdateRefs(new_refs_info)
         self.assertRefsMatch(
             [ref for ref in repository.refs if ref.path != "refs/tags/1.1"],
@@ -1650,7 +1658,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 path="refs/tags/1.1",
                 commit_sha1=master_ref.commit_sha1,
                 object_type=master_ref.object_type,
-                ))
+            ))
 
     def test_remove(self):
         repository = self.factory.makeGitRepository()
@@ -1669,7 +1677,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
         new_info = {
             "sha1": "0000000000000000000000000000000000000000",
             "type": GitObjectType.BLOB,
-            }
+        }
         repository.createOrUpdateRefs({"refs/tags/1.0": new_info})
         self.assertRefsMatch(
             [ref for ref in repository.refs if ref.path != "refs/tags/1.0"],
@@ -1681,13 +1689,13 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 path="refs/tags/1.0",
                 commit_sha1="0000000000000000000000000000000000000000",
                 object_type=GitObjectType.BLOB,
-                ))
+            ))
 
     def _getWaitingUpdatePreviewDiffJobs(self, repository):
         jobs = Store.of(repository).find(
             BranchMergeProposalJob,
             BranchMergeProposalJob.job_type ==
-                BranchMergeProposalJobType.UPDATE_PREVIEW_DIFF,
+            BranchMergeProposalJobType.UPDATE_PREVIEW_DIFF,
             BranchMergeProposalJob.job == Job.id,
             Job._status == JobStatus.WAITING)
         return [UpdatePreviewDiffJob(job) for job in jobs]
@@ -1702,7 +1710,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
         new_info = {
             "sha1": "0000000000000000000000000000000000000000",
             "type": GitObjectType.BLOB,
-            }
+        }
         repository.createOrUpdateRefs({ref.path: new_info})
         jobs = self._getWaitingUpdatePreviewDiffJobs(repository)
         self.assertEqual(
@@ -1746,39 +1754,39 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 "object": {
                     "sha1": "1111111111111111111111111111111111111111",
                     "type": "commit",
-                    },
                 },
+            },
             "refs/heads/foo": {
                 "object": {
                     "sha1": foo_sha1,
                     "type": "commit",
-                    },
                 },
+            },
             "refs/tags/1.0": {
                 "object": {
                     "sha1": master_sha1,
                     "type": "commit",
-                    },
                 },
-            }))
+            },
+        }))
         refs_to_upsert, refs_to_remove = repository.planRefChanges("dummy")
 
         expected_upsert = {
             "refs/heads/master": {
                 "sha1": "1111111111111111111111111111111111111111",
                 "type": GitObjectType.COMMIT,
-                },
+            },
             "refs/heads/foo": {
                 "sha1": six.ensure_text(
                     hashlib.sha1(b"refs/heads/foo").hexdigest()),
                 "type": GitObjectType.COMMIT,
-                },
+            },
             "refs/tags/1.0": {
                 "sha1": six.ensure_text(
                     hashlib.sha1(b"refs/heads/master").hexdigest()),
                 "type": GitObjectType.COMMIT,
-                },
-            }
+            },
+        }
         self.assertEqual(expected_upsert, refs_to_upsert)
         self.assertEqual(set(["refs/heads/bar"]), refs_to_remove)
 
@@ -1792,17 +1800,17 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             "refs/heads/blob": {
                 "sha1": blob_sha1,
                 "type": GitObjectType.BLOB,
-                },
-            }
+            },
+        }
         repository.createOrUpdateRefs(refs_info)
         self.useFixture(GitHostingFixture(refs={
             "refs/heads/blob": {
                 "object": {
                     "sha1": blob_sha1,
                     "type": "blob",
-                    },
                 },
-            }))
+            },
+        }))
         self.assertEqual(({}, set()), repository.planRefChanges("dummy"))
 
     def test_planRefChanges_includes_unfetched_commits(self):
@@ -1826,8 +1834,8 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 "object": {
                     "sha1": repository.getRefByPath(path).commit_sha1,
                     "type": "commit",
-                    },
-                }
+                },
+            }
             for path in paths}))
         refs_to_upsert, refs_to_remove = repository.planRefChanges("dummy")
 
@@ -1835,8 +1843,8 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             "refs/heads/foo": {
                 "sha1": repository.getRefByPath("refs/heads/foo").commit_sha1,
                 "type": GitObjectType.COMMIT,
-                },
-            }
+            },
+        }
         self.assertEqual(expected_upsert, refs_to_upsert)
         self.assertEqual(set(), refs_to_remove)
 
@@ -1876,25 +1884,25 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                     "name": author.displayname,
                     "email": author_email,
                     "time": int(seconds_since_epoch(author_date)),
-                    },
+                },
                 "committer": {
                     "name": "New Person",
                     "email": "new-person@xxxxxxxxxxx",
                     "time": int(seconds_since_epoch(committer_date)),
-                    },
+                },
                 "parents": [],
                 "tree": six.ensure_text(hashlib.sha1(b"").hexdigest()),
-                }]))
+            }]))
         refs = {
             "refs/heads/master": {
                 "sha1": master_sha1,
                 "type": GitObjectType.COMMIT,
-                },
+            },
             "refs/heads/foo": {
                 "sha1": foo_sha1,
                 "type": GitObjectType.COMMIT,
-                },
-            }
+            },
+        }
         GitRepository.fetchRefCommits("dummy", refs)
 
         expected_oids = [master_sha1, foo_sha1]
@@ -1917,12 +1925,12 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 "committer_addr": expected_committer_addr,
                 "committer_date": committer_date,
                 "commit_message": "tip of master",
-                },
+            },
             "refs/heads/foo": {
                 "sha1": foo_sha1,
                 "type": GitObjectType.COMMIT,
-                },
-            }
+            },
+        }
         self.assertEqual(expected_refs, refs)
 
     def test_fetchRefCommits_empty(self):
@@ -1943,17 +1951,17 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             "refs/heads/master": {
                 "sha1": "1111111111111111111111111111111111111111",
                 "type": GitObjectType.COMMIT,
-                },
+            },
             "refs/heads/foo": {
                 "sha1": repository.getRefByPath("refs/heads/foo").commit_sha1,
                 "type": GitObjectType.COMMIT,
-                },
+            },
             "refs/tags/1.0": {
                 "sha1": repository.getRefByPath(
                     "refs/heads/master").commit_sha1,
                 "type": GitObjectType.COMMIT,
-                },
-            }
+            },
+        }
         refs_to_remove = set(["refs/heads/bar"])
         repository.synchroniseRefs(refs_to_upsert, refs_to_remove)
         expected_sha1s = [
@@ -1962,14 +1970,14 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
              six.ensure_text(hashlib.sha1(b"refs/heads/foo").hexdigest())),
             ("refs/tags/1.0",
              six.ensure_text(hashlib.sha1(b"refs/heads/master").hexdigest())),
-            ]
+        ]
         matchers = [
             MatchesStructure.byEquality(
                 repository=repository,
                 path=path,
                 commit_sha1=sha1,
                 object_type=GitObjectType.COMMIT,
-                ) for path, sha1 in expected_sha1s]
+            ) for path, sha1 in expected_sha1s]
         self.assertThat(repository.refs, MatchesSetwise(*matchers))
 
     def test_set_default_branch(self):
@@ -1983,7 +1991,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             repository.default_branch = "new"
         self.assertEqual(
             [((repository.getInternalPath(),),
-             {"default_branch": "refs/heads/new"})],
+              {"default_branch": "refs/heads/new"})],
             hosting_fixture.setProperties.calls)
         self.assertEqual("refs/heads/new", repository.default_branch)
 
@@ -2010,7 +2018,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
             self.assertRaisesWithContent(
                 CannotModifyNonHostedGitRepository,
                 "Cannot modify non-hosted Git repository %s." %
-                    repository.display_name,
+                repository.display_name,
                 setattr, repository, "default_branch", "new")
         self.assertEqual([], hosting_fixture.setProperties.calls)
         self.assertEqual("refs/heads/master", repository.default_branch)
@@ -2024,7 +2032,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 NoSuchGitReference,
                 "The repository at %s does not contain "
                 "a reference named 'None'." %
-                    repository.display_name,
+                repository.display_name,
                 setattr, repository, "default_branch", None)
 
     def test_exception_set_default_branch_nonexistent_ref(self):
@@ -2041,7 +2049,7 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
                 NoSuchGitReference,
                 "The repository at %s does not contain "
                 "a reference named 'refs/heads/nonexistent'." %
-                    repository.display_name,
+                repository.display_name,
                 setattr, repository,
                 "default_branch", "refs/heads/nonexistent")
         self.assertEqual("refs/heads/master", repository.default_branch)
@@ -2175,7 +2183,7 @@ class TestGitRepositoryIsPersonTrustedReviewer(TestCaseWithFactory):
 
     def test_repository_owner_not_review_team_member_is_trusted(self):
         # If the owner of the repository is not in the review team,
-        #they are still trusted.
+        # they are still trusted.
         team = self.factory.makeTeam()
         repository = self.factory.makeGitRepository(reviewer=team)
         self.assertFalse(repository.owner.inTeam(team))
@@ -2516,7 +2524,6 @@ class TestGitRepositorySetTarget(TestCaseWithFactory):
 
 
 class TestGitRepositoryRescan(TestCaseWithFactory):
-
     layer = DatabaseFunctionalLayer
 
     def test_rescan(self):
@@ -2570,7 +2577,6 @@ class TestGitRepositoryRescan(TestCaseWithFactory):
 
 
 class TestGitRepositoryUpdateMergeCommitIDs(TestCaseWithFactory):
-
     layer = DatabaseFunctionalLayer
 
     def test_updates_proposals(self):
@@ -2630,7 +2636,6 @@ class TestGitRepositoryUpdateMergeCommitIDs(TestCaseWithFactory):
 
 
 class TestGitRepositoryUpdateLandingTargets(TestCaseWithFactory):
-
     layer = DatabaseFunctionalLayer
 
     def test_schedules_diff_updates(self):
@@ -2661,7 +2666,6 @@ class TestGitRepositoryUpdateLandingTargets(TestCaseWithFactory):
 
 
 class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
-
     layer = ZopelessDatabaseLayer
 
     def test_base_repository_recipe(self):
@@ -2753,7 +2757,6 @@ class TestGitRepositoryMarkRecipesStale(TestCaseWithFactory):
 
 
 class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
-
     layer = ZopelessDatabaseLayer
 
     def test_same_repository(self):
@@ -2797,7 +2800,6 @@ class TestGitRepositoryMarkSnapsStale(TestCaseWithFactory):
 
 
 class TestGitRepositoryFork(TestCaseWithFactory):
-
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
@@ -2899,7 +2901,6 @@ class TestGitRepositoryFork(TestCaseWithFactory):
 
 
 class TestGitRepositoryDetectMerges(TestCaseWithFactory):
-
     layer = ZopelessDatabaseLayer
 
     def test_markProposalMerged(self):
@@ -2939,7 +2940,7 @@ class TestGitRepositoryDetectMerges(TestCaseWithFactory):
                 "refs/heads/target-2",
                 "refs/heads/source-1",
                 "refs/heads/source-2",
-                ])
+            ])
         bmp1 = self.factory.makeBranchMergeProposalForGit(
             target_ref=target_1, source_ref=source_1)
         bmp2 = self.factory.makeBranchMergeProposalForGit(
@@ -2952,12 +2953,12 @@ class TestGitRepositoryDetectMerges(TestCaseWithFactory):
             "refs/heads/target-1": {
                 "sha1": "0" * 40,
                 "type": GitObjectType.COMMIT,
-                },
+            },
             "refs/heads/target-2": {
                 "sha1": "1" * 40,
                 "type": GitObjectType.COMMIT,
-                },
-            }
+            },
+        }
         expected_events = [
             ObjectModifiedEvent, ObjectModifiedEvent, GitRefsUpdatedEvent]
         _, events = self.assertNotifies(
@@ -2967,7 +2968,7 @@ class TestGitRepositoryDetectMerges(TestCaseWithFactory):
              set([source_1.commit_sha1, source_2.commit_sha1])),
             (repository.getInternalPath(), target_2.commit_sha1,
              set([source_1.commit_sha1])),
-            ]
+        ]
         self.assertContentEqual(
             expected_args, hosting_fixture.detectMerges.extract_args())
         self.assertEqual(BranchMergeProposalStatus.MERGED, bmp1.queue_status)
@@ -3006,7 +3007,6 @@ class TestGitRepositoryGetBlob(TestCaseWithFactory):
 
 
 class TestGitRepositoryRules(TestCaseWithFactory):
-
     layer = DatabaseFunctionalLayer
 
     def test_rules(self):
@@ -3025,7 +3025,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
             MatchesStructure.byEquality(
                 repository=repository,
                 ref_pattern="refs/heads/stable/*"),
-            ]))
+        ]))
 
     def test_getRule(self):
         repository = self.factory.makeGitRepository()
@@ -3061,7 +3061,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 repository=repository, ref_pattern="refs/heads/protected/*"),
             self.factory.makeGitRule(
                 repository=repository, ref_pattern="refs/heads/another/*"),
-            ]
+        ]
         self.assertEqual([0, 1, 2], [rule.position for rule in initial_rules])
         with person_logged_in(repository.owner):
             new_rule = repository.addRule(
@@ -3079,7 +3079,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 repository=repository, ref_pattern="refs/heads/exact"),
             self.factory.makeGitRule(
                 repository=repository, ref_pattern="refs/heads/*"),
-            ]
+        ]
         self.assertEqual([0, 1], [rule.position for rule in initial_rules])
         with person_logged_in(repository.owner):
             exact_rule = repository.addRule(
@@ -3129,7 +3129,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
             self.factory.makeGitRuleGrant(
                 rule=rule, grantee=self.factory.makePerson())
             for _ in range(2)
-            ]
+        ]
         self.factory.makeGitRuleGrant(
             rule=other_rule, grantee=self.factory.makePerson())
         self.assertContentEqual(grants, repository.grants)
@@ -3161,12 +3161,12 @@ class TestGitRepositoryRules(TestCaseWithFactory):
             IGitNascentRule({
                 "ref_pattern": "refs/heads/*",
                 "grants": [],
-                }),
+            }),
             IGitNascentRule({
                 "ref_pattern": "refs/heads/stable/*",
                 "grants": [],
-                }),
-            ]
+            }),
+        ]
         removeSecurityProxy(repository)._validateRules(rules)
 
     def test__validateRules_duplicate_ref_pattern(self):
@@ -3175,12 +3175,12 @@ class TestGitRepositoryRules(TestCaseWithFactory):
             IGitNascentRule({
                 "ref_pattern": "refs/heads/*",
                 "grants": [],
-                }),
+            }),
             IGitNascentRule({
                 "ref_pattern": "refs/heads/*",
                 "grants": [],
-                }),
-            ]
+            }),
+        ]
         self.assertRaisesWithContent(
             ValueError,
             "New rules may not contain duplicate ref patterns "
@@ -3201,9 +3201,9 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                             "grantee_type": GitGranteeType.REPOSITORY_OWNER,
                             "can_create": True,
                             "can_force_push": True,
-                            }),
-                        ],
-                    }),
+                        }),
+                    ],
+                }),
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/*",
                     "grants": [
@@ -3211,10 +3211,10 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                             "grantee_type": GitGranteeType.PERSON,
                             "grantee": grantee,
                             "can_push": True,
-                            }),
-                        ],
-                    }),
-                ], member)
+                        }),
+                    ],
+                }),
+            ], member)
         self.assertThat(list(repository.rules), MatchesListwise([
             MatchesStructure(
                 repository=Equals(repository),
@@ -3240,7 +3240,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                         can_create=Is(False),
                         can_push=Is(True),
                         can_force_push=Is(False)))),
-            ]))
+        ]))
 
     def test_setRules_move(self):
         owner = self.factory.makeTeam()
@@ -3258,16 +3258,16 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/*/next",
                     "grants": [],
-                    }),
+                }),
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/stable/*",
                     "grants": [],
-                    }),
+                }),
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/*",
                     "grants": [],
-                    }),
-                ], members[1])
+                }),
+            ], members[1])
             date_modified = get_transaction_timestamp(Store.of(repository))
         self.assertThat(list(repository.rules), MatchesListwise([
             MatchesStructure(
@@ -3288,7 +3288,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 creator=Equals(members[0]),
                 date_created=Equals(date_created),
                 date_last_modified=Equals(date_created)),
-            ]))
+        ]))
 
     def test_setRules_canonicalises_expected_ordering(self):
         repository = self.factory.makeGitRepository()
@@ -3297,12 +3297,12 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/master-next",
                     "grants": [],
-                    }),
+                }),
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/master",
                     "grants": [],
-                    }),
-                ], repository.owner)
+                }),
+            ], repository.owner)
 
     def test_setRules_modify_grants(self):
         owner = self.factory.makeTeam()
@@ -3329,20 +3329,20 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                         IGitNascentRuleGrant({
                             "grantee_type": GitGranteeType.REPOSITORY_OWNER,
                             "can_create": True,
-                            }),
+                        }),
                         IGitNascentRuleGrant({
                             "grantee_type": GitGranteeType.PERSON,
                             "grantee": grantee,
                             "can_push": True,
                             "can_force_push": True,
-                            }),
-                        ],
-                    }),
+                        }),
+                    ],
+                }),
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/*",
                     "grants": [],
-                    }),
-                ], members[1])
+                }),
+            ], members[1])
             date_modified = get_transaction_timestamp(
                 Store.of(repository))
         self.assertThat(list(repository.rules), MatchesListwise([
@@ -3378,7 +3378,7 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 date_created=Equals(date_created),
                 date_last_modified=Equals(date_created),
                 grants=MatchesSetwise()),
-            ]))
+        ]))
 
     def test_setRules_remove(self):
         repository = self.factory.makeGitRepository()
@@ -3393,8 +3393,8 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 IGitNascentRule({
                     "ref_pattern": "refs/heads/*",
                     "grants": [],
-                    }),
-                ], repository.owner)
+                }),
+            ], repository.owner)
         self.assertThat(list(repository.rules), MatchesListwise([
             MatchesStructure(
                 repository=Equals(repository),
@@ -3402,11 +3402,10 @@ class TestGitRepositoryRules(TestCaseWithFactory):
                 date_created=Equals(date_created),
                 date_last_modified=Equals(date_created),
                 grants=MatchesSetwise()),
-            ]))
+        ]))
 
 
 class TestGitRepositorySet(TestCaseWithFactory):
-
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
@@ -3513,7 +3512,7 @@ class TestGitRepositorySet(TestCaseWithFactory):
             datetime(2014, 1, 1, tzinfo=pytz.UTC),
             datetime(2020, 1, 1, tzinfo=pytz.UTC),
             datetime(2019, 1, 1, tzinfo=pytz.UTC),
-            ]
+        ]
         for repository, modified_date in zip(repositories, modified_dates):
             removeSecurityProxy(repository).date_last_modified = modified_date
         removeSecurityProxy(repositories[0]).transitionToInformationType(
@@ -3707,7 +3706,6 @@ class TestGitRepositorySet(TestCaseWithFactory):
 
 
 class TestGitRepositorySetDefaultsMixin:
-
     layer = DatabaseFunctionalLayer
 
     def setUp(self):
@@ -3715,7 +3713,7 @@ class TestGitRepositorySetDefaultsMixin:
         self.repository_set = getUtility(IGitRepositorySet)
         self.get_method = self.repository_set.getDefaultRepository
         self.set_method = (lambda target, repository, user:
-            self.repository_set.setDefaultRepository(target, repository))
+                           self.repository_set.setDefaultRepository(target, repository))
 
     def makeGitRepository(self, target):
         return self.factory.makeGitRepository(target=target)
@@ -3917,7 +3915,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             'pack_count': Is(None),
             'date_last_repacked': Is(None),
             'date_last_scanned': Is(None),
-            }))
+        }))
 
         repository_db = removeSecurityProxy(repository_db)
         repository_db.loose_object_count = 45
@@ -3932,7 +3930,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             'pack_count': Equals(523),
             'date_last_repacked': Equals(UTC_NOW),
             'date_last_scanned': Equals(UTC_NOW),
-            }))
+        }))
 
     def test_git_gc_owner(self):
         # Repository owner cannot request a git GC run
@@ -4021,7 +4019,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             "name": Equals(name),
             "owner_default": Is(False),
             "target_default": Is(False),
-            }))
+        }))
         self.assertEqual(1, hosting_fixture.create.call_count)
 
     def test_new_project(self):
@@ -4200,6 +4198,44 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             self.assertEqual(
                 InformationType.PUBLIC, repository_db.information_type)
 
+    def test_newRevisionStatusReport(self):
+        repository = self.factory.makeGitRepository()
+        requester = repository.owner
+        with person_logged_in(requester):
+            repository_url = api_url(repository)
+        webservice = webservice_for_person(
+            requester, permission=OAuthPermission.WRITE_PUBLIC,
+            default_api_version="devel")
+        response = webservice.named_post(
+            repository_url, "issueAccessToken", description="Test token",
+            scopes=["repository:build_status"])
+        self.assertEqual(200, response.status)
+        secret = response.jsonBody()
+        with person_logged_in(requester):
+            token = getUtility(IAccessTokenSet).getBySecret(secret)
+            self.assertThat(token, MatchesStructure(
+                owner=Equals(requester),
+                description=Equals("Test token"),
+                target=Equals(repository),
+                scopes=Equals([AccessTokenScope.REPOSITORY_BUILD_STATUS]),
+                date_expires=Is(None)))
+        # Use the token to create a new Status Report
+        with person_logged_in(requester):
+            repository_url = api_url(repository)
+        webservice = webservice_for_person(
+            requester, permission=OAuthPermission.WRITE_PRIVATE,
+            default_api_version="devel")
+
+        # header = "Authorization: Token %s" % token
+        header = {'Authorization-type': 'Token %s' % token}
+        response = webservice.named_post(
+            repository_url, "newRevisionStatusReport",
+            headers=header, name="CI", status="Queued",
+            description="120/120 tests passed",
+            commit_sha1='823748ur9804376', result='Success')
+
+        self.assertEqual(200, response.status)
+
     def test_set_target(self):
         # The repository owner can move the repository to another target;
         # this redirects to the new location.
@@ -4294,7 +4330,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                 ("b", ref_urls[1]),
                 ("refs/heads/b", ref_urls[1]),
                 ("HEAD", "%s/+ref/HEAD" % repository_url),
-                ):
+        ):
             response = webservice.named_get(
                 repository_url, "getRefByPath", path=path)
             self.assertEqual(200, response.status)
@@ -4394,7 +4430,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                         BranchSubscriptionNotificationLevel.NOEMAIL),
                     max_diff_lines=BranchSubscriptionDiffSize.WHOLEDIFF,
                     review_level=CodeReviewNotificationLevel.STATUS,
-                    ))
+                ))
         repository = webservice.get(repository_url).jsonBody()
         subscribers = webservice.get(
             repository["subscribers_collection_link"]).jsonBody()
@@ -4525,7 +4561,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                 repository=repository, ref_pattern="refs/heads/stable/*"),
             self.factory.makeGitRule(
                 repository=repository, ref_pattern="refs/heads/*"),
-            ]
+        ]
         self.factory.makeGitRuleGrant(
             rule=rules[0], grantee=GitGranteeType.REPOSITORY_OWNER,
             can_create=True, can_force_push=True)
@@ -4550,9 +4586,9 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                         "can_create": Is(True),
                         "can_push": Is(False),
                         "can_force_push": Is(True),
-                        }),
-                    ),
-                }),
+                    }),
+                ),
+            }),
             MatchesDict({
                 "ref_pattern": Equals("refs/heads/*"),
                 "grants": MatchesSetwise(*(
@@ -4563,10 +4599,10 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                         "can_create": Is(False),
                         "can_push": Is(True),
                         "can_force_push": Is(False),
-                        })
+                    })
                     for grantee_url in grantee_urls)),
-                }),
-            ]))
+            }),
+        ]))
 
     def test_setRules(self):
         repository = self.factory.makeGitRepository()
@@ -4588,9 +4624,9 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                             "grantee_type": "Repository owner",
                             "can_create": True,
                             "can_force_push": True,
-                            },
-                        ],
-                    },
+                        },
+                    ],
+                },
                 {
                     "ref_pattern": "refs/heads/*",
                     "grants": [
@@ -4598,10 +4634,10 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                             "grantee_type": "Person",
                             "grantee_link": grantee_url,
                             "can_push": True,
-                            },
-                        ],
-                    },
-                ])
+                        },
+                    ],
+                },
+            ])
         self.assertEqual(200, response.status)
         with person_logged_in(owner):
             self.assertThat(list(repository.rules), MatchesListwise([
@@ -4630,7 +4666,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                             can_create=Is(False),
                             can_push=Is(True),
                             can_force_push=Is(False)))),
-                ]))
+            ]))
 
     def test_checkRefPermissions(self):
         repository = self.factory.makeGitRepository()
@@ -4659,7 +4695,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             "refs/heads/master": Equals(["create", "push"]),
             "refs/heads/next": Equals(["create", "push"]),
             "refs/other": Equals(["create", "push", "force-push"]),
-            }))
+        }))
         response = webservice.named_get(
             repository_url, "checkRefPermissions", person=grantee_urls[0],
             paths=["refs/heads/master", "refs/heads/next", "refs/other"])
@@ -4667,7 +4703,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             "refs/heads/master": Equals(["create"]),
             "refs/heads/next": Equals([]),
             "refs/other": Equals([]),
-            }))
+        }))
         response = webservice.named_get(
             repository_url, "checkRefPermissions", person=grantee_urls[1],
             paths=["refs/heads/master", "refs/heads/next", "refs/other"])
@@ -4675,7 +4711,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
             "refs/heads/master": Equals(["push"]),
             "refs/heads/next": Equals(["push", "force-push"]),
             "refs/other": Equals([]),
-            }))
+        }))
 
     def test_issueAccessToken(self):
         # A user can request an access token via the webservice API.
@@ -4704,7 +4740,7 @@ class TestGitRepositoryWebservice(TestCaseWithFactory):
                         caveat_id=StartsWith(
                             "lp.principal.openid-identifier ")),
                     MatchesStructure(caveat_id=StartsWith("lp.expires ")),
-                    ])))
+                ])))
 
     def test_issueAccessToken_anonymous(self):
         # An anonymous user cannot request an access token via the
@@ -4823,11 +4859,11 @@ class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
                     caveat_id="lp.git-repository %s" % repository.id),
                 MatchesStructure.byEquality(
                     caveat_id=(
-                        "lp.principal.openid-identifier %s" % identifier)),
+                            "lp.principal.openid-identifier %s" % identifier)),
                 MatchesStructure.byEquality(
                     caveat_id="lp.expires %s" % (
                         expires.strftime("%Y-%m-%dT%H:%M:%S.%f"))),
-                ])))
+            ])))
 
     def test_issueMacaroon_expiry_feature_flag(self):
         self.useFixture(FeatureFixture(

Follow ups