Merge lp:~cjwatson/launchpad/git-permissions-notifications into lp:launchpad
- git-permissions-notifications
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18796 | ||||
Proposed branch: | lp:~cjwatson/launchpad/git-permissions-notifications | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/git-activity-model | ||||
Diff against target: |
610 lines (+301/-31) 10 files modified
database/schema/security.cfg (+1/-0) lib/lp/code/adapters/gitrepository.py (+8/-3) lib/lp/code/interfaces/gitactivity.py (+7/-1) lib/lp/code/interfaces/gitrepository.py (+3/-1) lib/lp/code/mail/branch.py (+26/-9) lib/lp/code/model/gitactivity.py (+11/-0) lib/lp/code/model/gitjob.py (+64/-5) lib/lp/code/model/gitrepository.py (+4/-2) lib/lp/code/model/tests/test_gitjob.py (+139/-1) lib/lp/code/model/tests/test_gitrepository.py (+38/-9) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-permissions-notifications | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+354500@code.launchpad.net |
Commit message
Send email notifications for GitRule and GitGrant changes.
Description of the change
We'll have to be careful to send the right ObjectModifiedE
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2018-10-15 14:44:25 +0000 |
3 | +++ database/schema/security.cfg 2018-10-16 10:11:25 +0000 |
4 | @@ -2053,6 +2053,7 @@ |
5 | public.branchmergeproposaljob = SELECT, INSERT |
6 | public.branchrevision = SELECT |
7 | public.branchsubscription = SELECT |
8 | +public.codeimport = SELECT |
9 | public.codereviewinlinecomment = SELECT, INSERT |
10 | public.codereviewmessage = SELECT, INSERT |
11 | public.codereviewvote = SELECT, INSERT |
12 | |
13 | === modified file 'lib/lp/code/adapters/gitrepository.py' |
14 | --- lib/lp/code/adapters/gitrepository.py 2015-07-08 16:05:11 +0000 |
15 | +++ lib/lp/code/adapters/gitrepository.py 2018-10-16 10:11:25 +0000 |
16 | @@ -1,4 +1,4 @@ |
17 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
18 | +# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
19 | # GNU Affero General Public License version 3 (see the file LICENSE). |
20 | |
21 | """Components related to Git repositories.""" |
22 | @@ -27,12 +27,14 @@ |
23 | |
24 | interface = IGitRepository |
25 | |
26 | - def __init__(self, repository, user, name=None, git_identity=None): |
27 | + def __init__(self, repository, user, name=None, git_identity=None, |
28 | + activities=None): |
29 | self.repository = repository |
30 | self.user = user |
31 | |
32 | self.name = name |
33 | self.git_identity = git_identity |
34 | + self.activities = activities |
35 | |
36 | @classmethod |
37 | def construct(klass, old_repository, new_repository, user): |
38 | @@ -43,10 +45,13 @@ |
39 | """ |
40 | delta = ObjectDelta(old_repository, new_repository) |
41 | delta.recordNewAndOld(klass.delta_values) |
42 | - if delta.changes: |
43 | + activities = list(new_repository.getActivity( |
44 | + changed_after=old_repository.date_last_modified)) |
45 | + if delta.changes or activities: |
46 | changes = delta.changes |
47 | changes["repository"] = new_repository |
48 | changes["user"] = user |
49 | + changes["activities"] = activities |
50 | |
51 | return GitRepositoryDelta(**changes) |
52 | else: |
53 | |
54 | === modified file 'lib/lp/code/interfaces/gitactivity.py' |
55 | --- lib/lp/code/interfaces/gitactivity.py 2018-10-03 00:53:23 +0000 |
56 | +++ lib/lp/code/interfaces/gitactivity.py 2018-10-16 10:11:25 +0000 |
57 | @@ -12,7 +12,10 @@ |
58 | ] |
59 | |
60 | from lazr.restful.fields import Reference |
61 | -from zope.interface import Interface |
62 | +from zope.interface import ( |
63 | + Attribute, |
64 | + Interface, |
65 | + ) |
66 | from zope.schema import ( |
67 | Choice, |
68 | Datetime, |
69 | @@ -55,6 +58,9 @@ |
70 | vocabulary="ValidPersonOrTeam", |
71 | description=_("The person or team that this change was applied to.")) |
72 | |
73 | + changee_description = Attribute( |
74 | + "A human-readable description of the changee.") |
75 | + |
76 | what_changed = Choice( |
77 | title=_("What changed"), required=True, readonly=True, |
78 | vocabulary=GitActivityType, |
79 | |
80 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
81 | --- lib/lp/code/interfaces/gitrepository.py 2018-10-15 14:44:25 +0000 |
82 | +++ lib/lp/code/interfaces/gitrepository.py 2018-10-16 10:11:25 +0000 |
83 | @@ -622,9 +622,11 @@ |
84 | :return: The diff as a binary string. |
85 | """ |
86 | |
87 | - def getActivity(): |
88 | + def getActivity(changed_after=None): |
89 | """Get activity log entries for this repository. |
90 | |
91 | + :param changed_after: If supplied, only return entries for changes |
92 | + made after this date. |
93 | :return: A `ResultSet` of `IGitActivity`. |
94 | """ |
95 | |
96 | |
97 | === modified file 'lib/lp/code/mail/branch.py' |
98 | --- lib/lp/code/mail/branch.py 2015-09-01 17:10:46 +0000 |
99 | +++ lib/lp/code/mail/branch.py 2018-10-16 10:11:25 +0000 |
100 | @@ -1,12 +1,16 @@ |
101 | -# Copyright 2009-2011 Canonical Ltd. This software is licensed under the |
102 | +# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
103 | # GNU Affero General Public License version 3 (see the file LICENSE). |
104 | |
105 | """Email notifications related to branches.""" |
106 | |
107 | __metaclass__ = type |
108 | |
109 | -from zope.component import getUtility |
110 | +from zope.component import ( |
111 | + getAdapter, |
112 | + getUtility, |
113 | + ) |
114 | |
115 | +from lp.app.interfaces.security import IAuthorization |
116 | from lp.code.adapters.branch import BranchDelta |
117 | from lp.code.adapters.gitrepository import GitRepositoryDelta |
118 | from lp.code.enums import ( |
119 | @@ -20,6 +24,7 @@ |
120 | from lp.code.interfaces.gitref import IGitRef |
121 | from lp.registry.interfaces.person import IPerson |
122 | from lp.registry.interfaces.product import IProduct |
123 | +from lp.registry.interfaces.role import IPersonRoles |
124 | from lp.services.config import config |
125 | from lp.services.mail import basemailer |
126 | from lp.services.mail.basemailer import BaseMailer |
127 | @@ -163,13 +168,14 @@ |
128 | app = 'code' |
129 | |
130 | def __init__(self, subject, template_name, recipients, from_address, |
131 | - delta=None, contents=None, diff=None, message_id=None, |
132 | - revno=None, revision_id=None, notification_type=None, |
133 | - **kwargs): |
134 | + delta=None, delta_for_editors=None, contents=None, diff=None, |
135 | + message_id=None, revno=None, revision_id=None, |
136 | + notification_type=None, **kwargs): |
137 | super(BranchMailer, self).__init__( |
138 | subject, template_name, recipients, from_address, |
139 | message_id=message_id, notification_type=notification_type) |
140 | self.delta_text = delta |
141 | + self.delta_for_editors_text = delta_for_editors |
142 | self.contents = contents |
143 | self.diff = diff |
144 | if diff is None: |
145 | @@ -181,12 +187,16 @@ |
146 | self.extra_template_params = kwargs |
147 | |
148 | @classmethod |
149 | - def forBranchModified(cls, branch, user, delta): |
150 | + def forBranchModified(cls, branch, user, delta, delta_for_editors=None): |
151 | """Construct a BranchMailer for mail about a branch modification. |
152 | |
153 | :param branch: The branch that was modified. |
154 | :param user: The user making the change. |
155 | - :param delta: an IBranchDelta representing the modification. |
156 | + :param delta: an IBranchDelta representing the modification as |
157 | + visible to people who cannot edit the branch. |
158 | + :param delta_for_editors: an IBranchDelta representing the |
159 | + notification as visible to people who can edit the branch. If |
160 | + None, `delta` is used for people who can edit the branch too. |
161 | :return: a BranchMailer. |
162 | """ |
163 | recipients = branch.getNotificationRecipients() |
164 | @@ -218,6 +228,7 @@ |
165 | return cls( |
166 | '[Branch %(unique_name)s]', 'branch-modified.txt', |
167 | actual_recipients, from_address, delta=delta, |
168 | + delta_for_editors=delta_for_editors, |
169 | notification_type='branch-updated') |
170 | |
171 | @classmethod |
172 | @@ -288,8 +299,14 @@ |
173 | params['diff'] = self.contents or '' |
174 | if not self._includeDiff(email): |
175 | params['diff'] += self._explainNotPresentDiff(email) |
176 | - params['delta'] = ( |
177 | - self.delta_text if self.delta_text is not None else '') |
178 | + if self.delta_for_editors_text is not None: |
179 | + authz = getAdapter(branch, IAuthorization, 'launchpad.Edit') |
180 | + if authz.checkAuthenticated(IPersonRoles(recipient)): |
181 | + params['delta'] = self.delta_for_editors_text |
182 | + else: |
183 | + params['delta'] = self.delta_text or '' |
184 | + else: |
185 | + params['delta'] = self.delta_text or '' |
186 | params.update(self.extra_template_params) |
187 | return params |
188 | |
189 | |
190 | === modified file 'lib/lp/code/model/gitactivity.py' |
191 | --- lib/lp/code/model/gitactivity.py 2018-09-11 16:39:57 +0000 |
192 | +++ lib/lp/code/model/gitactivity.py 2018-10-16 10:11:25 +0000 |
193 | @@ -72,6 +72,17 @@ |
194 | self.old_value = old_value |
195 | self.new_value = new_value |
196 | |
197 | + @property |
198 | + def changee_description(self): |
199 | + if self.changee is not None: |
200 | + return "~" + self.changee.name |
201 | + elif self.new_value is not None and "changee_type" in self.new_value: |
202 | + return self.new_value["changee_type"].lower() |
203 | + elif self.old_value is not None and "changee_type" in self.old_value: |
204 | + return self.old_value["changee_type"].lower() |
205 | + else: |
206 | + return None |
207 | + |
208 | |
209 | def _make_rule_value(rule, position=None): |
210 | return { |
211 | |
212 | === modified file 'lib/lp/code/model/gitjob.py' |
213 | --- lib/lp/code/model/gitjob.py 2015-10-01 14:45:57 +0000 |
214 | +++ lib/lp/code/model/gitjob.py 2018-10-16 10:11:25 +0000 |
215 | @@ -1,9 +1,10 @@ |
216 | -# Copyright 2015 Canonical Ltd. This software is licensed under the |
217 | +# Copyright 2015-2018 Canonical Ltd. This software is licensed under the |
218 | # GNU Affero General Public License version 3 (see the file LICENSE). |
219 | |
220 | __metaclass__ = type |
221 | |
222 | __all__ = [ |
223 | + 'describe_repository_delta', |
224 | 'GitJob', |
225 | 'GitJobType', |
226 | 'GitRefScanJob', |
227 | @@ -32,6 +33,7 @@ |
228 | from zope.security.proxy import removeSecurityProxy |
229 | |
230 | from lp.app.errors import NotFoundError |
231 | +from lp.code.enums import GitActivityType |
232 | from lp.code.interfaces.githosting import IGitHostingClient |
233 | from lp.code.interfaces.gitjob import ( |
234 | IGitJob, |
235 | @@ -57,6 +59,7 @@ |
236 | ) |
237 | from lp.services.database.stormbase import StormBase |
238 | from lp.services.features import getFeatureFlag |
239 | +from lp.services.helpers import english_list |
240 | from lp.services.job.model.job import ( |
241 | EnumeratedSubclass, |
242 | Job, |
243 | @@ -298,6 +301,57 @@ |
244 | getUtility(IGitHostingClient).delete(self.repository_path, logger=log) |
245 | |
246 | |
247 | +activity_descriptions = { |
248 | + GitActivityType.RULE_ADDED: "Added protected ref: {new[ref_pattern]}", |
249 | + GitActivityType.RULE_CHANGED: ( |
250 | + "Changed protected ref: {old[ref_pattern]} => {new[ref_pattern]}"), |
251 | + GitActivityType.RULE_REMOVED: "Removed protected ref: {old[ref_pattern]}", |
252 | + GitActivityType.RULE_MOVED: ( |
253 | + "Moved rule for protected ref {new[ref_pattern]}: " |
254 | + "position {old[position]} => {new[position]}"), |
255 | + GitActivityType.GRANT_ADDED: ( |
256 | + "Added access for {changee} to {new[ref_pattern]}: {new_grants}"), |
257 | + GitActivityType.GRANT_CHANGED: ( |
258 | + "Changed access for {changee} to {new[ref_pattern]}: " |
259 | + "{old_grants} => {new_grants}"), |
260 | + GitActivityType.GRANT_REMOVED: ( |
261 | + "Removed access for {changee} to {old[ref_pattern]}: {old_grants}"), |
262 | + } |
263 | + |
264 | + |
265 | +def describe_grants(activity_value): |
266 | + output = [] |
267 | + if activity_value is not None: |
268 | + if activity_value.get("can_create"): |
269 | + output.append("create") |
270 | + if activity_value.get("can_push"): |
271 | + output.append("push") |
272 | + if activity_value.get("can_force_push"): |
273 | + output.append("force-push") |
274 | + return english_list(output) |
275 | + |
276 | + |
277 | +def describe_repository_delta(repository_delta): |
278 | + output = text_delta( |
279 | + repository_delta, repository_delta.delta_values, |
280 | + repository_delta.new_values, repository_delta.interface).split("\n") |
281 | + if output and not output[-1]: # text_delta returned empty string |
282 | + output.pop() |
283 | + # Parts of the delta are only visible to people who can edit the |
284 | + # repository. |
285 | + output_for_editors = list(output) |
286 | + indent = " " * 4 |
287 | + for activity in repository_delta.activities: |
288 | + if activity.what_changed in activity_descriptions: |
289 | + description = activity_descriptions[activity.what_changed].format( |
290 | + old=activity.old_value, new=activity.new_value, |
291 | + changee=activity.changee_description, |
292 | + old_grants=describe_grants(activity.old_value), |
293 | + new_grants=describe_grants(activity.new_value)) |
294 | + output_for_editors.append(indent + description) |
295 | + return "\n".join(output), "\n".join(output_for_editors) |
296 | + |
297 | + |
298 | @implementer(IGitRepositoryModifiedMailJob) |
299 | @provider(IGitRepositoryModifiedMailJobSource) |
300 | class GitRepositoryModifiedMailJob(GitJobDerived): |
301 | @@ -310,11 +364,11 @@ |
302 | @classmethod |
303 | def create(cls, repository, user, repository_delta): |
304 | """See `IGitRepositoryModifiedMailJobSource`.""" |
305 | + delta, delta_for_editors = describe_repository_delta(repository_delta) |
306 | metadata = { |
307 | "user": user.id, |
308 | - "repository_delta": text_delta( |
309 | - repository_delta, repository_delta.delta_values, |
310 | - repository_delta.new_values, repository_delta.interface), |
311 | + "repository_delta": delta, |
312 | + "repository_delta_for_editors": delta_for_editors, |
313 | } |
314 | git_job = GitJob(repository, cls.class_job_type, metadata) |
315 | job = cls(git_job) |
316 | @@ -329,10 +383,15 @@ |
317 | def repository_delta(self): |
318 | return self.metadata["repository_delta"] |
319 | |
320 | + @property |
321 | + def repository_delta_for_editors(self): |
322 | + return self.metadata["repository_delta_for_editors"] |
323 | + |
324 | def getMailer(self): |
325 | """Return a `BranchMailer` for this job.""" |
326 | return BranchMailer.forBranchModified( |
327 | - self.repository, self.user, self.repository_delta) |
328 | + self.repository, self.user, self.repository_delta, |
329 | + delta_for_editors=self.repository_delta_for_editors) |
330 | |
331 | def run(self): |
332 | """See `IGitRepositoryModifiedMailJob`.""" |
333 | |
334 | === modified file 'lib/lp/code/model/gitrepository.py' |
335 | --- lib/lp/code/model/gitrepository.py 2018-10-15 14:44:25 +0000 |
336 | +++ lib/lp/code/model/gitrepository.py 2018-10-16 10:11:25 +0000 |
337 | @@ -240,7 +240,7 @@ |
338 | """ |
339 | if event.edited_fields: |
340 | repository.date_last_modified = UTC_NOW |
341 | - send_git_repository_modified_notifications(repository, event) |
342 | + send_git_repository_modified_notifications(repository, event) |
343 | |
344 | |
345 | @implementer(IGitRepository, IHasOwner, IPrivacy, IInformationType) |
346 | @@ -1208,9 +1208,11 @@ |
347 | return self.grants.find( |
348 | GitRuleGrant.grantee_type == GitGranteeType.REPOSITORY_OWNER) |
349 | |
350 | - def getActivity(self): |
351 | + def getActivity(self, changed_after=None): |
352 | """See `IGitRepository`.""" |
353 | clauses = [GitActivity.repository_id == self.id] |
354 | + if changed_after is not None: |
355 | + clauses.append(GitActivity.date_changed > changed_after) |
356 | return Store.of(self).find(GitActivity, *clauses).order_by( |
357 | Desc(GitActivity.date_changed), Desc(GitActivity.id)) |
358 | |
359 | |
360 | === modified file 'lib/lp/code/model/tests/test_gitjob.py' |
361 | --- lib/lp/code/model/tests/test_gitjob.py 2018-09-27 13:50:06 +0000 |
362 | +++ lib/lp/code/model/tests/test_gitjob.py 2018-10-16 10:11:25 +0000 |
363 | @@ -13,6 +13,8 @@ |
364 | ) |
365 | import hashlib |
366 | |
367 | +from lazr.lifecycle.event import ObjectModifiedEvent |
368 | +from lazr.lifecycle.snapshot import Snapshot |
369 | import pytz |
370 | from testtools.matchers import ( |
371 | ContainsDict, |
372 | @@ -21,9 +23,16 @@ |
373 | MatchesSetwise, |
374 | MatchesStructure, |
375 | ) |
376 | +import transaction |
377 | +from zope.event import notify |
378 | +from zope.interface import providedBy |
379 | from zope.security.proxy import removeSecurityProxy |
380 | |
381 | -from lp.code.enums import GitObjectType |
382 | +from lp.code.adapters.gitrepository import GitRepositoryDelta |
383 | +from lp.code.enums import ( |
384 | + GitGranteeType, |
385 | + GitObjectType, |
386 | + ) |
387 | from lp.code.interfaces.branchmergeproposal import ( |
388 | BRANCH_MERGE_PROPOSAL_WEBHOOKS_FEATURE_FLAG, |
389 | ) |
390 | @@ -33,6 +42,7 @@ |
391 | IReclaimGitRepositorySpaceJob, |
392 | ) |
393 | from lp.code.model.gitjob import ( |
394 | + describe_repository_delta, |
395 | GitJob, |
396 | GitJobDerived, |
397 | GitJobType, |
398 | @@ -47,6 +57,7 @@ |
399 | from lp.services.utils import seconds_since_epoch |
400 | from lp.services.webapp import canonical_url |
401 | from lp.testing import ( |
402 | + person_logged_in, |
403 | TestCaseWithFactory, |
404 | time_counter, |
405 | ) |
406 | @@ -338,5 +349,132 @@ |
407 | self.assertEqual([(path,)], hosting_fixture.delete.extract_args()) |
408 | |
409 | |
410 | +class TestDescribeRepositoryDelta(TestCaseWithFactory): |
411 | + """Tests for `describe_repository_delta`.""" |
412 | + |
413 | + layer = ZopelessDatabaseLayer |
414 | + |
415 | + def assertDeltaDescriptionEqual(self, expected, expected_for_editors, |
416 | + snapshot, repository): |
417 | + repository_delta = GitRepositoryDelta.construct( |
418 | + snapshot, repository, repository.owner) |
419 | + delta, delta_for_editors = describe_repository_delta(repository_delta) |
420 | + self.assertEqual( |
421 | + "\n".join(" %s" % line for line in expected), delta) |
422 | + self.assertEqual( |
423 | + "\n".join(" %s" % line for line in expected_for_editors), |
424 | + delta_for_editors) |
425 | + |
426 | + def test_change_basic_properties(self): |
427 | + repository = self.factory.makeGitRepository(name="foo") |
428 | + transaction.commit() |
429 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
430 | + with person_logged_in(repository.owner): |
431 | + repository.setName("bar", repository.owner) |
432 | + expected = [ |
433 | + "Name: foo => bar", |
434 | + "Git identity: lp:~{person}/{project}/+git/foo => " |
435 | + "lp:~{person}/{project}/+git/bar".format( |
436 | + person=repository.owner.name, project=repository.target.name), |
437 | + ] |
438 | + self.assertDeltaDescriptionEqual( |
439 | + expected, expected, snapshot, repository) |
440 | + |
441 | + def test_add_rule(self): |
442 | + repository = self.factory.makeGitRepository() |
443 | + transaction.commit() |
444 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
445 | + with person_logged_in(repository.owner): |
446 | + repository.addRule("refs/heads/*", repository.owner) |
447 | + self.assertDeltaDescriptionEqual( |
448 | + [], ["Added protected ref: refs/heads/*"], snapshot, repository) |
449 | + |
450 | + def test_change_rule(self): |
451 | + repository = self.factory.makeGitRepository() |
452 | + rule = self.factory.makeGitRule( |
453 | + repository=repository, ref_pattern="refs/heads/foo") |
454 | + transaction.commit() |
455 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
456 | + rule_snapshot = Snapshot(rule, providing=providedBy(rule)) |
457 | + with person_logged_in(repository.owner): |
458 | + rule.ref_pattern = "refs/heads/bar" |
459 | + notify(ObjectModifiedEvent(rule, rule_snapshot, ["ref_pattern"])) |
460 | + self.assertDeltaDescriptionEqual( |
461 | + [], ["Changed protected ref: refs/heads/foo => refs/heads/bar"], |
462 | + snapshot, repository) |
463 | + |
464 | + def test_remove_rule(self): |
465 | + repository = self.factory.makeGitRepository() |
466 | + rule = self.factory.makeGitRule( |
467 | + repository=repository, ref_pattern="refs/heads/*") |
468 | + transaction.commit() |
469 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
470 | + with person_logged_in(repository.owner): |
471 | + rule.destroySelf(repository.owner) |
472 | + self.assertDeltaDescriptionEqual( |
473 | + [], ["Removed protected ref: refs/heads/*"], snapshot, repository) |
474 | + |
475 | + def test_move_rule(self): |
476 | + repository = self.factory.makeGitRepository() |
477 | + rule = self.factory.makeGitRule( |
478 | + repository=repository, ref_pattern="refs/heads/*") |
479 | + self.factory.makeGitRule( |
480 | + repository=repository, ref_pattern="refs/heads/stable/*") |
481 | + transaction.commit() |
482 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
483 | + with person_logged_in(repository.owner): |
484 | + repository.moveRule(rule, 1, repository.owner) |
485 | + self.assertDeltaDescriptionEqual( |
486 | + [], ["Moved rule for protected ref refs/heads/*: position 0 => 1"], |
487 | + snapshot, repository) |
488 | + |
489 | + def test_add_grant(self): |
490 | + repository = self.factory.makeGitRepository() |
491 | + rule = self.factory.makeGitRule( |
492 | + repository=repository, ref_pattern="refs/heads/*") |
493 | + transaction.commit() |
494 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
495 | + with person_logged_in(repository.owner): |
496 | + rule.addGrant( |
497 | + GitGranteeType.REPOSITORY_OWNER, repository.owner, |
498 | + can_create=True, can_push=True, can_force_push=True) |
499 | + self.assertDeltaDescriptionEqual( |
500 | + [], |
501 | + ["Added access for repository owner to refs/heads/*: " |
502 | + "create, push, and force-push"], |
503 | + snapshot, repository) |
504 | + |
505 | + def test_change_grant(self): |
506 | + repository = self.factory.makeGitRepository() |
507 | + grant = self.factory.makeGitRuleGrant( |
508 | + repository=repository, ref_pattern="refs/heads/*", |
509 | + can_create=True) |
510 | + transaction.commit() |
511 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
512 | + grant_snapshot = Snapshot(grant, providing=providedBy(grant)) |
513 | + with person_logged_in(repository.owner): |
514 | + grant.can_push = True |
515 | + notify(ObjectModifiedEvent(grant, grant_snapshot, ["can_push"])) |
516 | + self.assertDeltaDescriptionEqual( |
517 | + [], |
518 | + ["Changed access for ~{grantee} to refs/heads/*: " |
519 | + "create => create and push".format(grantee=grant.grantee.name)], |
520 | + snapshot, repository) |
521 | + |
522 | + def test_remove_grant(self): |
523 | + repository = self.factory.makeGitRepository() |
524 | + grant = self.factory.makeGitRuleGrant( |
525 | + repository=repository, ref_pattern="refs/heads/*", |
526 | + grantee=GitGranteeType.REPOSITORY_OWNER, can_push=True) |
527 | + transaction.commit() |
528 | + snapshot = Snapshot(repository, providing=providedBy(repository)) |
529 | + with person_logged_in(repository.owner): |
530 | + grant.destroySelf(repository.owner) |
531 | + self.assertDeltaDescriptionEqual( |
532 | + [], |
533 | + ["Removed access for repository owner to refs/heads/*: push"], |
534 | + snapshot, repository) |
535 | + |
536 | + |
537 | # XXX cjwatson 2015-03-12: We should test that the jobs work via Celery too, |
538 | # but that isn't feasible until we have a proper turnip fixture. |
539 | |
540 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
541 | --- lib/lp/code/model/tests/test_gitrepository.py 2018-10-15 14:44:25 +0000 |
542 | +++ lib/lp/code/model/tests/test_gitrepository.py 2018-10-16 10:11:25 +0000 |
543 | @@ -981,6 +981,12 @@ |
544 | # Attribute modifications send mail to subscribers. |
545 | self.assertEqual(0, len(stub.test_emails)) |
546 | repository = self.factory.makeGitRepository(name="foo") |
547 | + subscriber = self.factory.makePerson() |
548 | + owner_address = ( |
549 | + removeSecurityProxy(repository.owner.preferredemail).email) |
550 | + subscriber_address = ( |
551 | + removeSecurityProxy(subscriber.preferredemail).email) |
552 | + transaction.commit() |
553 | repository_before_modification = Snapshot( |
554 | repository, providing=providedBy(repository)) |
555 | with person_logged_in(repository.owner): |
556 | @@ -990,22 +996,45 @@ |
557 | BranchSubscriptionDiffSize.NODIFF, |
558 | CodeReviewNotificationLevel.NOEMAIL, |
559 | repository.owner) |
560 | + repository.subscribe( |
561 | + subscriber, |
562 | + BranchSubscriptionNotificationLevel.ATTRIBUTEONLY, |
563 | + BranchSubscriptionDiffSize.NODIFF, |
564 | + CodeReviewNotificationLevel.NOEMAIL, |
565 | + repository.owner) |
566 | repository.setName("bar", repository.owner) |
567 | + repository.addRule("refs/heads/stable/*", repository.owner) |
568 | notify(ObjectModifiedEvent( |
569 | repository, repository_before_modification, ["name"], |
570 | user=repository.owner)) |
571 | with dbuser(config.IGitRepositoryModifiedMailJobSource.dbuser): |
572 | JobRunner.fromReady( |
573 | getUtility(IGitRepositoryModifiedMailJobSource)).runAll() |
574 | - self.assertEqual(1, len(stub.test_emails)) |
575 | - message = email.message_from_string(stub.test_emails[0][2]) |
576 | - body = message.get_payload(decode=True) |
577 | - self.assertIn("Name: foo => bar\n", body) |
578 | - self.assertIn( |
579 | - "Git identity: lp:~{person}/{project}/+git/foo => " |
580 | - "lp:~{person}/{project}/+git/bar\n".format( |
581 | - person=repository.owner.name, project=repository.target.name), |
582 | - body) |
583 | + bodies_by_recipient = {} |
584 | + for from_addr, to_addrs, message in stub.test_emails: |
585 | + body = email.message_from_string(message).get_payload(decode=True) |
586 | + for to_addr in to_addrs: |
587 | + bodies_by_recipient[to_addr] = body |
588 | + # Both the owner and the unprivileged subscriber receive email. |
589 | + self.assertContentEqual( |
590 | + [owner_address, subscriber_address], bodies_by_recipient.keys()) |
591 | + # The owner receives a message including details of permission |
592 | + # changes. |
593 | + self.assertEqual( |
594 | + " Name: foo => bar\n" |
595 | + " Git identity: lp:~{person}/{project}/+git/foo => " |
596 | + "lp:~{person}/{project}/+git/bar\n" |
597 | + " Added protected ref: refs/heads/stable/*".format( |
598 | + person=repository.owner.name, project=repository.target.name), |
599 | + bodies_by_recipient[owner_address].split("\n\n--\n")[0]) |
600 | + # The unprivileged subscriber receives a message omitting details of |
601 | + # permission changes. |
602 | + self.assertEqual( |
603 | + " Name: foo => bar\n" |
604 | + " Git identity: lp:~{person}/{project}/+git/foo => " |
605 | + "lp:~{person}/{project}/+git/bar".format( |
606 | + person=repository.owner.name, project=repository.target.name), |
607 | + bodies_by_recipient[subscriber_address].split("\n\n--\n")[0]) |
608 | |
609 | # XXX cjwatson 2015-02-04: This will need to be expanded once Launchpad |
610 | # actually notices any interesting kind of repository modifications. |
My only concern is that this will probably send rule changes to all subscribers, not just those who can see the rules normally (the owner).