Merge lp:~cjwatson/launchpad/git-defaults into lp:launchpad

Proposed by Colin Watson
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
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+250474@code.launchpad.net

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}DefaultRepository* methods on GitRepositorySet. But for some internal purposes, it's useful to have an ICanHasDefaultGitRepository abstraction that's sortable and so on, much like ICanHasLinkedBranch: this is useful when working out Git identities.

I chose to have {get,set}DefaultRepository and {get,set}DefaultRepositoryForOwner be separate methods rather than taking an optional owner keyword argument, because I felt that the optional argument causing completely different semantics was a little too easy to get wrong for webservice users. It's marginally more awkward internally but not too bad.

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

I'm not sure about the target.getDefaultGitRepository/setDefaultGitRepository 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.

review: Needs Fixing (code)
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
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml 2015-02-19 23:57:34 +0000
+++ lib/lp/code/configure.zcml 2015-02-26 14:40:55 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2009-2013 Canonical Ltd. This software is licensed under the1<!-- Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -858,6 +858,13 @@
858 <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />858 <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
859 </securedutility>859 </securedutility>
860860
861 <!-- Default Git repositories -->
862
863 <adapter factory="lp.code.model.defaultgit.ProjectDefaultGitRepository" />
864 <adapter factory="lp.code.model.defaultgit.PackageDefaultGitRepository" />
865 <adapter factory="lp.code.model.defaultgit.OwnerProjectDefaultGitRepository" />
866 <adapter factory="lp.code.model.defaultgit.OwnerPackageDefaultGitRepository" />
867
861 <lp:help-folder folder="help" name="+help-code" />868 <lp:help-folder folder="help" name="+help-code" />
862869
863 <!-- Diffs -->870 <!-- Diffs -->
864871
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py 2015-02-11 17:10:09 +0000
+++ lib/lp/code/errors.py 2015-02-26 14:40:55 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Errors used in the lp/code modules."""4"""Errors used in the lp/code modules."""
@@ -20,9 +20,9 @@
20 'BuildNotAllowedForDistro',20 'BuildNotAllowedForDistro',
21 'BranchMergeProposalExists',21 'BranchMergeProposalExists',
22 'CannotDeleteBranch',22 'CannotDeleteBranch',
23 'CannotHaveLinkedBranch',
23 'CannotUpgradeBranch',24 'CannotUpgradeBranch',
24 'CannotUpgradeNonHosted',25 'CannotUpgradeNonHosted',
25 'CannotHaveLinkedBranch',
26 'CodeImportAlreadyRequested',26 'CodeImportAlreadyRequested',
27 'CodeImportAlreadyRunning',27 'CodeImportAlreadyRunning',
28 'CodeImportNotInReviewedState',28 'CodeImportNotInReviewedState',
2929
=== added file 'lib/lp/code/interfaces/defaultgit.py'
--- lib/lp/code/interfaces/defaultgit.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/interfaces/defaultgit.py 2015-02-26 14:40:55 +0000
@@ -0,0 +1,29 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Interface for objects that have a default Git repository.
5
6A default Git repository is a repository that's somehow officially related
7to an object. It might be a project, a distribution source package, or a
8combination of one of those with an owner to represent that owner's default
9repository for that target.
10"""
11
12__metaclass__ = type
13__all__ = [
14 'ICanHasDefaultGitRepository',
15 ]
16
17from zope.interface import (
18 Attribute,
19 Interface,
20 )
21
22
23class ICanHasDefaultGitRepository(Interface):
24 """Something that has a default Git repository."""
25
26 context = Attribute("The object that can have a default Git repository.")
27 path = Attribute(
28 "The path for the default Git repository. "
29 "Note that this will be set even if there is no default repository.")
030
=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py 2015-02-19 23:57:34 +0000
+++ lib/lp/code/interfaces/gitrepository.py 2015-02-26 14:40:55 +0000
@@ -16,6 +16,7 @@
16import re16import re
1717
18from lazr.restful.fields import Reference18from lazr.restful.fields import Reference
19from zope.component import getUtility
19from zope.interface import (20from zope.interface import (
20 Attribute,21 Attribute,
21 Interface,22 Interface,
@@ -32,7 +33,16 @@
32from lp import _33from lp import _
33from lp.app.enums import InformationType34from lp.app.enums import InformationType
34from lp.app.validators import LaunchpadValidationError35from lp.app.validators import LaunchpadValidationError
36from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
35from lp.code.interfaces.hasgitrepositories import IHasGitRepositories37from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
38from lp.registry.interfaces.distributionsourcepackage import (
39 IDistributionSourcePackage,
40 )
41from lp.registry.interfaces.persondistributionsourcepackage import (
42 IPersonDistributionSourcePackageFactory,
43 )
44from lp.registry.interfaces.personproduct import IPersonProductFactory
45from lp.registry.interfaces.product import IProduct
36from lp.registry.interfaces.role import IPersonRoles46from lp.registry.interfaces.role import IPersonRoles
37from lp.services.fields import (47from lp.services.fields import (
38 PersonChoice,48 PersonChoice,
@@ -309,16 +319,44 @@
309 Return None if no match was found.319 Return None if no match was found.
310 """320 """
311321
312 def getDefaultRepository(target, owner=None):322 def getDefaultRepository(target):
313 """Get the default repository for a target or owner-target.323 """Get the default repository for a target.
314324
315 :param target: An `IHasGitRepositories`.325 :param target: An `IHasGitRepositories`.
316 :param owner: An `IPerson`, in which case search for that person's326
317 default repository for this target; or None, in which case327 :raises GitTargetError: if `target` is an `IPerson`.
318 search for the overall default repository for this target.328 :return: An `IGitRepository`, or None.
319329 """
320 :raises GitTargetError: if `target` is an `IPerson`.330
321 :return: An `IGitRepository`, or None.331 def getDefaultRepositoryForOwner(owner, target):
332 """Get a person's default repository for a target.
333
334 :param owner: An `IPerson`.
335 :param target: An `IHasGitRepositories`.
336
337 :raises GitTargetError: if `target` is an `IPerson`.
338 :return: An `IGitRepository`, or None.
339 """
340
341 def setDefaultRepository(target, repository):
342 """Set the default repository for a target.
343
344 :param target: An `IHasGitRepositories`.
345 :param repository: An `IGitRepository`, or None to unset the default
346 repository.
347
348 :raises GitTargetError: if `target` is an `IPerson`.
349 """
350
351 def setDefaultRepositoryForOwner(owner, target, repository):
352 """Set a person's default repository for a target.
353
354 :param owner: An `IPerson`.
355 :param target: An `IHasGitRepositories`.
356 :param repository: An `IGitRepository`, or None to unset the default
357 repository.
358
359 :raises GitTargetError: if `target` is an `IPerson`.
322 """360 """
323361
324 def getRepositories():362 def getRepositories():
@@ -349,9 +387,23 @@
349387
350 def getRepositoryDefaults(self):388 def getRepositoryDefaults(self):
351 """See `IGitRepository`."""389 """See `IGitRepository`."""
352 # XXX cjwatson 2015-02-06: This will return shortcut defaults once390 defaults = []
353 # they're implemented.391 if self.target_default:
354 return []392 defaults.append(ICanHasDefaultGitRepository(self.target))
393 if self.owner_default:
394 if IProduct.providedBy(self.target):
395 factory = getUtility(IPersonProductFactory)
396 default = factory.create(self.owner, self.target)
397 elif IDistributionSourcePackage.providedBy(self.target):
398 factory = getUtility(IPersonDistributionSourcePackageFactory)
399 default = factory.create(self.owner, self.target)
400 else:
401 # Also enforced by database constraint.
402 raise AssertionError(
403 "Only projects or packages can have owner-target default "
404 "repositories.")
405 defaults.append(ICanHasDefaultGitRepository(default))
406 return sorted(defaults)
355407
356 def getRepositoryIdentities(self):408 def getRepositoryIdentities(self):
357 """See `IGitRepository`."""409 """See `IGitRepository`."""
358410
=== added file 'lib/lp/code/model/defaultgit.py'
--- lib/lp/code/model/defaultgit.py 1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/defaultgit.py 2015-02-26 14:40:55 +0000
@@ -0,0 +1,113 @@
1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Implementation of `ICanHasDefaultGitRepository`."""
5
6__metaclass__ = type
7# Don't export anything -- anything you need from this module you can get by
8# adapting another object.
9__all__ = []
10
11from zope.component import adapts
12from zope.interface import implements
13
14from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
15from lp.registry.interfaces.distributionsourcepackage import (
16 IDistributionSourcePackage,
17 )
18from lp.registry.interfaces.persondistributionsourcepackage import (
19 IPersonDistributionSourcePackage,
20 )
21from lp.registry.interfaces.personproduct import IPersonProduct
22from lp.registry.interfaces.product import IProduct
23
24
25class BaseDefaultGitRepository:
26 """Provides the common sorting algorithm."""
27
28 def __cmp__(self, other):
29 if not ICanHasDefaultGitRepository.providedBy(other):
30 raise AssertionError("Can't compare with: %r" % other)
31 return cmp(self.sort_order, other.sort_order)
32
33 def __eq__(self, other):
34 return (
35 isinstance(other, self.__class__) and
36 self.context == other.context)
37
38 def __ne__(self, other):
39 return not self == other
40
41
42class ProjectDefaultGitRepository(BaseDefaultGitRepository):
43 """Implement a default Git repository for a project."""
44
45 adapts(IProduct)
46 implements(ICanHasDefaultGitRepository)
47
48 sort_order = 0
49
50 def __init__(self, project):
51 self.context = project
52
53 @property
54 def path(self):
55 """See `ICanHasDefaultGitRepository`."""
56 return self.context.name
57
58
59class PackageDefaultGitRepository(BaseDefaultGitRepository):
60 """Implement a default Git repository for a distribution source package."""
61
62 adapts(IDistributionSourcePackage)
63 implements(ICanHasDefaultGitRepository)
64
65 sort_order = 0
66
67 def __init__(self, distro_source_package):
68 self.context = distro_source_package
69
70 @property
71 def path(self):
72 """See `ICanHasDefaultGitRepository`."""
73 return "%s/+source/%s" % (
74 self.context.distribution.name,
75 self.context.sourcepackagename.name)
76
77
78class OwnerProjectDefaultGitRepository(BaseDefaultGitRepository):
79 """Implement an owner's default Git repository for a project."""
80
81 adapts(IPersonProduct)
82 implements(ICanHasDefaultGitRepository)
83
84 sort_order = 1
85
86 def __init__(self, person_project):
87 self.context = person_project
88
89 @property
90 def path(self):
91 """See `ICanHasDefaultGitRepository`."""
92 return "~%s/%s" % (self.context.person.name, self.context.product.name)
93
94
95class OwnerPackageDefaultGitRepository(BaseDefaultGitRepository):
96 """Implement an owner's default Git repository for a distribution source
97 package."""
98
99 adapts(IPersonDistributionSourcePackage)
100 implements(ICanHasDefaultGitRepository)
101
102 sort_order = 1
103
104 def __init__(self, person_distro_source_package):
105 self.context = person_distro_source_package
106
107 @property
108 def path(self):
109 """See `ICanHasDefaultGitRepository`."""
110 dsp = self.context.distro_source_package
111 return "~%s/%s/+source/%s" % (
112 self.context.person.name, dsp.distribution.name,
113 dsp.sourcepackagename.name)
0114
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2015-02-19 23:57:34 +0000
+++ lib/lp/code/model/gitrepository.py 2015-02-26 14:40:55 +0000
@@ -83,6 +83,7 @@
83 ArrayIntersects,83 ArrayIntersects,
84 )84 )
85from lp.services.propertycache import cachedproperty85from lp.services.propertycache import cachedproperty
86from lp.services.webapp.authorization import available_with_permission
8687
8788
88def git_repository_modified(repository, event):89def git_repository_modified(repository, event):
@@ -206,8 +207,9 @@
206 """See `IGitRepository`."""207 """See `IGitRepository`."""
207 if value:208 if value:
208 # Check for an existing owner-target default.209 # Check for an existing owner-target default.
209 existing = getUtility(IGitRepositorySet).getDefaultRepository(210 repository_set = getUtility(IGitRepositorySet)
210 self.target, owner=self.owner)211 existing = repository_set.getDefaultRepositoryForOwner(
212 self.owner, self.target)
211 if existing is not None:213 if existing is not None:
212 raise GitDefaultConflict(214 raise GitDefaultConflict(
213 existing, self.target, owner=self.owner)215 existing, self.target, owner=self.owner)
@@ -358,24 +360,76 @@
358 # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.360 # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
359 raise NotImplementedError361 raise NotImplementedError
360362
361 def getDefaultRepository(self, target, owner=None):363 def getDefaultRepository(self, target):
362 """See `IGitRepositorySet`."""364 """See `IGitRepositorySet`."""
363 clauses = []365 clauses = [GitRepository.target_default == True]
364 if IProduct.providedBy(target):366 if IProduct.providedBy(target):
365 clauses.append(GitRepository.project == target)367 clauses.append(GitRepository.project == target)
366 elif IDistributionSourcePackage.providedBy(target):368 elif IDistributionSourcePackage.providedBy(target):
367 clauses.append(GitRepository.distribution == target.distribution)369 clauses.append(GitRepository.distribution == target.distribution)
368 clauses.append(370 clauses.append(
369 GitRepository.sourcepackagename == target.sourcepackagename)371 GitRepository.sourcepackagename == target.sourcepackagename)
370 else:372 else:
371 raise GitTargetError(373 raise GitTargetError(
372 "Personal repositories cannot be defaults for any target.")374 "Personal repositories cannot be defaults for any target.")
373 if owner is not None:375 return IStore(GitRepository).find(GitRepository, *clauses).one()
374 clauses.append(GitRepository.owner == owner)376
375 clauses.append(GitRepository.owner_default == True)377 def getDefaultRepositoryForOwner(self, owner, target):
376 else:378 """See `IGitRepositorySet`."""
377 clauses.append(GitRepository.target_default == True)379 clauses = [
378 return IStore(GitRepository).find(GitRepository, *clauses).one()380 GitRepository.owner == owner,
381 GitRepository.owner_default == True,
382 ]
383 if IProduct.providedBy(target):
384 clauses.append(GitRepository.project == target)
385 elif IDistributionSourcePackage.providedBy(target):
386 clauses.append(GitRepository.distribution == target.distribution)
387 clauses.append(
388 GitRepository.sourcepackagename == target.sourcepackagename)
389 else:
390 raise GitTargetError(
391 "Personal repositories cannot be defaults for any target.")
392 return IStore(GitRepository).find(GitRepository, *clauses).one()
393
394 @available_with_permission('launchpad.Edit', 'target')
395 def setDefaultRepository(self, target, repository):
396 """See `IGitRepositorySet`."""
397 if IPerson.providedBy(target):
398 raise GitTargetError(
399 "Cannot set a default Git repository for a person, only "
400 "for a project or a package.")
401 if repository is not None:
402 if repository.target != target:
403 raise GitTargetError(
404 "Cannot set default Git repository to one attached to "
405 "another target.")
406 repository.setTargetDefault(True)
407 else:
408 previous = self.getDefaultRepository(target)
409 if previous is not None:
410 previous.setTargetDefault(False)
411
412 @available_with_permission('launchpad.Edit', 'owner')
413 def setDefaultRepositoryForOwner(self, owner, target, repository):
414 """See `IGitRepositorySet`."""
415 if IPerson.providedBy(target):
416 raise GitTargetError(
417 "Cannot set a default Git repository for a person, only "
418 "for a project or a package.")
419 if repository is not None:
420 if repository.target != target:
421 raise GitTargetError(
422 "Cannot set default Git repository to one attached to "
423 "another target.")
424 if repository.owner != owner:
425 raise GitTargetError(
426 "Cannot set a person's default Git repository to one "
427 "owned by somebody else.")
428 repository.setOwnerDefault(True)
429 else:
430 previous = self.getDefaultRepositoryForOwner(owner, target)
431 if previous is not None:
432 previous.setOwnerDefault(False)
379433
380 def getRepositories(self):434 def getRepositories(self):
381 """See `IGitRepositorySet`."""435 """See `IGitRepositorySet`."""
382436
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2015-02-20 00:56:57 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-26 14:40:55 +0000
@@ -6,6 +6,7 @@
6__metaclass__ = type6__metaclass__ = type
77
8from datetime import datetime8from datetime import datetime
9from functools import partial
910
10from lazr.lifecycle.event import ObjectModifiedEvent11from lazr.lifecycle.event import ObjectModifiedEvent
11import pytz12import pytz
@@ -24,12 +25,20 @@
24 GitRepositoryCreatorNotOwner,25 GitRepositoryCreatorNotOwner,
25 GitTargetError,26 GitTargetError,
26 )27 )
28from lp.code.interfaces.defaultgit import ICanHasDefaultGitRepository
27from lp.code.interfaces.gitnamespace import (29from lp.code.interfaces.gitnamespace import (
28 IGitNamespacePolicy,30 IGitNamespacePolicy,
29 IGitNamespaceSet,31 IGitNamespaceSet,
30 )32 )
31from lp.code.interfaces.gitrepository import IGitRepository33from lp.code.interfaces.gitrepository import (
34 IGitRepository,
35 IGitRepositorySet,
36 )
32from lp.registry.enums import BranchSharingPolicy37from lp.registry.enums import BranchSharingPolicy
38from lp.registry.interfaces.persondistributionsourcepackage import (
39 IPersonDistributionSourcePackageFactory,
40 )
41from lp.registry.interfaces.personproduct import IPersonProductFactory
33from lp.services.database.constants import UTC_NOW42from lp.services.database.constants import UTC_NOW
34from lp.services.webapp.authorization import check_permission43from lp.services.webapp.authorization import check_permission
35from lp.testing import (44from lp.testing import (
@@ -96,6 +105,10 @@
96105
97 layer = DatabaseFunctionalLayer106 layer = DatabaseFunctionalLayer
98107
108 def setUp(self):
109 super(TestGitIdentityMixin, self).setUp()
110 self.repository_set = getUtility(IGitRepositorySet)
111
99 def assertGitIdentity(self, repository, identity_path):112 def assertGitIdentity(self, repository, identity_path):
100 """Assert that the Git identity of 'repository' is 'identity_path'.113 """Assert that the Git identity of 'repository' is 'identity_path'.
101114
@@ -111,6 +124,53 @@
111 repository = self.factory.makeGitRepository()124 repository = self.factory.makeGitRepository()
112 self.assertGitIdentity(repository, repository.unique_name)125 self.assertGitIdentity(repository, repository.unique_name)
113126
127 def test_git_identity_default_for_project(self):
128 # If a repository is the default for a project, then its Git
129 # identity is the project name.
130 project = self.factory.makeProduct()
131 repository = self.factory.makeGitRepository(
132 owner=project.owner, target=project)
133 with person_logged_in(project.owner):
134 self.repository_set.setDefaultRepository(project, repository)
135 self.assertGitIdentity(repository, project.name)
136
137 def test_git_identity_default_for_package(self):
138 # If a repository is the default for a package, then its Git
139 # identity uses the path to that package.
140 dsp = self.factory.makeDistributionSourcePackage()
141 repository = self.factory.makeGitRepository(target=dsp)
142 with admin_logged_in():
143 self.repository_set.setDefaultRepository(dsp, repository)
144 self.assertGitIdentity(
145 repository,
146 "%s/+source/%s" % (
147 dsp.distribution.name, dsp.sourcepackagename.name))
148
149 def test_git_identity_owner_default_for_project(self):
150 # If a repository is a person's default for a project, then its Git
151 # identity is a combination of the person and project names.
152 project = self.factory.makeProduct()
153 repository = self.factory.makeGitRepository(target=project)
154 with person_logged_in(repository.owner):
155 self.repository_set.setDefaultRepositoryForOwner(
156 repository.owner, project, repository)
157 self.assertGitIdentity(
158 repository, "~%s/%s" % (repository.owner.name, project.name))
159
160 def test_git_identity_owner_default_for_package(self):
161 # If a repository is a person's default for a package, then its Git
162 # identity is a combination of the person name and the package path.
163 dsp = self.factory.makeDistributionSourcePackage()
164 repository = self.factory.makeGitRepository(target=dsp)
165 with person_logged_in(repository.owner):
166 self.repository_set.setDefaultRepositoryForOwner(
167 repository.owner, dsp, repository)
168 self.assertGitIdentity(
169 repository,
170 "~%s/%s/+source/%s" % (
171 repository.owner.name, dsp.distribution.name,
172 dsp.sourcepackagename.name))
173
114 def test_identities_no_defaults(self):174 def test_identities_no_defaults(self):
115 # If there are no defaults, the only repository identity is the175 # If there are no defaults, the only repository identity is the
116 # unique name.176 # unique name.
@@ -119,8 +179,54 @@
119 [(repository.unique_name, repository)],179 [(repository.unique_name, repository)],
120 repository.getRepositoryIdentities())180 repository.getRepositoryIdentities())
121181
122 # XXX cjwatson 2015-02-12: This will need to be expanded once support182 def test_default_for_project(self):
123 # for default repositories is in place.183 # If a repository is the default for a project, then that is the
184 # preferred identity. Target defaults are preferred over
185 # owner-target defaults.
186 eric = self.factory.makePerson(name="eric")
187 fooix = self.factory.makeProduct(name="fooix", owner=eric)
188 repository = self.factory.makeGitRepository(
189 owner=eric, target=fooix, name=u"fooix-repo")
190 with person_logged_in(fooix.owner):
191 self.repository_set.setDefaultRepositoryForOwner(
192 repository.owner, fooix, repository)
193 self.repository_set.setDefaultRepository(fooix, repository)
194 eric_fooix = getUtility(IPersonProductFactory).create(eric, fooix)
195 self.assertEqual(
196 [ICanHasDefaultGitRepository(target)
197 for target in (fooix, eric_fooix)],
198 repository.getRepositoryDefaults())
199 self.assertEqual(
200 [("fooix", fooix), ("~eric/fooix", eric_fooix),
201 ("~eric/fooix/+git/fooix-repo", repository)],
202 repository.getRepositoryIdentities())
203
204 def test_default_for_package(self):
205 # If a repository is the default for a package, then that is the
206 # preferred identity. Target defaults are preferred over
207 # owner-target defaults.
208 mint = self.factory.makeDistribution(name="mint")
209 eric = self.factory.makePerson(name="eric")
210 mint_choc = self.factory.makeDistributionSourcePackage(
211 distribution=mint, sourcepackagename="choc")
212 repository = self.factory.makeGitRepository(
213 owner=eric, target=mint_choc, name=u"choc-repo")
214 dsp = repository.target
215 with admin_logged_in():
216 self.repository_set.setDefaultRepositoryForOwner(
217 repository.owner, dsp, repository)
218 self.repository_set.setDefaultRepository(dsp, repository)
219 eric_dsp = getUtility(IPersonDistributionSourcePackageFactory).create(
220 eric, dsp)
221 self.assertEqual(
222 [ICanHasDefaultGitRepository(target)
223 for target in (dsp, eric_dsp)],
224 repository.getRepositoryDefaults())
225 self.assertEqual(
226 [("mint/+source/choc", dsp),
227 ("~eric/mint/+source/choc", eric_dsp),
228 ("~eric/mint/+source/choc/+git/choc-repo", repository)],
229 repository.getRepositoryIdentities())
124230
125231
126class TestGitRepositoryDateLastModified(TestCaseWithFactory):232class TestGitRepositoryDateLastModified(TestCaseWithFactory):
@@ -392,3 +498,133 @@
392 self.assertRaises(498 self.assertRaises(
393 GitTargetError, repository.setTarget,499 GitTargetError, repository.setTarget,
394 target=commercial_project, user=owner)500 target=commercial_project, user=owner)
501
502
503class TestGitRepositorySet(TestCaseWithFactory):
504
505 layer = DatabaseFunctionalLayer
506
507 def setUp(self):
508 super(TestGitRepositorySet, self).setUp()
509 self.repository_set = getUtility(IGitRepositorySet)
510
511 def test_provides_IGitRepositorySet(self):
512 # GitRepositorySet instances provide IGitRepositorySet.
513 verifyObject(IGitRepositorySet, self.repository_set)
514
515 def test_setDefaultRepository_refuses_person(self):
516 # setDefaultRepository refuses if the target is a person.
517 person = self.factory.makePerson()
518 repository = self.factory.makeGitRepository(owner=person)
519 with person_logged_in(person):
520 self.assertRaises(
521 GitTargetError, self.repository_set.setDefaultRepository,
522 person, repository)
523
524 def test_setDefaultRepositoryForOwner_refuses_person(self):
525 # setDefaultRepositoryForOwner refuses if the target is a person.
526 person = self.factory.makePerson()
527 repository = self.factory.makeGitRepository(owner=person)
528 with person_logged_in(person):
529 self.assertRaises(
530 GitTargetError,
531 self.repository_set.setDefaultRepositoryForOwner,
532 person, person, repository)
533
534
535class TestGitRepositorySetDefaultsMixin:
536
537 layer = DatabaseFunctionalLayer
538
539 def setUp(self):
540 super(TestGitRepositorySetDefaultsMixin, self).setUp()
541 self.repository_set = getUtility(IGitRepositorySet)
542 self.get_method = self.repository_set.getDefaultRepository
543 self.set_method = self.repository_set.setDefaultRepository
544
545 def makeGitRepository(self, target):
546 return self.factory.makeGitRepository(target=target)
547
548 def test_default_repository_round_trip(self):
549 # A target's default Git repository set using setDefaultRepository*
550 # can be retrieved using getDefaultRepository*.
551 target = self.makeTarget()
552 repository = self.makeGitRepository(target)
553 self.assertIsNone(self.get_method(target))
554 with person_logged_in(self.getPersonForLogin(target)):
555 self.set_method(target, repository)
556 self.assertEqual(repository, self.get_method(target))
557
558 def test_set_default_repository_None(self):
559 # setDefaultRepository*(target, None) clears the default.
560 target = self.makeTarget()
561 repository = self.makeGitRepository(target)
562 with person_logged_in(self.getPersonForLogin(target)):
563 self.set_method(target, repository)
564 self.set_method(target, None)
565 self.assertIsNone(self.get_method(target))
566
567 def test_set_default_repository_different_target(self):
568 # setDefaultRepository* refuses if the repository is attached to a
569 # different target.
570 target = self.makeTarget()
571 other_target = self.makeTarget(template=target)
572 repository = self.makeGitRepository(other_target)
573 with person_logged_in(self.getPersonForLogin(target)):
574 self.assertRaises(
575 GitTargetError, self.set_method, target, repository)
576
577
578class TestGitRepositorySetDefaultsProject(
579 TestGitRepositorySetDefaultsMixin, TestCaseWithFactory):
580
581 def makeTarget(self, template=None):
582 return self.factory.makeProduct()
583
584 @staticmethod
585 def getPersonForLogin(target):
586 return target.owner
587
588
589class TestGitRepositorySetDefaultsPackage(
590 TestGitRepositorySetDefaultsMixin, TestCaseWithFactory):
591
592 def makeTarget(self, template=None):
593 kwargs = {}
594 if template is not None:
595 kwargs["distribution"] = template.distribution
596 return self.factory.makeDistributionSourcePackage(**kwargs)
597
598 @staticmethod
599 def getPersonForLogin(target):
600 return target.distribution.owner
601
602
603class TestGitRepositorySetDefaultsOwnerMixin(
604 TestGitRepositorySetDefaultsMixin):
605
606 def setUp(self):
607 super(TestGitRepositorySetDefaultsOwnerMixin, self).setUp()
608 self.person = self.factory.makePerson()
609 self.get_method = partial(
610 self.repository_set.getDefaultRepositoryForOwner, self.person)
611 self.set_method = partial(
612 self.repository_set.setDefaultRepositoryForOwner, self.person)
613
614 def makeGitRepository(self, target):
615 return self.factory.makeGitRepository(owner=self.person, target=target)
616
617 def getPersonForLogin(self, target):
618 return self.person
619
620
621class TestGitRepositorySetDefaultsOwnerProject(
622 TestGitRepositorySetDefaultsOwnerMixin,
623 TestGitRepositorySetDefaultsProject):
624 pass
625
626
627class TestGitRepositorySetDefaultsOwnerPackage(
628 TestGitRepositorySetDefaultsOwnerMixin,
629 TestGitRepositorySetDefaultsPackage):
630 pass
395631
=== modified file 'lib/lp/registry/model/persondistributionsourcepackage.py'
--- lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-06 13:37:58 +0000
+++ lib/lp/registry/model/persondistributionsourcepackage.py 2015-02-26 14:40:55 +0000
@@ -37,3 +37,12 @@
37 def displayname(self):37 def displayname(self):
38 return '%s in %s' % (38 return '%s in %s' % (
39 self.person.displayname, self.distro_source_package.displayname)39 self.person.displayname, self.distro_source_package.displayname)
40
41 def __eq__(self, other):
42 return (
43 IPersonDistributionSourcePackage.providedBy(other) and
44 self.person.id == other.person.id and
45 self.distro_source_package == other.distro_source_package)
46
47 def __ne__(self, other):
48 return not self == other
4049
=== modified file 'lib/lp/registry/model/personproduct.py'
--- lib/lp/registry/model/personproduct.py 2011-12-19 23:38:16 +0000
+++ lib/lp/registry/model/personproduct.py 2015-02-26 14:40:55 +0000
@@ -38,3 +38,12 @@
38 def displayname(self):38 def displayname(self):
39 return '%s in %s' % (39 return '%s in %s' % (
40 self.person.displayname, self.product.displayname)40 self.person.displayname, self.product.displayname)
41
42 def __eq__(self, other):
43 return (
44 IPersonProduct.providedBy(other) and
45 self.person.id == other.person.id and
46 self.product.id == other.product.id)
47
48 def __ne__(self, other):
49 return not self == other