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

Proposed by Colin Watson on 2015-02-20
Status: Merged
Approved by: Colin Watson on 2015-02-27
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 2015-02-20 Approve on 2015-02-26
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.
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)
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/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