launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22931
[Merge] lp:~cjwatson/launchpad/git-permissions-webservice-repository into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-permissions-webservice-repository into lp:launchpad with lp:~cjwatson/launchpad/git-permissions-webservice-ref as a prerequisite.
Commit message:
Allow getting and setting rules and grants for a Git repository over the webservice.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1517559 in Launchpad itself: "git fine-grained permissions"
https://bugs.launchpad.net/launchpad/+bug/1517559
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-permissions-webservice-repository/+merge/355610
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-permissions-webservice-repository into lp:launchpad.
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py 2018-09-25 16:51:42 +0000
+++ lib/lp/_schema_circular_imports.py 2018-09-25 16:51:42 +0000
@@ -68,7 +68,10 @@
from lp.code.interfaces.diff import IPreviewDiff
from lp.code.interfaces.gitref import IGitRef
from lp.code.interfaces.gitrepository import IGitRepository
-from lp.code.interfaces.gitrule import IGitNascentRuleGrant
+from lp.code.interfaces.gitrule import (
+ IGitNascentRule,
+ IGitNascentRuleGrant,
+ )
from lp.code.interfaces.gitsubscription import IGitSubscription
from lp.code.interfaces.hasbranches import (
IHasBranches,
@@ -527,6 +530,8 @@
IGitRepository, 'dependent_landings', IBranchMergeProposal)
patch_collection_return_type(
IGitRepository, 'getMergeProposals', IBranchMergeProposal)
+patch_list_parameter_type(
+ IGitRepository, 'setRules', 'rules', InlineObject(schema=IGitNascentRule))
# ILiveFSFile
patch_reference_property(ILiveFSFile, 'livefsbuild', ILiveFSBuild)
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2018-09-25 16:51:42 +0000
+++ lib/lp/code/configure.zcml 2018-09-25 16:51:42 +0000
@@ -942,6 +942,7 @@
permission="launchpad.Edit"
interface="lp.code.interfaces.gitrule.IGitRuleEdit"
set_schema="lp.code.interfaces.gitrule.IGitRuleEditableAttributes" />
+ <allow interface="lazr.restful.interfaces.IJSONPublishable" />
</class>
<subscriber
for="lp.code.interfaces.gitrule.IGitRule zope.lifecycleevent.interfaces.IObjectModifiedEvent"
@@ -960,9 +961,13 @@
<subscriber
for="lp.code.interfaces.gitrule.IGitRuleGrant zope.lifecycleevent.interfaces.IObjectModifiedEvent"
handler="lp.code.model.gitrule.git_rule_grant_modified"/>
+ <class class="lp.code.model.gitrule.GitNascentRule">
+ <allow interface="lp.code.interfaces.gitrule.IGitNascentRule" />
+ </class>
<class class="lp.code.model.gitrule.GitNascentRuleGrant">
<allow interface="lp.code.interfaces.gitrule.IGitNascentRuleGrant" />
</class>
+ <adapter factory="lp.code.model.gitrule.nascent_rule_from_dict" />
<adapter factory="lp.code.model.gitrule.nascent_rule_grant_from_dict" />
<!-- GitActivity -->
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2018-09-25 16:51:42 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2018-09-25 16:51:42 +0000
@@ -78,6 +78,7 @@
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.role import IPersonRoles
from lp.services.fields import (
+ InlineObject,
PersonChoice,
PublicPersonChoice,
)
@@ -749,6 +750,23 @@
@export_read_operation()
@operation_for_version("devel")
+ def getRules():
+ """Get the access rules for this repository."""
+
+ @operation_parameters(
+ rules=List(
+ title=_("Rules"),
+ # Really IGitNascentRule, patched in
+ # _schema_circular_imports.py.
+ value_type=InlineObject(schema=Interface)))
+ @call_with(user=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def setRules(rules, user):
+ """Set the access rules for this repository."""
+
+ @export_read_operation()
+ @operation_for_version("devel")
def canBeDeleted():
"""Can this repository be deleted in its current state?
=== modified file 'lib/lp/code/interfaces/gitrule.py'
--- lib/lp/code/interfaces/gitrule.py 2018-09-25 16:51:42 +0000
+++ lib/lp/code/interfaces/gitrule.py 2018-09-25 16:51:42 +0000
@@ -7,6 +7,7 @@
__metaclass__ = type
__all__ = [
+ 'IGitNascentRule',
'IGitNascentRuleGrant',
'IGitRule',
'IGitRuleGrant',
@@ -23,6 +24,7 @@
Choice,
Datetime,
Int,
+ List,
TextLine,
)
@@ -30,6 +32,7 @@
from lp.code.enums import GitGranteeType
from lp.code.interfaces.gitrepository import IGitRepository
from lp.services.fields import (
+ InlineObject,
PersonChoice,
PublicPersonChoice,
)
@@ -209,3 +212,15 @@
can_force_push = copy_field(
IGitRuleGrant["can_force_push"], required=False, default=False)
+
+
+class IGitNascentRule(Interface):
+ """An access rule in the process of being created.
+
+ This represents parameters for a rule that have been deserialised from a
+ webservice request, but that have not yet been attached to a repository.
+ """
+
+ ref_pattern = copy_field(IGitRule["ref_pattern"])
+
+ grants = List(value_type=InlineObject(schema=IGitNascentRuleGrant))
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2018-09-25 16:51:42 +0000
+++ lib/lp/code/model/gitrepository.py 2018-09-25 16:51:42 +0000
@@ -9,6 +9,7 @@
'parse_git_commits',
]
+from collections import OrderedDict
from datetime import datetime
import email
from functools import partial
@@ -20,6 +21,8 @@
from urllib import quote_plus
from bzrlib import urlutils
+from lazr.lifecycle.event import ObjectModifiedEvent
+from lazr.lifecycle.snapshot import Snapshot
import pytz
from storm.databases.postgres import Returning
from storm.expr import (
@@ -47,7 +50,10 @@
from storm.store import Store
from zope.component import getUtility
from zope.event import notify
-from zope.interface import implementer
+from zope.interface import (
+ implementer,
+ providedBy,
+ )
from zope.security.interfaces import Unauthorized
from zope.security.proxy import (
ProxyFactory,
@@ -1192,6 +1198,64 @@
return Store.of(self).find(
GitRuleGrant, GitRuleGrant.repository_id == self.id)
+ def getRules(self):
+ """See `IGitRepository`."""
+ rules = list(self.rules)
+ GitRule.preloadGrantsForRules(rules)
+ return rules
+
+ @staticmethod
+ def _validateRules(rules):
+ """Validate a new iterable of access rules."""
+ patterns = set()
+ for rule in rules:
+ if rule.ref_pattern in patterns:
+ raise ValueError(
+ "New rules may not contain duplicate ref patterns "
+ "(e.g. %s)" % rule.ref_pattern)
+ patterns.add(rule.ref_pattern)
+
+ def setRules(self, rules, user):
+ """See `IGitRepository`."""
+ self._validateRules(rules)
+ existing_rules = {rule.ref_pattern: rule for rule in self.rules}
+ new_rules = OrderedDict((rule.ref_pattern, rule) for rule in rules)
+ GitRule.preloadGrantsForRules(existing_rules.values())
+
+ # Remove old rules first so that we don't generate unnecessary move
+ # events.
+ for ref_pattern, rule in existing_rules.items():
+ if ref_pattern not in new_rules:
+ rule.destroySelf(user)
+
+ # Do our best to match up the new rules and grants to any existing
+ # ones, in order to preserve creator and date-created information.
+ # XXX cjwatson 2018-09-11: We could optimise this to create the new
+ # rules in bulk, but it probably isn't worth the extra complexity.
+ for position, (ref_pattern, new_rule) in enumerate(new_rules.items()):
+ rule = existing_rules.get(ref_pattern)
+ if rule is None:
+ rule = self.addRule(
+ new_rule.ref_pattern, user, position=position)
+ else:
+ rule_before_modification = Snapshot(
+ rule, providing=providedBy(rule))
+ self.moveRule(rule, position, user)
+ if rule.position != rule_before_modification.position:
+ notify(ObjectModifiedEvent(
+ rule, rule_before_modification, ["position"]))
+ rule.setGrants(new_rule.grants, user)
+
+ # The individual moves and adds above should have resulted in
+ # correct rule ordering, but check this.
+ requested_rule_order = list(new_rules)
+ observed_rule_order = [rule.ref_pattern for rule in self.rules]
+ if requested_rule_order != observed_rule_order:
+ raise AssertionError(
+ "setRules failed to establish requested rule order %s "
+ "(got %s instead)" %
+ (requested_rule_order, observed_rule_order))
+
def getActivity(self, changed_after=None):
"""See `IGitRepository`."""
clauses = [GitActivity.repository_id == self.id]
=== modified file 'lib/lp/code/model/gitrule.py'
--- lib/lp/code/model/gitrule.py 2018-09-25 16:51:42 +0000
+++ lib/lp/code/model/gitrule.py 2018-09-25 16:51:42 +0000
@@ -11,7 +11,10 @@
'GitRuleGrant',
]
-from collections import OrderedDict
+from collections import (
+ defaultdict,
+ OrderedDict,
+ )
import re
from lazr.enum import DBItem
@@ -42,6 +45,7 @@
from lp.code.enums import GitGranteeType
from lp.code.interfaces.gitactivity import IGitActivitySet
from lp.code.interfaces.gitrule import (
+ IGitNascentRule,
IGitNascentRuleGrant,
IGitRule,
IGitRuleGrant,
@@ -51,12 +55,21 @@
validate_person,
validate_public_person,
)
+from lp.registry.model.person import Person
+from lp.services.database.bulk import (
+ load_referencing,
+ load_related,
+ )
from lp.services.database.constants import (
DEFAULT,
UTC_NOW,
)
from lp.services.database.enumcol import DBEnum
from lp.services.database.stormbase import StormBase
+from lp.services.propertycache import (
+ cachedproperty,
+ get_property_cache,
+ )
from lp.services.webapp.publisher import canonical_url
@@ -73,7 +86,7 @@
removeSecurityProxy(rule).date_last_modified = UTC_NOW
-@implementer(IGitRule)
+@implementer(IGitRule, IJSONPublishable)
class GitRule(StormBase):
"""See `IGitRule`."""
@@ -106,6 +119,7 @@
self.creator = creator
self.date_created = date_created
self.date_last_modified = date_created
+ get_property_cache(self).grants = []
def __repr__(self):
return "<GitRule '%s'> for %r" % (self.ref_pattern, self.repository)
@@ -118,11 +132,20 @@
# must be an exact-match rule.
return re.search(r"[*?[]", self.ref_pattern) is None
- @property
+ def toDataForJSON(self, media_type):
+ """See `IJSONPublishable`."""
+ if media_type != "application/json":
+ raise ValueError("Unhandled media type %s" % media_type)
+ return {
+ "ref_pattern": self.ref_pattern,
+ "grants": self.grants,
+ }
+
+ @cachedproperty
def grants(self):
"""See `IGitRule`."""
- return Store.of(self).find(
- GitRuleGrant, GitRuleGrant.rule_id == self.id)
+ return list(Store.of(self).find(
+ GitRuleGrant, GitRuleGrant.rule_id == self.id))
def addGrant(self, grantee, grantor, can_create=False, can_push=False,
can_force_push=False):
@@ -132,6 +155,7 @@
can_push=can_push, can_force_push=can_force_push, grantor=grantor,
date_created=DEFAULT)
getUtility(IGitActivitySet).logGrantAdded(grant, grantor)
+ del get_property_cache(self).grants
return grant
def _validateGrants(self, grants):
@@ -182,6 +206,17 @@
notify(ObjectModifiedEvent(
grant, grant_before_modification, edited_fields))
+ @staticmethod
+ def preloadGrantsForRules(rules):
+ """Preload the access grants related to an iterable of rules."""
+ grants = load_referencing(GitRuleGrant, rules, ["rule_id"])
+ grants_map = defaultdict(list)
+ for grant in grants:
+ grants_map[grant.rule_id].append(grant)
+ for rule in rules:
+ get_property_cache(rule).grants = grants_map[rule.id]
+ load_related(Person, grants, ["grantee_id"])
+
def destroySelf(self, user):
"""See `IGitRule`."""
getUtility(IGitActivitySet).logRuleRemoved(self, user)
@@ -297,9 +332,24 @@
"""See `IGitRuleGrant`."""
if user is not None:
getUtility(IGitActivitySet).logGrantRemoved(self, user)
+ del get_property_cache(self.rule).grants
Store.of(self).remove(self)
+@implementer(IGitNascentRule)
+class GitNascentRule:
+
+ def __init__(self, ref_pattern, grants):
+ self.ref_pattern = ref_pattern
+ self.grants = grants
+
+
+@adapter(dict)
+@implementer(IGitNascentRule)
+def nascent_rule_from_dict(template):
+ return GitNascentRule(**template)
+
+
@implementer(IGitNascentRuleGrant)
class GitNascentRuleGrant:
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2018-09-25 16:51:42 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2018-09-25 16:51:42 +0000
@@ -22,7 +22,10 @@
from storm.store import Store
from testtools.matchers import (
EndsWith,
+ Equals,
+ Is,
LessThan,
+ MatchesDict,
MatchesListwise,
MatchesSetwise,
MatchesStructure,
@@ -47,6 +50,7 @@
BranchSubscriptionDiffSize,
BranchSubscriptionNotificationLevel,
CodeReviewNotificationLevel,
+ GitGranteeType,
GitObjectType,
GitRepositoryType,
TargetRevisionControlSystems,
@@ -79,6 +83,10 @@
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 (
@@ -121,6 +129,7 @@
from lp.services.config import config
from lp.services.database.constants import UTC_NOW
from lp.services.database.interfaces import IStore
+from lp.services.database.sqlbase import get_transaction_timestamp
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.model.job import Job
from lp.services.job.runner import JobRunner
@@ -2315,6 +2324,262 @@
rule=other_rule, grantee=self.factory.makePerson())
self.assertContentEqual(grants, repository.grants)
+ def test_getRules_query_count(self):
+ repository = self.factory.makeGitRepository()
+ owner = repository.owner
+
+ def create_rule_and_grants():
+ with person_logged_in(owner):
+ rule = self.factory.makeGitRule(
+ repository=repository,
+ ref_pattern=self.factory.getUniqueUnicode(
+ prefix="refs/heads/"))
+ for i in range(2):
+ self.factory.makeGitRuleGrant(rule=rule)
+
+ def get_rules():
+ with person_logged_in(owner):
+ return repository.getRules()
+
+ recorder1, recorder2 = record_two_runs(
+ get_rules, create_rule_and_grants, 2)
+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
+
+ def test__validateRules_ok(self):
+ repository = self.factory.makeGitRepository()
+ rules = [
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*",
+ "grants": [],
+ }),
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/stable/*",
+ "grants": [],
+ }),
+ ]
+ removeSecurityProxy(repository)._validateRules(rules)
+
+ def test__validateRules_duplicate_ref_pattern(self):
+ repository = self.factory.makeGitRepository()
+ rules = [
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*",
+ "grants": [],
+ }),
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*",
+ "grants": [],
+ }),
+ ]
+ self.assertRaisesWithContent(
+ ValueError,
+ "New rules may not contain duplicate ref patterns "
+ "(e.g. refs/heads/*)",
+ removeSecurityProxy(repository)._validateRules, rules)
+
+ def test_setRules_add(self):
+ owner = self.factory.makeTeam()
+ member = self.factory.makePerson(member_of=[owner])
+ repository = self.factory.makeGitRepository(owner=owner)
+ grantee = self.factory.makePerson()
+ with person_logged_in(member):
+ repository.setRules([
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/stable/*",
+ "grants": [
+ IGitNascentRuleGrant({
+ "grantee_type": GitGranteeType.REPOSITORY_OWNER,
+ "can_create": True,
+ "can_force_push": True,
+ }),
+ ],
+ }),
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*",
+ "grants": [
+ IGitNascentRuleGrant({
+ "grantee_type": GitGranteeType.PERSON,
+ "grantee": grantee,
+ "can_push": True,
+ }),
+ ],
+ }),
+ ], member)
+ self.assertThat(list(repository.rules), MatchesListwise([
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/stable/*"),
+ creator=Equals(member),
+ grants=MatchesSetwise(
+ MatchesStructure(
+ grantor=Equals(member),
+ grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
+ grantee=Is(None),
+ can_create=Is(True),
+ can_push=Is(False),
+ can_force_push=Is(True)))),
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/*"),
+ creator=Equals(member),
+ grants=MatchesSetwise(
+ MatchesStructure(
+ grantor=Equals(member),
+ grantee_type=Equals(GitGranteeType.PERSON),
+ grantee=Equals(grantee),
+ can_create=Is(False),
+ can_push=Is(True),
+ can_force_push=Is(False)))),
+ ]))
+
+ def test_setRules_move(self):
+ owner = self.factory.makeTeam()
+ members = [
+ self.factory.makePerson(member_of=[owner]) for _ in range(2)]
+ repository = self.factory.makeGitRepository(owner=owner)
+ for ref_pattern in (
+ "refs/heads/stable/*", "refs/heads/*/next", "refs/heads/*"):
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern=ref_pattern,
+ creator=members[0])
+ date_created = get_transaction_timestamp(Store.of(repository))
+ with person_logged_in(members[1]):
+ repository.setRules([
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*/next",
+ "grants": [],
+ }),
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/stable/*",
+ "grants": [],
+ }),
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*",
+ "grants": [],
+ }),
+ ], members[1])
+ date_modified = get_transaction_timestamp(Store.of(repository))
+ self.assertThat(list(repository.rules), MatchesListwise([
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/*/next"),
+ creator=Equals(members[0]),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_modified)),
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/stable/*"),
+ creator=Equals(members[0]),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_created)),
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/*"),
+ creator=Equals(members[0]),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_created)),
+ ]))
+
+ def test_setRules_modify_grants(self):
+ owner = self.factory.makeTeam()
+ members = [
+ self.factory.makePerson(member_of=[owner]) for _ in range(2)]
+ repository = self.factory.makeGitRepository(owner=owner)
+ stable_rule = self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/stable/*",
+ creator=members[0])
+ grantee = self.factory.makePerson()
+ self.factory.makeGitRuleGrant(
+ rule=stable_rule, grantee=grantee, grantor=members[0],
+ can_push=True)
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*",
+ creator=members[0])
+ date_created = get_transaction_timestamp(Store.of(repository))
+ transaction.commit()
+ with person_logged_in(members[1]):
+ repository.setRules([
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/stable/*",
+ "grants": [
+ 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])
+ date_modified = get_transaction_timestamp(
+ Store.of(repository))
+ self.assertThat(list(repository.rules), MatchesListwise([
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/stable/*"),
+ creator=Equals(members[0]),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_created),
+ grants=MatchesSetwise(
+ MatchesStructure(
+ grantor=Equals(members[1]),
+ grantee_type=Equals(GitGranteeType.REPOSITORY_OWNER),
+ grantee=Is(None),
+ can_create=Is(True),
+ can_push=Is(False),
+ can_force_push=Is(False),
+ date_created=Equals(date_modified),
+ date_last_modified=Equals(date_modified)),
+ MatchesStructure(
+ grantor=Equals(members[0]),
+ grantee_type=Equals(GitGranteeType.PERSON),
+ grantee=Equals(grantee),
+ can_create=Is(False),
+ can_push=Is(True),
+ can_force_push=Is(True),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_modified)))),
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/*"),
+ creator=Equals(members[0]),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_created),
+ grants=MatchesSetwise()),
+ ]))
+
+ def test_setRules_remove(self):
+ repository = self.factory.makeGitRepository()
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/stable/*")
+ self.factory.makeGitRule(
+ repository=repository, ref_pattern="refs/heads/*")
+ date_created = get_transaction_timestamp(Store.of(repository))
+ transaction.commit()
+ with person_logged_in(repository.owner):
+ repository.setRules([
+ IGitNascentRule({
+ "ref_pattern": "refs/heads/*",
+ "grants": [],
+ }),
+ ], repository.owner)
+ self.assertThat(list(repository.rules), MatchesListwise([
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/*"),
+ date_created=Equals(date_created),
+ date_last_modified=Equals(date_created),
+ grants=MatchesSetwise()),
+ ]))
+
class TestGitRepositorySet(TestCaseWithFactory):
@@ -3088,3 +3353,117 @@
self.assertEqual(1, len(dependent_landings["entries"]))
self.assertThat(
dependent_landings["entries"][0]["self_link"], EndsWith(bmp_url))
+
+ def test_getRules(self):
+ repository = self.factory.makeGitRepository()
+ rules = [
+ self.factory.makeGitRule(
+ 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)
+ grantees = [self.factory.makePerson() for _ in range(2)]
+ for grantee in grantees:
+ self.factory.makeGitRuleGrant(
+ rule=rules[1], grantee=grantee, can_push=True)
+ with person_logged_in(repository.owner):
+ repository_url = api_url(repository)
+ grantee_urls = [api_url(grantee) for grantee in grantees]
+ webservice = webservice_for_person(
+ repository.owner, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_get(repository_url, "getRules")
+ self.assertThat(json.loads(response.body), MatchesListwise([
+ MatchesDict({
+ "ref_pattern": Equals("refs/heads/stable/*"),
+ "grants": MatchesSetwise(
+ MatchesDict({
+ "grantee_type": Equals("Repository owner"),
+ "grantee": Is(None),
+ "can_create": Is(True),
+ "can_push": Is(False),
+ "can_force_push": Is(True),
+ }),
+ ),
+ }),
+ MatchesDict({
+ "ref_pattern": Equals("refs/heads/*"),
+ "grants": MatchesSetwise(*(
+ MatchesDict({
+ "grantee_type": Equals("Person"),
+ "grantee": Equals(
+ webservice.getAbsoluteUrl(grantee_url)),
+ "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()
+ owner = repository.owner
+ grantee = self.factory.makePerson()
+ with person_logged_in(owner):
+ repository_url = api_url(repository)
+ grantee_url = api_url(grantee)
+ webservice = webservice_for_person(
+ owner, permission=OAuthPermission.WRITE_PUBLIC)
+ webservice.default_api_version = "devel"
+ response = webservice.named_post(
+ repository_url, "setRules",
+ rules=[
+ {
+ "ref_pattern": "refs/heads/stable/*",
+ "grants": [
+ {
+ "grantee_type": "Repository owner",
+ "can_create": True,
+ "can_force_push": True,
+ },
+ ],
+ },
+ {
+ "ref_pattern": "refs/heads/*",
+ "grants": [
+ {
+ "grantee_type": "Person",
+ "grantee": grantee_url,
+ "can_push": True,
+ },
+ ],
+ },
+ ])
+ self.assertEqual(200, response.status)
+ with person_logged_in(owner):
+ self.assertThat(list(repository.rules), MatchesListwise([
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/stable/*"),
+ creator=Equals(owner),
+ grants=MatchesSetwise(
+ MatchesStructure(
+ grantor=Equals(owner),
+ grantee_type=Equals(
+ GitGranteeType.REPOSITORY_OWNER),
+ grantee=Is(None),
+ can_create=Is(True),
+ can_push=Is(False),
+ can_force_push=Is(True)))),
+ MatchesStructure(
+ repository=Equals(repository),
+ ref_pattern=Equals("refs/heads/*"),
+ creator=Equals(owner),
+ grants=MatchesSetwise(
+ MatchesStructure(
+ grantor=Equals(owner),
+ grantee_type=Equals(GitGranteeType.PERSON),
+ grantee=Equals(grantee),
+ can_create=Is(False),
+ can_push=Is(True),
+ can_force_push=Is(False)))),
+ ]))
=== modified file 'lib/lp/registry/tests/test_personmerge.py'
--- lib/lp/registry/tests/test_personmerge.py 2018-09-25 16:51:42 +0000
+++ lib/lp/registry/tests/test_personmerge.py 2018-09-25 16:51:42 +0000
@@ -7,7 +7,10 @@
from operator import attrgetter
import pytz
-from testtools.matchers import MatchesStructure
+from testtools.matchers import (
+ MatchesListwise,
+ MatchesStructure,
+ )
import transaction
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -501,10 +504,12 @@
grant = self.factory.makeGitRuleGrant(rule=rule)
self._do_premerge(grant.grantee, person)
- self.assertEqual(grant.grantee, rule.grants.one().grantee)
+ self.assertEqual(1, len(rule.grants))
+ self.assertEqual(grant.grantee, rule.grants[0].grantee)
with person_logged_in(person):
self._do_merge(grant.grantee, person)
- self.assertEqual(person, rule.grants.one().grantee)
+ self.assertEqual(1, len(rule.grants))
+ self.assertEqual(person, rule.grants[0].grantee)
def test_merge_gitrulegrants_conflicts(self):
# Conflicting GitRuleGrants are deleted.
@@ -522,12 +527,11 @@
self._do_merge(duplicate, person)
# Only one grant for the rule exists: the retained person's.
- self.assertThat(
- rule.grants.one(),
+ self.assertThat(rule.grants, MatchesListwise([
MatchesStructure.byEquality(
rule=rule,
grantee=person,
- date_created=person_grant_date))
+ date_created=person_grant_date)]))
def test_mergeAsync(self):
# mergeAsync() creates a new `PersonMergeJob`.
Follow ups