Merge lp:~cjwatson/launchpad/git-defaults into lp:launchpad
- git-defaults
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | no longer in the source branch. |
Merged at revision: | 17363 |
Proposed branch: | lp:~cjwatson/launchpad/git-defaults |
Merge into: | lp:launchpad |
Diff against target: |
750 lines (+548/-39) 9 files modified
lib/lp/code/configure.zcml (+8/-1) lib/lp/code/errors.py (+2/-2) lib/lp/code/interfaces/defaultgit.py (+29/-0) lib/lp/code/interfaces/gitrepository.py (+65/-13) lib/lp/code/model/defaultgit.py (+113/-0) lib/lp/code/model/gitrepository.py (+74/-20) lib/lp/code/model/tests/test_gitrepository.py (+239/-3) lib/lp/registry/model/persondistributionsourcepackage.py (+9/-0) lib/lp/registry/model/personproduct.py (+9/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-defaults |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email:
|
Commit message
Implement target and owner-target default Git repositories.
Description of the change
Implement target and owner-target default Git repositories.
This presents two different layers because of the different ways we'll need to call this. For the webservice, it's simplest to just have {get,set}
I chose to have {get,set}
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
William Grant (wgrant) : | # |
Preview Diff
1 | === modified file 'lib/lp/code/configure.zcml' |
2 | --- lib/lp/code/configure.zcml 2015-02-19 23:57:34 +0000 |
3 | +++ lib/lp/code/configure.zcml 2015-02-26 14:40:55 +0000 |
4 | @@ -1,4 +1,4 @@ |
5 | -<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
6 | +<!-- Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
7 | GNU Affero General Public License version 3 (see the file LICENSE). |
8 | --> |
9 | |
10 | @@ -858,6 +858,13 @@ |
11 | <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" /> |
12 | </securedutility> |
13 | |
14 | + <!-- Default Git repositories --> |
15 | + |
16 | + <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" /> |
17 | + <adapter factory="lp.code.model.defaultgit.PackageDefaultGitRepository" /> |
18 | + <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" /> |
19 | + <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" /> |
20 | + |
21 | <lp:help-folder folder="help" name="+help-code" /> |
22 | |
23 | <!-- Diffs --> |
24 | |
25 | === modified file 'lib/lp/code/errors.py' |
26 | --- lib/lp/code/errors.py 2015-02-11 17:10:09 +0000 |
27 | +++ lib/lp/code/errors.py 2015-02-26 14:40:55 +0000 |
28 | @@ -1,4 +1,4 @@ |
29 | -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
30 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
31 | # GNU Affero General Public License version 3 (see the file LICENSE). |
32 | |
33 | """Errors used in the lp/code modules.""" |
34 | @@ -20,9 +20,9 @@ |
35 | 'BuildNotAllowedForDistro', |
36 | 'BranchMergeProposalExists', |
37 | 'CannotDeleteBranch', |
38 | + 'CannotHaveLinkedBranch', |
39 | 'CannotUpgradeBranch', |
40 | 'CannotUpgradeNonHosted', |
41 | - 'CannotHaveLinkedBranch', |
42 | 'CodeImportAlreadyRequested', |
43 | 'CodeImportAlreadyRunning', |
44 | 'CodeImportNotInReviewedState', |
45 | |
46 | === added file 'lib/lp/code/interfaces/defaultgit.py' |
47 | --- lib/lp/code/interfaces/defaultgit.py 1970-01-01 00:00:00 +0000 |
48 | +++ lib/lp/code/interfaces/defaultgit.py 2015-02-26 14:40:55 +0000 |
49 | @@ -0,0 +1,29 @@ |
50 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
51 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
52 | + |
53 | +"""Interface for objects that have a default Git repository. |
54 | + |
55 | +A default Git repository is a repository that's somehow officially related |
56 | +to an object. It might be a project, a distribution source package, or a |
57 | +combination of one of those with an owner to represent that owner's default |
58 | +repository for that target. |
59 | +""" |
60 | + |
61 | +__metaclass__ = type |
62 | +__all__ = [ |
63 | + 'ICanHasDefaultGitRepository', |
64 | + ] |
65 | + |
66 | +from zope.interface import ( |
67 | + Attribute, |
68 | + Interface, |
69 | + ) |
70 | + |
71 | + |
72 | +class ICanHasDefaultGitRepository(Interface): |
73 | + """Something that has a default Git repository.""" |
74 | + |
75 | + context = Attribute("The object that can have a default Git repository.") |
76 | + path = Attribute( |
77 | + "The path for the default Git repository. " |
78 | + "Note that this will be set even if there is no default repository.") |
79 | |
80 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
81 | --- lib/lp/code/interfaces/gitrepository.py 2015-02-19 23:57:34 +0000 |
82 | +++ lib/lp/code/interfaces/gitrepository.py 2015-02-26 14:40:55 +0000 |
83 | @@ -16,6 +16,7 @@ |
84 | import re |
85 | |
86 | from lazr.restful.fields import Reference |
87 | +from zope.component import getUtility |
88 | from zope.interface import ( |
89 | Attribute, |
90 | Interface, |
91 | @@ -32,7 +33,16 @@ |
92 | from lp import _ |
93 | from lp.app.enums import InformationType |
94 | from lp.app.validators import LaunchpadValidationError |
95 | +from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
96 | from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
97 | +from lp.registry.interfaces.distributionsourcepackage import ( |
98 | + IDistributionSourcePackage, |
99 | + ) |
100 | +from lp.registry.interfaces.persondistributionsourcepackage import ( |
101 | + IPersonDistributionSourcePackageFactory, |
102 | + ) |
103 | +from lp.registry.interfaces.personproduct import IPersonProductFactory |
104 | +from lp.registry.interfaces.product import IProduct |
105 | from lp.registry.interfaces.role import IPersonRoles |
106 | from lp.services.fields import ( |
107 | PersonChoice, |
108 | @@ -309,16 +319,44 @@ |
109 | Return None if no match was found. |
110 | """ |
111 | |
112 | - def getDefaultRepository(target, owner=None): |
113 | - """Get the default repository for a target or owner-target. |
114 | - |
115 | - :param target: An `IHasGitRepositories`. |
116 | - :param owner: An `IPerson`, in which case search for that person's |
117 | - default repository for this target; or None, in which case |
118 | - search for the overall default repository for this target. |
119 | - |
120 | - :raises GitTargetError: if `target` is an `IPerson`. |
121 | - :return: An `IGitRepository`, or None. |
122 | + def getDefaultRepository(target): |
123 | + """Get the default repository for a target. |
124 | + |
125 | + :param target: An `IHasGitRepositories`. |
126 | + |
127 | + :raises GitTargetError: if `target` is an `IPerson`. |
128 | + :return: An `IGitRepository`, or None. |
129 | + """ |
130 | + |
131 | + def getDefaultRepositoryForOwner(owner, target): |
132 | + """Get a person's default repository for a target. |
133 | + |
134 | + :param owner: An `IPerson`. |
135 | + :param target: An `IHasGitRepositories`. |
136 | + |
137 | + :raises GitTargetError: if `target` is an `IPerson`. |
138 | + :return: An `IGitRepository`, or None. |
139 | + """ |
140 | + |
141 | + def setDefaultRepository(target, repository): |
142 | + """Set the default repository for a target. |
143 | + |
144 | + :param target: An `IHasGitRepositories`. |
145 | + :param repository: An `IGitRepository`, or None to unset the default |
146 | + repository. |
147 | + |
148 | + :raises GitTargetError: if `target` is an `IPerson`. |
149 | + """ |
150 | + |
151 | + def setDefaultRepositoryForOwner(owner, target, repository): |
152 | + """Set a person's default repository for a target. |
153 | + |
154 | + :param owner: An `IPerson`. |
155 | + :param target: An `IHasGitRepositories`. |
156 | + :param repository: An `IGitRepository`, or None to unset the default |
157 | + repository. |
158 | + |
159 | + :raises GitTargetError: if `target` is an `IPerson`. |
160 | """ |
161 | |
162 | def getRepositories(): |
163 | @@ -349,9 +387,23 @@ |
164 | |
165 | def getRepositoryDefaults(self): |
166 | """See `IGitRepository`.""" |
167 | - # XXX cjwatson 2015-02-06: This will return shortcut defaults once |
168 | - # they're implemented. |
169 | - return [] |
170 | + defaults = [] |
171 | + if self.target_default: |
172 | + defaults.append(ICanHasDefaultGitRepository(self.target)) |
173 | + if self.owner_default: |
174 | + if IProduct.providedBy(self.target): |
175 | + factory = getUtility(IPersonProductFactory) |
176 | + default = factory.create(self.owner, self.target) |
177 | + elif IDistributionSourcePackage.providedBy(self.target): |
178 | + factory = getUtility(IPersonDistributionSourcePackageFactory) |
179 | + default = factory.create(self.owner, self.target) |
180 | + else: |
181 | + # Also enforced by database constraint. |
182 | + raise AssertionError( |
183 | + "Only projects or packages can have owner-target default " |
184 | + "repositories.") |
185 | + defaults.append(ICanHasDefaultGitRepository(default)) |
186 | + return sorted(defaults) |
187 | |
188 | def getRepositoryIdentities(self): |
189 | """See `IGitRepository`.""" |
190 | |
191 | === added file 'lib/lp/code/model/defaultgit.py' |
192 | --- lib/lp/code/model/defaultgit.py 1970-01-01 00:00:00 +0000 |
193 | +++ lib/lp/code/model/defaultgit.py 2015-02-26 14:40:55 +0000 |
194 | @@ -0,0 +1,113 @@ |
195 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
196 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
197 | + |
198 | +"""Implementation of `ICanHasDefaultGitRepository`.""" |
199 | + |
200 | +__metaclass__ = type |
201 | +# Don't export anything -- anything you need from this module you can get by |
202 | +# adapting another object. |
203 | +__all__ = [] |
204 | + |
205 | +from zope.component import adapts |
206 | +from zope.interface import implements |
207 | + |
208 | +from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
209 | +from lp.registry.interfaces.distributionsourcepackage import ( |
210 | + IDistributionSourcePackage, |
211 | + ) |
212 | +from lp.registry.interfaces.persondistributionsourcepackage import ( |
213 | + IPersonDistributionSourcePackage, |
214 | + ) |
215 | +from lp.registry.interfaces.personproduct import IPersonProduct |
216 | +from lp.registry.interfaces.product import IProduct |
217 | + |
218 | + |
219 | +class BaseDefaultGitRepository: |
220 | + """Provides the common sorting algorithm.""" |
221 | + |
222 | + def __cmp__(self, other): |
223 | + if not ICanHasDefaultGitRepository.providedBy(other): |
224 | + raise AssertionError("Can't compare with: %r" % other) |
225 | + return cmp(self.sort_order, other.sort_order) |
226 | + |
227 | + def __eq__(self, other): |
228 | + return ( |
229 | + isinstance(other, self.__class__) and |
230 | + self.context == other.context) |
231 | + |
232 | + def __ne__(self, other): |
233 | + return not self == other |
234 | + |
235 | + |
236 | +class ProjectDefaultGitRepository(BaseDefaultGitRepository): |
237 | + """Implement a default Git repository for a project.""" |
238 | + |
239 | + adapts(IProduct) |
240 | + implements(ICanHasDefaultGitRepository) |
241 | + |
242 | + sort_order = 0 |
243 | + |
244 | + def __init__(self, project): |
245 | + self.context = project |
246 | + |
247 | + @property |
248 | + def path(self): |
249 | + """See `ICanHasDefaultGitRepository`.""" |
250 | + return self.context.name |
251 | + |
252 | + |
253 | +class PackageDefaultGitRepository(BaseDefaultGitRepository): |
254 | + """Implement a default Git repository for a distribution source package.""" |
255 | + |
256 | + adapts(IDistributionSourcePackage) |
257 | + implements(ICanHasDefaultGitRepository) |
258 | + |
259 | + sort_order = 0 |
260 | + |
261 | + def __init__(self, distro_source_package): |
262 | + self.context = distro_source_package |
263 | + |
264 | + @property |
265 | + def path(self): |
266 | + """See `ICanHasDefaultGitRepository`.""" |
267 | + return "%s/+source/%s" % ( |
268 | + self.context.distribution.name, |
269 | + self.context.sourcepackagename.name) |
270 | + |
271 | + |
272 | +class OwnerProjectDefaultGitRepository(BaseDefaultGitRepository): |
273 | + """Implement an owner's default Git repository for a project.""" |
274 | + |
275 | + adapts(IPersonProduct) |
276 | + implements(ICanHasDefaultGitRepository) |
277 | + |
278 | + sort_order = 1 |
279 | + |
280 | + def __init__(self, person_project): |
281 | + self.context = person_project |
282 | + |
283 | + @property |
284 | + def path(self): |
285 | + """See `ICanHasDefaultGitRepository`.""" |
286 | + return "~%s/%s" % (self.context.person.name, self.context.product.name) |
287 | + |
288 | + |
289 | +class OwnerPackageDefaultGitRepository(BaseDefaultGitRepository): |
290 | + """Implement an owner's default Git repository for a distribution source |
291 | + package.""" |
292 | + |
293 | + adapts(IPersonDistributionSourcePackage) |
294 | + implements(ICanHasDefaultGitRepository) |
295 | + |
296 | + sort_order = 1 |
297 | + |
298 | + def __init__(self, person_distro_source_package): |
299 | + self.context = person_distro_source_package |
300 | + |
301 | + @property |
302 | + def path(self): |
303 | + """See `ICanHasDefaultGitRepository`.""" |
304 | + dsp = self.context.distro_source_package |
305 | + return "~%s/%s/+source/%s" % ( |
306 | + self.context.person.name, dsp.distribution.name, |
307 | + dsp.sourcepackagename.name) |
308 | |
309 | === modified file 'lib/lp/code/model/gitrepository.py' |
310 | --- lib/lp/code/model/gitrepository.py 2015-02-19 23:57:34 +0000 |
311 | +++ lib/lp/code/model/gitrepository.py 2015-02-26 14:40:55 +0000 |
312 | @@ -83,6 +83,7 @@ |
313 | ArrayIntersects, |
314 | ) |
315 | from lp.services.propertycache import cachedproperty |
316 | +from lp.services.webapp.authorization import available_with_permission |
317 | |
318 | |
319 | def git_repository_modified(repository, event): |
320 | @@ -206,8 +207,9 @@ |
321 | """See `IGitRepository`.""" |
322 | if value: |
323 | # Check for an existing owner-target default. |
324 | - existing = getUtility(IGitRepositorySet).getDefaultRepository( |
325 | - self.target, owner=self.owner) |
326 | + repository_set = getUtility(IGitRepositorySet) |
327 | + existing = repository_set.getDefaultRepositoryForOwner( |
328 | + self.owner, self.target) |
329 | if existing is not None: |
330 | raise GitDefaultConflict( |
331 | existing, self.target, owner=self.owner) |
332 | @@ -358,24 +360,76 @@ |
333 | # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place. |
334 | raise NotImplementedError |
335 | |
336 | - def getDefaultRepository(self, target, owner=None): |
337 | - """See `IGitRepositorySet`.""" |
338 | - clauses = [] |
339 | - if IProduct.providedBy(target): |
340 | - clauses.append(GitRepository.project == target) |
341 | - elif IDistributionSourcePackage.providedBy(target): |
342 | - clauses.append(GitRepository.distribution == target.distribution) |
343 | - clauses.append( |
344 | - GitRepository.sourcepackagename == target.sourcepackagename) |
345 | - else: |
346 | - raise GitTargetError( |
347 | - "Personal repositories cannot be defaults for any target.") |
348 | - if owner is not None: |
349 | - clauses.append(GitRepository.owner == owner) |
350 | - clauses.append(GitRepository.owner_default == True) |
351 | - else: |
352 | - clauses.append(GitRepository.target_default == True) |
353 | - return IStore(GitRepository).find(GitRepository, *clauses).one() |
354 | + def getDefaultRepository(self, target): |
355 | + """See `IGitRepositorySet`.""" |
356 | + clauses = [GitRepository.target_default == True] |
357 | + if IProduct.providedBy(target): |
358 | + clauses.append(GitRepository.project == target) |
359 | + elif IDistributionSourcePackage.providedBy(target): |
360 | + clauses.append(GitRepository.distribution == target.distribution) |
361 | + clauses.append( |
362 | + GitRepository.sourcepackagename == target.sourcepackagename) |
363 | + else: |
364 | + raise GitTargetError( |
365 | + "Personal repositories cannot be defaults for any target.") |
366 | + return IStore(GitRepository).find(GitRepository, *clauses).one() |
367 | + |
368 | + def getDefaultRepositoryForOwner(self, owner, target): |
369 | + """See `IGitRepositorySet`.""" |
370 | + clauses = [ |
371 | + GitRepository.owner == owner, |
372 | + GitRepository.owner_default == True, |
373 | + ] |
374 | + if IProduct.providedBy(target): |
375 | + clauses.append(GitRepository.project == target) |
376 | + elif IDistributionSourcePackage.providedBy(target): |
377 | + clauses.append(GitRepository.distribution == target.distribution) |
378 | + clauses.append( |
379 | + GitRepository.sourcepackagename == target.sourcepackagename) |
380 | + else: |
381 | + raise GitTargetError( |
382 | + "Personal repositories cannot be defaults for any target.") |
383 | + return IStore(GitRepository).find(GitRepository, *clauses).one() |
384 | + |
385 | + @available_with_permission('launchpad.Edit', 'target') |
386 | + def setDefaultRepository(self, target, repository): |
387 | + """See `IGitRepositorySet`.""" |
388 | + if IPerson.providedBy(target): |
389 | + raise GitTargetError( |
390 | + "Cannot set a default Git repository for a person, only " |
391 | + "for a project or a package.") |
392 | + if repository is not None: |
393 | + if repository.target != target: |
394 | + raise GitTargetError( |
395 | + "Cannot set default Git repository to one attached to " |
396 | + "another target.") |
397 | + repository.setTargetDefault(True) |
398 | + else: |
399 | + previous = self.getDefaultRepository(target) |
400 | + if previous is not None: |
401 | + previous.setTargetDefault(False) |
402 | + |
403 | + @available_with_permission('launchpad.Edit', 'owner') |
404 | + def setDefaultRepositoryForOwner(self, owner, target, repository): |
405 | + """See `IGitRepositorySet`.""" |
406 | + if IPerson.providedBy(target): |
407 | + raise GitTargetError( |
408 | + "Cannot set a default Git repository for a person, only " |
409 | + "for a project or a package.") |
410 | + if repository is not None: |
411 | + if repository.target != target: |
412 | + raise GitTargetError( |
413 | + "Cannot set default Git repository to one attached to " |
414 | + "another target.") |
415 | + if repository.owner != owner: |
416 | + raise GitTargetError( |
417 | + "Cannot set a person's default Git repository to one " |
418 | + "owned by somebody else.") |
419 | + repository.setOwnerDefault(True) |
420 | + else: |
421 | + previous = self.getDefaultRepositoryForOwner(owner, target) |
422 | + if previous is not None: |
423 | + previous.setOwnerDefault(False) |
424 | |
425 | def getRepositories(self): |
426 | """See `IGitRepositorySet`.""" |
427 | |
428 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
429 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-02-20 00:56:57 +0000 |
430 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-26 14:40:55 +0000 |
431 | @@ -6,6 +6,7 @@ |
432 | __metaclass__ = type |
433 | |
434 | from datetime import datetime |
435 | +from functools import partial |
436 | |
437 | from lazr.lifecycle.event import ObjectModifiedEvent |
438 | import pytz |
439 | @@ -24,12 +25,20 @@ |
440 | GitRepositoryCreatorNotOwner, |
441 | GitTargetError, |
442 | ) |
443 | +from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository |
444 | from lp.code.interfaces.gitnamespace import ( |
445 | IGitNamespacePolicy, |
446 | IGitNamespaceSet, |
447 | ) |
448 | -from lp.code.interfaces.gitrepository import IGitRepository |
449 | +from lp.code.interfaces.gitrepository import ( |
450 | + IGitRepository, |
451 | + IGitRepositorySet, |
452 | + ) |
453 | from lp.registry.enums import BranchSharingPolicy |
454 | +from lp.registry.interfaces.persondistributionsourcepackage import ( |
455 | + IPersonDistributionSourcePackageFactory, |
456 | + ) |
457 | +from lp.registry.interfaces.personproduct import IPersonProductFactory |
458 | from lp.services.database.constants import UTC_NOW |
459 | from lp.services.webapp.authorization import check_permission |
460 | from lp.testing import ( |
461 | @@ -96,6 +105,10 @@ |
462 | |
463 | layer = DatabaseFunctionalLayer |
464 | |
465 | + def setUp(self): |
466 | + super(TestGitIdentityMixin, self).setUp() |
467 | + self.repository_set = getUtility(IGitRepositorySet) |
468 | + |
469 | def assertGitIdentity(self, repository, identity_path): |
470 | """Assert that the Git identity of 'repository' is 'identity_path'. |
471 | |
472 | @@ -111,6 +124,53 @@ |
473 | repository = self.factory.makeGitRepository() |
474 | self.assertGitIdentity(repository, repository.unique_name) |
475 | |
476 | + def test_git_identity_default_for_project(self): |
477 | + # If a repository is the default for a project, then its Git |
478 | + # identity is the project name. |
479 | + project = self.factory.makeProduct() |
480 | + repository = self.factory.makeGitRepository( |
481 | + owner=project.owner, target=project) |
482 | + with person_logged_in(project.owner): |
483 | + self.repository_set.setDefaultRepository(project, repository) |
484 | + self.assertGitIdentity(repository, project.name) |
485 | + |
486 | + def test_git_identity_default_for_package(self): |
487 | + # If a repository is the default for a package, then its Git |
488 | + # identity uses the path to that package. |
489 | + dsp = self.factory.makeDistributionSourcePackage() |
490 | + repository = self.factory.makeGitRepository(target=dsp) |
491 | + with admin_logged_in(): |
492 | + self.repository_set.setDefaultRepository(dsp, repository) |
493 | + self.assertGitIdentity( |
494 | + repository, |
495 | + "%s/+source/%s" % ( |
496 | + dsp.distribution.name, dsp.sourcepackagename.name)) |
497 | + |
498 | + def test_git_identity_owner_default_for_project(self): |
499 | + # If a repository is a person's default for a project, then its Git |
500 | + # identity is a combination of the person and project names. |
501 | + project = self.factory.makeProduct() |
502 | + repository = self.factory.makeGitRepository(target=project) |
503 | + with person_logged_in(repository.owner): |
504 | + self.repository_set.setDefaultRepositoryForOwner( |
505 | + repository.owner, project, repository) |
506 | + self.assertGitIdentity( |
507 | + repository, "~%s/%s" % (repository.owner.name, project.name)) |
508 | + |
509 | + def test_git_identity_owner_default_for_package(self): |
510 | + # If a repository is a person's default for a package, then its Git |
511 | + # identity is a combination of the person name and the package path. |
512 | + dsp = self.factory.makeDistributionSourcePackage() |
513 | + repository = self.factory.makeGitRepository(target=dsp) |
514 | + with person_logged_in(repository.owner): |
515 | + self.repository_set.setDefaultRepositoryForOwner( |
516 | + repository.owner, dsp, repository) |
517 | + self.assertGitIdentity( |
518 | + repository, |
519 | + "~%s/%s/+source/%s" % ( |
520 | + repository.owner.name, dsp.distribution.name, |
521 | + dsp.sourcepackagename.name)) |
522 | + |
523 | def test_identities_no_defaults(self): |
524 | # If there are no defaults, the only repository identity is the |
525 | # unique name. |
526 | @@ -119,8 +179,54 @@ |
527 | [(repository.unique_name, repository)], |
528 | repository.getRepositoryIdentities()) |
529 | |
530 | - # XXX cjwatson 2015-02-12: This will need to be expanded once support |
531 | - # for default repositories is in place. |
532 | + def test_default_for_project(self): |
533 | + # If a repository is the default for a project, then that is the |
534 | + # preferred identity. Target defaults are preferred over |
535 | + # owner-target defaults. |
536 | + eric = self.factory.makePerson(name="eric") |
537 | + fooix = self.factory.makeProduct(name="fooix", owner=eric) |
538 | + repository = self.factory.makeGitRepository( |
539 | + owner=eric, target=fooix, name=u"fooix-repo") |
540 | + with person_logged_in(fooix.owner): |
541 | + self.repository_set.setDefaultRepositoryForOwner( |
542 | + repository.owner, fooix, repository) |
543 | + self.repository_set.setDefaultRepository(fooix, repository) |
544 | + eric_fooix = getUtility(IPersonProductFactory).create(eric, fooix) |
545 | + self.assertEqual( |
546 | + [ICanHasDefaultGitRepository(target) |
547 | + for target in (fooix, eric_fooix)], |
548 | + repository.getRepositoryDefaults()) |
549 | + self.assertEqual( |
550 | + [("fooix", fooix), ("~eric/fooix", eric_fooix), |
551 | + ("~eric/fooix/+git/fooix-repo", repository)], |
552 | + repository.getRepositoryIdentities()) |
553 | + |
554 | + def test_default_for_package(self): |
555 | + # If a repository is the default for a package, then that is the |
556 | + # preferred identity. Target defaults are preferred over |
557 | + # owner-target defaults. |
558 | + mint = self.factory.makeDistribution(name="mint") |
559 | + eric = self.factory.makePerson(name="eric") |
560 | + mint_choc = self.factory.makeDistributionSourcePackage( |
561 | + distribution=mint, sourcepackagename="choc") |
562 | + repository = self.factory.makeGitRepository( |
563 | + owner=eric, target=mint_choc, name=u"choc-repo") |
564 | + dsp = repository.target |
565 | + with admin_logged_in(): |
566 | + self.repository_set.setDefaultRepositoryForOwner( |
567 | + repository.owner, dsp, repository) |
568 | + self.repository_set.setDefaultRepository(dsp, repository) |
569 | + eric_dsp = getUtility(IPersonDistributionSourcePackageFactory).create( |
570 | + eric, dsp) |
571 | + self.assertEqual( |
572 | + [ICanHasDefaultGitRepository(target) |
573 | + for target in (dsp, eric_dsp)], |
574 | + repository.getRepositoryDefaults()) |
575 | + self.assertEqual( |
576 | + [("mint/+source/choc", dsp), |
577 | + ("~eric/mint/+source/choc", eric_dsp), |
578 | + ("~eric/mint/+source/choc/+git/choc-repo", repository)], |
579 | + repository.getRepositoryIdentities()) |
580 | |
581 | |
582 | class TestGitRepositoryDateLastModified(TestCaseWithFactory): |
583 | @@ -392,3 +498,133 @@ |
584 | self.assertRaises( |
585 | GitTargetError, repository.setTarget, |
586 | target=commercial_project, user=owner) |
587 | + |
588 | + |
589 | +class TestGitRepositorySet(TestCaseWithFactory): |
590 | + |
591 | + layer = DatabaseFunctionalLayer |
592 | + |
593 | + def setUp(self): |
594 | + super(TestGitRepositorySet, self).setUp() |
595 | + self.repository_set = getUtility(IGitRepositorySet) |
596 | + |
597 | + def test_provides_IGitRepositorySet(self): |
598 | + # GitRepositorySet instances provide IGitRepositorySet. |
599 | + verifyObject(IGitRepositorySet, self.repository_set) |
600 | + |
601 | + def test_setDefaultRepository_refuses_person(self): |
602 | + # setDefaultRepository refuses if the target is a person. |
603 | + person = self.factory.makePerson() |
604 | + repository = self.factory.makeGitRepository(owner=person) |
605 | + with person_logged_in(person): |
606 | + self.assertRaises( |
607 | + GitTargetError, self.repository_set.setDefaultRepository, |
608 | + person, repository) |
609 | + |
610 | + def test_setDefaultRepositoryForOwner_refuses_person(self): |
611 | + # setDefaultRepositoryForOwner refuses if the target is a person. |
612 | + person = self.factory.makePerson() |
613 | + repository = self.factory.makeGitRepository(owner=person) |
614 | + with person_logged_in(person): |
615 | + self.assertRaises( |
616 | + GitTargetError, |
617 | + self.repository_set.setDefaultRepositoryForOwner, |
618 | + person, person, repository) |
619 | + |
620 | + |
621 | +class TestGitRepositorySetDefaultsMixin: |
622 | + |
623 | + layer = DatabaseFunctionalLayer |
624 | + |
625 | + def setUp(self): |
626 | + super(TestGitRepositorySetDefaultsMixin, self).setUp() |
627 | + self.repository_set = getUtility(IGitRepositorySet) |
628 | + self.get_method = self.repository_set.getDefaultRepository |
629 | + self.set_method = self.repository_set.setDefaultRepository |
630 | + |
631 | + def makeGitRepository(self, target): |
632 | + return self.factory.makeGitRepository(target=target) |
633 | + |
634 | + def test_default_repository_round_trip(self): |
635 | + # A target's default Git repository set using setDefaultRepository* |
636 | + # can be retrieved using getDefaultRepository*. |
637 | + target = self.makeTarget() |
638 | + repository = self.makeGitRepository(target) |
639 | + self.assertIsNone(self.get_method(target)) |
640 | + with person_logged_in(self.getPersonForLogin(target)): |
641 | + self.set_method(target, repository) |
642 | + self.assertEqual(repository, self.get_method(target)) |
643 | + |
644 | + def test_set_default_repository_None(self): |
645 | + # setDefaultRepository*(target, None) clears the default. |
646 | + target = self.makeTarget() |
647 | + repository = self.makeGitRepository(target) |
648 | + with person_logged_in(self.getPersonForLogin(target)): |
649 | + self.set_method(target, repository) |
650 | + self.set_method(target, None) |
651 | + self.assertIsNone(self.get_method(target)) |
652 | + |
653 | + def test_set_default_repository_different_target(self): |
654 | + # setDefaultRepository* refuses if the repository is attached to a |
655 | + # different target. |
656 | + target = self.makeTarget() |
657 | + other_target = self.makeTarget(template=target) |
658 | + repository = self.makeGitRepository(other_target) |
659 | + with person_logged_in(self.getPersonForLogin(target)): |
660 | + self.assertRaises( |
661 | + GitTargetError, self.set_method, target, repository) |
662 | + |
663 | + |
664 | +class TestGitRepositorySetDefaultsProject( |
665 | + TestGitRepositorySetDefaultsMixin, TestCaseWithFactory): |
666 | + |
667 | + def makeTarget(self, template=None): |
668 | + return self.factory.makeProduct() |
669 | + |
670 | + @staticmethod |
671 | + def getPersonForLogin(target): |
672 | + return target.owner |
673 | + |
674 | + |
675 | +class TestGitRepositorySetDefaultsPackage( |
676 | + TestGitRepositorySetDefaultsMixin, TestCaseWithFactory): |
677 | + |
678 | + def makeTarget(self, template=None): |
679 | + kwargs = {} |
680 | + if template is not None: |
681 | + kwargs["distribution"] = template.distribution |
682 | + return self.factory.makeDistributionSourcePackage(**kwargs) |
683 | + |
684 | + @staticmethod |
685 | + def getPersonForLogin(target): |
686 | + return target.distribution.owner |
687 | + |
688 | + |
689 | +class TestGitRepositorySetDefaultsOwnerMixin( |
690 | + TestGitRepositorySetDefaultsMixin): |
691 | + |
692 | + def setUp(self): |
693 | + super(TestGitRepositorySetDefaultsOwnerMixin, self).setUp() |
694 | + self.person = self.factory.makePerson() |
695 | + self.get_method = partial( |
696 | + self.repository_set.getDefaultRepositoryForOwner, self.person) |
697 | + self.set_method = partial( |
698 | + self.repository_set.setDefaultRepositoryForOwner, self.person) |
699 | + |
700 | + def makeGitRepository(self, target): |
701 | + return self.factory.makeGitRepository(owner=self.person, target=target) |
702 | + |
703 | + def getPersonForLogin(self, target): |
704 | + return self.person |
705 | + |
706 | + |
707 | +class TestGitRepositorySetDefaultsOwnerProject( |
708 | + TestGitRepositorySetDefaultsOwnerMixin, |
709 | + TestGitRepositorySetDefaultsProject): |
710 | + pass |
711 | + |
712 | + |
713 | +class TestGitRepositorySetDefaultsOwnerPackage( |
714 | + TestGitRepositorySetDefaultsOwnerMixin, |
715 | + TestGitRepositorySetDefaultsPackage): |
716 | + pass |
717 | |
718 | === modified file 'lib/lp/registry/model/persondistributionsourcepackage.py' |
719 | --- lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-06 13:37:58 +0000 |
720 | +++ lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-26 14:40:55 +0000 |
721 | @@ -37,3 +37,12 @@ |
722 | def displayname(self): |
723 | return '%s in %s' % ( |
724 | self.person.displayname, self.distro_source_package.displayname) |
725 | + |
726 | + def __eq__(self, other): |
727 | + return ( |
728 | + IPersonDistributionSourcePackage.providedBy(other) and |
729 | + self.person.id == other.person.id and |
730 | + self.distro_source_package == other.distro_source_package) |
731 | + |
732 | + def __ne__(self, other): |
733 | + return not self == other |
734 | |
735 | === modified file 'lib/lp/registry/model/personproduct.py' |
736 | --- lib/lp/registry/model/personproduct.py 2011-12-19 23:38:16 +0000 |
737 | +++ lib/lp/registry/model/personproduct.py 2015-02-26 14:40:55 +0000 |
738 | @@ -38,3 +38,12 @@ |
739 | def displayname(self): |
740 | return '%s in %s' % ( |
741 | self.person.displayname, self.product.displayname) |
742 | + |
743 | + def __eq__(self, other): |
744 | + return ( |
745 | + IPersonProduct.providedBy(other) and |
746 | + self.person.id == other.person.id and |
747 | + self.product.id == other.product.id) |
748 | + |
749 | + def __ne__(self, other): |
750 | + return not self == other |
I'm not sure about the target. getDefaultGitRe pository/ setDefaultGitRe pository design. They're yet more domain-specific methods on already huge Registry classes, which just isn't maintainable long-term.
During Disclosure we experimented with SharingService to stop the bloat, which ended up hideous more because of the different artifact types than because of a fundamental issue with the service concept.