Merge lp:~cjwatson/launchpad/git-fix-rule-ordering into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18878
Proposed branch: lp:~cjwatson/launchpad/git-fix-rule-ordering
Merge into: lp:launchpad
Diff against target: 295 lines (+89/-48)
5 files modified
lib/lp/code/interfaces/gitrule.py (+14/-7)
lib/lp/code/model/gitrepository.py (+19/-9)
lib/lp/code/model/gitrule.py (+36/-27)
lib/lp/code/model/tests/test_gitrepository.py (+15/-1)
lib/lp/code/model/tests/test_gitrule.py (+5/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-fix-rule-ordering
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+362978@code.launchpad.net

Commit message

Canonicalise expected rule ordering in GitRepository.setRules.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/interfaces/gitrule.py'
2--- lib/lp/code/interfaces/gitrule.py 2018-10-31 14:56:02 +0000
3+++ lib/lp/code/interfaces/gitrule.py 2019-02-11 12:57:37 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2018 Canonical Ltd. This software is licensed under the
6+# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Git repository access rules."""
10@@ -12,6 +12,7 @@
11 'IGitNascentRuleGrant',
12 'IGitRule',
13 'IGitRuleGrant',
14+ 'is_rule_exact',
15 ]
16
17 from lazr.restful.fields import Reference
18@@ -43,6 +44,18 @@
19 )
20
21
22+def is_rule_exact(rule):
23+ """Is this an exact-match rule?
24+
25+ :param rule: An `IGitRule` or `IGitNascentRule`.
26+ :return: True if the rule is for an exact reference name, or False if it
27+ is for a wildcard.
28+ """
29+ # turnip's glob_to_re only treats * as special, so any rule whose
30+ # pattern does not contain * must be an exact-match rule.
31+ return "*" not in rule.ref_pattern
32+
33+
34 class IGitRuleView(Interface):
35 """`IGitRule` attributes that require launchpad.View."""
36
37@@ -71,12 +84,6 @@
38 title=_("Date last modified"), required=True, readonly=True,
39 description=_("The time when this rule was last modified."))
40
41- is_exact = Bool(
42- title=_("Is this an exact-match rule?"), required=True, readonly=True,
43- description=_(
44- "True if this rule is for an exact reference name, or False if "
45- "it is for a wildcard."))
46-
47 grants = Attribute("The access grants for this rule.")
48
49
50
51=== modified file 'lib/lp/code/model/gitrepository.py'
52--- lib/lp/code/model/gitrepository.py 2019-02-01 14:08:34 +0000
53+++ lib/lp/code/model/gitrepository.py 2019-02-11 12:57:37 +0000
54@@ -1,4 +1,4 @@
55-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
56+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
57 # GNU Affero General Public License version 3 (see the file LICENSE).
58
59 __metaclass__ = type
60@@ -123,7 +123,10 @@
61 IGitRepositorySet,
62 user_has_special_git_repository_access,
63 )
64-from lp.code.interfaces.gitrule import describe_git_permissions
65+from lp.code.interfaces.gitrule import (
66+ describe_git_permissions,
67+ is_rule_exact,
68+ )
69 from lp.code.interfaces.revision import IRevisionSet
70 from lp.code.mail.branch import send_git_repository_modified_notifications
71 from lp.code.model.branchmergeproposal import BranchMergeProposal
72@@ -1171,17 +1174,22 @@
73 return Store.of(self).find(
74 GitRule, GitRule.repository == self).order_by(GitRule.position)
75
76+ def _canonicaliseRuleOrdering(self, rules):
77+ """Canonicalise rule ordering.
78+
79+ Exact-match rules come first in lexicographical order, followed by
80+ wildcard rules in the requested order. (Note that `sorted` is
81+ guaranteed to be stable.)
82+ """
83+ return sorted(rules, key=lambda rule: (
84+ (0, rule.ref_pattern) if is_rule_exact(rule) else (1,)))
85+
86 def _syncRulePositions(self, rules):
87 """Synchronise rule positions with their order in a provided list.
88
89 :param rules: A sequence of `IGitRule`s in the desired order.
90 """
91- # Canonicalise rule ordering: exact-match rules come first in
92- # lexicographical order, followed by wildcard rules in the requested
93- # order. (Note that `sorted` is guaranteed to be stable.)
94- rules = sorted(
95- rules,
96- key=lambda rule: (0, rule.ref_pattern) if rule.is_exact else (1,))
97+ rules = self._canonicaliseRuleOrdering(rules)
98 # Ensure the correct position of all rules, which may involve more
99 # work than necessary, but is simple and tends to be
100 # self-correcting. This works because the unique constraint on
101@@ -1283,7 +1291,9 @@
102 """See `IGitRepository`."""
103 self._validateRules(rules)
104 existing_rules = {rule.ref_pattern: rule for rule in self.rules}
105- new_rules = OrderedDict((rule.ref_pattern, rule) for rule in rules)
106+ new_rules = OrderedDict(
107+ (rule.ref_pattern, rule)
108+ for rule in self._canonicaliseRuleOrdering(rules))
109 GitRule.preloadGrantsForRules(existing_rules.values())
110
111 # Remove old rules first so that we don't generate unnecessary move
112
113=== modified file 'lib/lp/code/model/gitrule.py'
114--- lib/lp/code/model/gitrule.py 2018-10-31 14:56:02 +0000
115+++ lib/lp/code/model/gitrule.py 2019-02-11 12:57:37 +0000
116@@ -1,4 +1,4 @@
117-# Copyright 2018 Canonical Ltd. This software is licensed under the
118+# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
119 # GNU Affero General Public License version 3 (see the file LICENSE).
120
121 """Git repository access rules."""
122@@ -127,13 +127,6 @@
123 return "<GitRule '%s' for %s>" % (
124 self.ref_pattern, self.repository.unique_name)
125
126- @property
127- def is_exact(self):
128- """See `IGitRule`."""
129- # turnip's glob_to_re only treats * as special, so any rule whose
130- # pattern does not contain * must be an exact-match rule.
131- return "*" not in self.ref_pattern
132-
133 def toDataForJSON(self, media_type):
134 """See `IJSONPublishable`."""
135 if media_type != "application/json":
136@@ -253,8 +246,29 @@
137 removeSecurityProxy(grant).date_last_modified = UTC_NOW
138
139
140+class GitRuleGrantMixin:
141+ """Properties common to GitRuleGrant and GitNascentRuleGrant."""
142+
143+ @property
144+ def permissions(self):
145+ permissions = set()
146+ if self.can_create:
147+ permissions.add(GitPermissionType.CAN_CREATE)
148+ if self.can_push:
149+ permissions.add(GitPermissionType.CAN_PUSH)
150+ if self.can_force_push:
151+ permissions.add(GitPermissionType.CAN_FORCE_PUSH)
152+ return frozenset(permissions)
153+
154+ @permissions.setter
155+ def permissions(self, value):
156+ self.can_create = GitPermissionType.CAN_CREATE in value
157+ self.can_push = GitPermissionType.CAN_PUSH in value
158+ self.can_force_push = GitPermissionType.CAN_FORCE_PUSH in value
159+
160+
161 @implementer(IGitRuleGrant, IJSONPublishable)
162-class GitRuleGrant(StormBase):
163+class GitRuleGrant(StormBase, GitRuleGrantMixin):
164 """See `IGitRuleGrant`."""
165
166 __storm_table__ = 'GitRuleGrant'
167@@ -326,23 +340,6 @@
168 ", ".join(describe_git_permissions(self.permissions)),
169 grantee_name, self.repository.unique_name, self.rule.ref_pattern)
170
171- @property
172- def permissions(self):
173- permissions = set()
174- if self.can_create:
175- permissions.add(GitPermissionType.CAN_CREATE)
176- if self.can_push:
177- permissions.add(GitPermissionType.CAN_PUSH)
178- if self.can_force_push:
179- permissions.add(GitPermissionType.CAN_FORCE_PUSH)
180- return frozenset(permissions)
181-
182- @permissions.setter
183- def permissions(self, value):
184- self.can_create = GitPermissionType.CAN_CREATE in value
185- self.can_push = GitPermissionType.CAN_PUSH in value
186- self.can_force_push = GitPermissionType.CAN_FORCE_PUSH in value
187-
188 def toDataForJSON(self, media_type):
189 """See `IJSONPublishable`."""
190 if media_type != "application/json":
191@@ -367,6 +364,9 @@
192 self.ref_pattern = ref_pattern
193 self.grants = grants
194
195+ def __repr__(self):
196+ return "<GitNascentRule '%s'>" % self.ref_pattern
197+
198
199 @adapter(dict)
200 @implementer(IGitNascentRule)
201@@ -375,7 +375,7 @@
202
203
204 @implementer(IGitNascentRuleGrant)
205-class GitNascentRuleGrant:
206+class GitNascentRuleGrant(GitRuleGrantMixin):
207
208 def __init__(self, grantee_type, grantee=None, can_create=False,
209 can_push=False, can_force_push=False):
210@@ -385,6 +385,15 @@
211 self.can_push = can_push
212 self.can_force_push = can_force_push
213
214+ def __repr__(self):
215+ if self.grantee_type == GitGranteeType.PERSON:
216+ grantee_name = "~%s" % self.grantee.name
217+ else:
218+ grantee_name = self.grantee_type.title.lower()
219+ return "<GitNascentRuleGrant [%s] to %s>" % (
220+ ", ".join(describe_git_permissions(self.permissions)),
221+ grantee_name)
222+
223
224 @adapter(dict)
225 @implementer(IGitNascentRuleGrant)
226
227=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
228--- lib/lp/code/model/tests/test_gitrepository.py 2019-01-28 18:09:21 +0000
229+++ lib/lp/code/model/tests/test_gitrepository.py 2019-02-11 12:57:37 +0000
230@@ -1,4 +1,4 @@
231-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
232+# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
233 # GNU Affero General Public License version 3 (see the file LICENSE).
234
235 """Tests for Git repositories."""
236@@ -2848,6 +2848,20 @@
237 date_last_modified=Equals(date_created)),
238 ]))
239
240+ def test_setRules_canonicalises_expected_ordering(self):
241+ repository = self.factory.makeGitRepository()
242+ with person_logged_in(repository.owner):
243+ repository.setRules([
244+ IGitNascentRule({
245+ "ref_pattern": "refs/heads/master-next",
246+ "grants": [],
247+ }),
248+ IGitNascentRule({
249+ "ref_pattern": "refs/heads/master",
250+ "grants": [],
251+ }),
252+ ], repository.owner)
253+
254 def test_setRules_modify_grants(self):
255 owner = self.factory.makeTeam()
256 members = [
257
258=== modified file 'lib/lp/code/model/tests/test_gitrule.py'
259--- lib/lp/code/model/tests/test_gitrule.py 2018-10-31 14:56:02 +0000
260+++ lib/lp/code/model/tests/test_gitrule.py 2019-02-11 12:57:37 +0000
261@@ -1,4 +1,4 @@
262-# Copyright 2018 Canonical Ltd. This software is licensed under the
263+# Copyright 2018-2019 Canonical Ltd. This software is licensed under the
264 # GNU Affero General Public License version 3 (see the file LICENSE).
265
266 """Tests for Git repository access rules."""
267@@ -28,6 +28,7 @@
268 IGitNascentRuleGrant,
269 IGitRule,
270 IGitRuleGrant,
271+ is_rule_exact,
272 )
273 from lp.services.database.sqlbase import get_transaction_timestamp
274 from lp.services.webapp.snapshot import notify_modified
275@@ -69,7 +70,7 @@
276 "<GitRule 'refs/heads/*' for %s>" % repository.unique_name,
277 repr(rule))
278
279- def test_is_exact(self):
280+ def test_is_rule_exact(self):
281 repository = self.factory.makeGitRepository()
282 for ref_pattern, is_exact in (
283 ("refs/heads/master", True),
284@@ -83,9 +84,9 @@
285 ):
286 self.assertEqual(
287 is_exact,
288- self.factory.makeGitRule(
289+ is_rule_exact(self.factory.makeGitRule(
290 repository=repository,
291- ref_pattern=ref_pattern).is_exact)
292+ ref_pattern=ref_pattern)))
293
294 def test_grants(self):
295 rule = self.factory.makeGitRule()