Merge lp:~cjwatson/launchpad/git-mail-fix-delta into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17445
Proposed branch: lp:~cjwatson/launchpad/git-mail-fix-delta
Merge into: lp:launchpad
Diff against target: 311 lines (+157/-17)
6 files modified
lib/lp/code/adapters/gitrepository.py (+6/-4)
lib/lp/code/adapters/tests/test_gitrepository.py (+53/-0)
lib/lp/code/interfaces/gitrepository.py (+12/-3)
lib/lp/code/model/gitnamespace.py (+10/-6)
lib/lp/code/model/gitrepository.py (+4/-0)
lib/lp/code/model/tests/test_gitrepository.py (+72/-4)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-mail-fix-delta
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+257015@code.launchpad.net

Commit message

Allow modifying Git repository names; fix sending mail for repository deltas.

Description of the change

While Git repository subscriptions work now, there's no way to actually cause any mail to be sent due to various bugs elsewhere. This makes it possible to change repository names, and fixes a missing part of GitRepositoryDelta's interface that caused the mailer to crash. The whole assemblage is now tested.

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/adapters/gitrepository.py'
2--- lib/lp/code/adapters/gitrepository.py 2015-04-16 14:35:17 +0000
3+++ lib/lp/code/adapters/gitrepository.py 2015-04-21 23:25:53 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
6+# Copyright 2015 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Components related to Git repositories."""
10@@ -22,16 +22,18 @@
11
12 implements(IGitRepositoryDelta)
13
14- delta_values = ('name', 'identity')
15+ delta_values = ('name', 'git_identity')
16+
17+ new_values = ()
18
19 interface = IGitRepository
20
21- def __init__(self, repository, user, name=None, identity=None):
22+ def __init__(self, repository, user, name=None, git_identity=None):
23 self.repository = repository
24 self.user = user
25
26 self.name = name
27- self.identity = identity
28+ self.git_identity = git_identity
29
30 @classmethod
31 def construct(klass, old_repository, new_repository, user):
32
33=== added file 'lib/lp/code/adapters/tests/test_gitrepository.py'
34--- lib/lp/code/adapters/tests/test_gitrepository.py 1970-01-01 00:00:00 +0000
35+++ lib/lp/code/adapters/tests/test_gitrepository.py 2015-04-21 23:25:53 +0000
36@@ -0,0 +1,53 @@
37+# Copyright 2015 Canonical Ltd. This software is licensed under the
38+# GNU Affero General Public License version 3 (see the file LICENSE).
39+
40+from lazr.lifecycle.snapshot import Snapshot
41+from testtools.matchers import MatchesStructure
42+from zope.interface import providedBy
43+
44+from lp.code.adapters.gitrepository import GitRepositoryDelta
45+from lp.code.interfaces.gitrepository import GIT_FEATURE_FLAG
46+from lp.services.features.testing import FeatureFixture
47+from lp.testing import (
48+ person_logged_in,
49+ TestCaseWithFactory,
50+ )
51+from lp.testing.layers import LaunchpadFunctionalLayer
52+
53+
54+class TestGitRepositoryDelta(TestCaseWithFactory):
55+
56+ layer = LaunchpadFunctionalLayer
57+
58+ def setUp(self):
59+ super(TestGitRepositoryDelta, self).setUp()
60+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
61+
62+ def test_no_modification(self):
63+ # If there are no modifications, no delta is returned.
64+ repository = self.factory.makeGitRepository(name=u"foo")
65+ old_repository = Snapshot(repository, providing=providedBy(repository))
66+ delta = GitRepositoryDelta.construct(
67+ old_repository, repository, repository.owner)
68+ self.assertIsNone(delta)
69+
70+ def test_modification(self):
71+ # If there are modifications, the delta reflects them.
72+ owner = self.factory.makePerson(name="person")
73+ project = self.factory.makeProduct(name="project")
74+ repository = self.factory.makeGitRepository(
75+ owner=owner, target=project, name=u"foo")
76+ old_repository = Snapshot(repository, providing=providedBy(repository))
77+ with person_logged_in(repository.owner):
78+ repository.setName(u"bar", repository.owner)
79+ delta = GitRepositoryDelta.construct(old_repository, repository, owner)
80+ self.assertIsNotNone(delta)
81+ self.assertThat(delta, MatchesStructure.byEquality(
82+ name={
83+ "old": u"foo",
84+ "new": u"bar",
85+ },
86+ git_identity={
87+ "old": u"lp:~person/project/+git/foo",
88+ "new": u"lp:~person/project/+git/bar",
89+ }))
90
91=== modified file 'lib/lp/code/interfaces/gitrepository.py'
92--- lib/lp/code/interfaces/gitrepository.py 2015-04-17 00:01:15 +0000
93+++ lib/lp/code/interfaces/gitrepository.py 2015-04-21 23:25:53 +0000
94@@ -200,9 +200,9 @@
95 def isPersonTrustedReviewer(reviewer):
96 """Return true if the `reviewer` is a trusted reviewer.
97
98- The reviewer is trusted if they either own the repository, or are in the
99- team that owns the repository, or they are in the review team for the
100- repository.
101+ The reviewer is trusted if they either own the repository, or are in
102+ the team that owns the repository, or they are in the review team
103+ for the repository.
104 """
105
106 git_identity = exported(Text(
107@@ -490,6 +490,15 @@
108 class IGitRepositoryEdit(Interface):
109 """IGitRepository methods that require launchpad.Edit permission."""
110
111+ @mutator_for(IGitRepositoryView["name"])
112+ @call_with(user=REQUEST_USER)
113+ @operation_parameters(
114+ new_name=TextLine(title=_("The new name of the repository.")))
115+ @export_write_operation()
116+ @operation_for_version("devel")
117+ def setName(new_name, user):
118+ """Set the name of the repository to be `new_name`."""
119+
120 @mutator_for(IGitRepositoryView["owner"])
121 @call_with(user=REQUEST_USER)
122 @operation_parameters(
123
124=== modified file 'lib/lp/code/model/gitnamespace.py'
125--- lib/lp/code/model/gitnamespace.py 2015-04-16 23:56:21 +0000
126+++ lib/lp/code/model/gitnamespace.py 2015-04-21 23:25:53 +0000
127@@ -152,9 +152,10 @@
128 def moveRepository(self, repository, mover, new_name=None,
129 rename_if_necessary=False):
130 """See `IGitNamespace`."""
131- # Check to see if the repository is already in this namespace.
132+ # Check to see if the repository is already in this namespace with
133+ # this name.
134 old_namespace = repository.namespace
135- if self.name == old_namespace.name:
136+ if self.name == old_namespace.name and new_name is None:
137 return
138 if new_name is None:
139 new_name = repository.name
140@@ -164,10 +165,13 @@
141 # Remove the security proxy of the repository as the owner and
142 # target attributes are read-only through the interface.
143 naked_repository = removeSecurityProxy(repository)
144- naked_repository.owner = self.owner
145- self._retargetRepository(naked_repository)
146- del get_property_cache(naked_repository).target
147- naked_repository.name = new_name
148+ if self.owner != repository.owner:
149+ naked_repository.owner = self.owner
150+ if self.target != repository.target:
151+ self._retargetRepository(naked_repository)
152+ del get_property_cache(naked_repository).target
153+ if new_name != repository.name:
154+ naked_repository.name = new_name
155
156 def getRepositories(self):
157 """See `IGitNamespace`."""
158
159=== modified file 'lib/lp/code/model/gitrepository.py'
160--- lib/lp/code/model/gitrepository.py 2015-04-21 11:39:09 +0000
161+++ lib/lp/code/model/gitrepository.py 2015-04-21 23:25:53 +0000
162@@ -622,6 +622,10 @@
163 # subscriptions.
164 getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])
165
166+ def setName(self, new_name, user):
167+ """See `IGitRepository`."""
168+ self.namespace.moveRepository(self, user, new_name=new_name)
169+
170 def setOwner(self, new_owner, user):
171 """See `IGitRepository`."""
172 new_namespace = get_git_namespace(self.target, new_owner)
173
174=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
175--- lib/lp/code/model/tests/test_gitrepository.py 2015-04-21 11:39:09 +0000
176+++ lib/lp/code/model/tests/test_gitrepository.py 2015-04-21 23:25:53 +0000
177@@ -6,11 +6,14 @@
178 __metaclass__ = type
179
180 from datetime import datetime
181+import email
182 from functools import partial
183 import hashlib
184 import json
185
186 from lazr.lifecycle.event import ObjectModifiedEvent
187+from lazr.lifecycle.snapshot import Snapshot
188+import transaction
189 import pytz
190 from testtools.matchers import (
191 EndsWith,
192@@ -19,6 +22,8 @@
193 )
194 from zope.component import getUtility
195 from zope.event import notify
196+from zope.interface import providedBy
197+from zope.security.interfaces import Unauthorized
198 from zope.security.proxy import removeSecurityProxy
199
200 from lp.app.enums import (
201@@ -37,6 +42,7 @@
202 GitFeatureDisabled,
203 GitRepositoryCreatorNotMemberOfOwnerTeam,
204 GitRepositoryCreatorNotOwner,
205+ GitRepositoryExists,
206 GitTargetError,
207 )
208 from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
209@@ -68,6 +74,7 @@
210 from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
211 from lp.services.database.constants import UTC_NOW
212 from lp.services.features.testing import FeatureFixture
213+from lp.services.mail import stub
214 from lp.services.webapp.authorization import check_permission
215 from lp.services.webapp.interfaces import OAuthPermission
216 from lp.testing import (
217@@ -288,16 +295,16 @@
218 repository.getRepositoryIdentities())
219
220
221-class TestGitRepositoryDateLastModified(TestCaseWithFactory):
222- """Exercise the situations where date_last_modified is updated."""
223+class TestGitRepositoryModifications(TestCaseWithFactory):
224+ """Tests for Git repository modification notifications."""
225
226 layer = DatabaseFunctionalLayer
227
228 def setUp(self):
229- super(TestGitRepositoryDateLastModified, self).setUp()
230+ super(TestGitRepositoryModifications, self).setUp()
231 self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
232
233- def test_initial_value(self):
234+ def test_date_last_modified_initial_value(self):
235 # The initial value of date_last_modified is date_created.
236 repository = self.factory.makeGitRepository()
237 self.assertEqual(
238@@ -314,6 +321,34 @@
239 self.assertSqlAttributeEqualsDate(
240 repository, "date_last_modified", UTC_NOW)
241
242+ def test_sends_notifications(self):
243+ # Attribute modifications send mail to subscribers.
244+ self.assertEqual(0, len(stub.test_emails))
245+ repository = self.factory.makeGitRepository(name=u"foo")
246+ repository_before_modification = Snapshot(
247+ repository, providing=providedBy(repository))
248+ with person_logged_in(repository.owner):
249+ repository.subscribe(
250+ repository.owner,
251+ BranchSubscriptionNotificationLevel.ATTRIBUTEONLY,
252+ BranchSubscriptionDiffSize.NODIFF,
253+ CodeReviewNotificationLevel.NOEMAIL,
254+ repository.owner)
255+ repository.setName(u"bar", repository.owner)
256+ notify(ObjectModifiedEvent(
257+ repository, repository_before_modification, ["name"],
258+ user=repository.owner))
259+ transaction.commit()
260+ self.assertEqual(1, len(stub.test_emails))
261+ message = email.message_from_string(stub.test_emails[0][2])
262+ body = message.get_payload(decode=True)
263+ self.assertIn("Name: foo => bar\n", body)
264+ self.assertIn(
265+ "Git identity: lp:~{person}/{project}/+git/foo => "
266+ "lp:~{person}/{project}/+git/bar\n".format(
267+ person=repository.owner.name, project=repository.target.name),
268+ body)
269+
270 # XXX cjwatson 2015-02-04: This will need to be expanded once Launchpad
271 # actually notices any interesting kind of repository modifications.
272
273@@ -892,6 +927,39 @@
274 self.assertNotTrustedReviewer(repository, reviewer)
275
276
277+class TestGitRepositorySetName(TestCaseWithFactory):
278+ """Test `IGitRepository.setName`."""
279+
280+ layer = DatabaseFunctionalLayer
281+
282+ def setUp(self):
283+ super(TestGitRepositorySetName, self).setUp()
284+ self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
285+
286+ def test_not_owner(self):
287+ # A non-owner non-admin user cannot rename a repository.
288+ repository = self.factory.makeGitRepository()
289+ with person_logged_in(self.factory.makePerson()):
290+ self.assertRaises(Unauthorized, getattr, repository, "setName")
291+
292+ def test_name_clash(self):
293+ # Name clashes are refused.
294+ repository = self.factory.makeGitRepository(name=u"foo")
295+ self.factory.makeGitRepository(
296+ owner=repository.owner, target=repository.target, name=u"bar")
297+ with person_logged_in(repository.owner):
298+ self.assertRaises(
299+ GitRepositoryExists, repository.setName,
300+ u"bar", repository.owner)
301+
302+ def test_rename(self):
303+ # A non-clashing rename request works.
304+ repository = self.factory.makeGitRepository(name=u"foo")
305+ with person_logged_in(repository.owner):
306+ repository.setName(u"bar", repository.owner)
307+ self.assertEqual(u"bar", repository.name)
308+
309+
310 class TestGitRepositorySetOwner(TestCaseWithFactory):
311 """Test `IGitRepository.setOwner`."""
312