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

Proposed by Colin Watson on 2015-02-07
Status: Merged
Merged at revision: 17353
Proposed branch: lp:~cjwatson/launchpad/git-namespace
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-basic-model
Diff against target: 1044 lines (+910/-12)
7 files modified
lib/lp/code/configure.zcml (+20/-0)
lib/lp/code/errors.py (+63/-0)
lib/lp/code/interfaces/gitnamespace.py (+249/-0)
lib/lp/code/interfaces/gitrepository.py (+3/-0)
lib/lp/code/model/branchnamespace.py (+2/-0)
lib/lp/code/model/gitnamespace.py (+538/-0)
lib/lp/code/model/gitrepository.py (+35/-12)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-namespace
Reviewer Review Type Date Requested Status
William Grant code 2015-02-07 Approve on 2015-02-19
Review via email: mp+248995@code.launchpad.net

Commit message

Add GitNamespace, and fill in a few bits of GitRepository that require it.

Description of the change

Add GitNamespace, and fill in a few bits of GitRepository that require it.

This follows on from https://code.launchpad.net/~cjwatson/launchpad/git-basic-model/+merge/248976, and similar caveats apply. However, this adds almost enough infrastructure to start being able to run useful tests, since it's now possible to create test objects, so the next branch after this will start bringing these up.

To post a comment you must log in.
William Grant (wgrant) :
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-13 18:35:27 +0000
3+++ lib/lp/code/configure.zcml 2015-02-13 18:35:27 +0000
4@@ -838,6 +838,26 @@
5 <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
6 </securedutility>
7
8+ <!-- GitNamespace -->
9+
10+ <class class="lp.code.model.gitnamespace.PackageGitNamespace">
11+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
12+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
13+ </class>
14+ <class class="lp.code.model.gitnamespace.PersonalGitNamespace">
15+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
16+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
17+ </class>
18+ <class class="lp.code.model.gitnamespace.ProjectGitNamespace">
19+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
20+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
21+ </class>
22+ <securedutility
23+ class="lp.code.model.gitnamespace.GitNamespaceSet"
24+ provides="lp.code.interfaces.gitnamespace.IGitNamespaceSet">
25+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
26+ </securedutility>
27+
28 <lp:help-folder folder="help" name="+help-code" />
29
30 <!-- Diffs -->
31
32=== modified file 'lib/lp/code/errors.py'
33--- lib/lp/code/errors.py 2015-02-13 18:35:27 +0000
34+++ lib/lp/code/errors.py 2015-02-13 18:35:27 +0000
35@@ -29,12 +29,19 @@
36 'ClaimReviewFailed',
37 'DiffNotFound',
38 'GitDefaultConflict',
39+ 'GitRepositoryCreationException',
40+ 'GitRepositoryCreationFault',
41+ 'GitRepositoryCreationForbidden',
42+ 'GitRepositoryCreatorNotMemberOfOwnerTeam',
43+ 'GitRepositoryCreatorNotOwner',
44+ 'GitRepositoryExists',
45 'GitTargetError',
46 'InvalidBranchMergeProposal',
47 'InvalidMergeQueueConfig',
48 'InvalidNamespace',
49 'NoLinkedBranch',
50 'NoSuchBranch',
51+ 'NoSuchGitRepository',
52 'PrivateBranchRecipe',
53 'ReviewNotPending',
54 'StaleLastMirrored',
55@@ -314,10 +321,66 @@
56 """Raised when the user specifies an unrecognized branch type."""
57
58
59+class GitRepositoryCreationException(Exception):
60+ """Base class for Git repository creation exceptions."""
61+
62+
63+@error_status(httplib.CONFLICT)
64+class GitRepositoryExists(GitRepositoryCreationException):
65+ """Raised when creating a Git repository that already exists."""
66+
67+ def __init__(self, existing_repository):
68+ params = {
69+ "name": existing_repository.name,
70+ "context": existing_repository.namespace.name,
71+ }
72+ message = (
73+ 'A Git repository with the name "%(name)s" already exists for '
74+ '%(context)s.' % params)
75+ self.existing_repository = existing_repository
76+ GitRepositoryCreationException.__init__(self, message)
77+
78+
79+class GitRepositoryCreationForbidden(GitRepositoryCreationException):
80+ """A visibility policy forbids Git repository creation.
81+
82+ The exception is raised if the policy for the project does not allow the
83+ creator of the repository to create a repository for that project.
84+ """
85+
86+
87+@error_status(httplib.BAD_REQUEST)
88+class GitRepositoryCreatorNotMemberOfOwnerTeam(GitRepositoryCreationException):
89+ """Git repository creator is not a member of the owner team.
90+
91+ Raised when a user is attempting to create a repository and set the
92+ owner of the repository to a team that they are not a member of.
93+ """
94+
95+
96+@error_status(httplib.BAD_REQUEST)
97+class GitRepositoryCreatorNotOwner(GitRepositoryCreationException):
98+ """A user cannot create a Git repository belonging to another user.
99+
100+ Raised when a user is attempting to create a repository and set the
101+ owner of the repository to another user.
102+ """
103+
104+
105+class GitRepositoryCreationFault(GitRepositoryCreationException):
106+ """Raised when there is a hosting fault creating a Git repository."""
107+
108+
109 class GitTargetError(Exception):
110 """Raised when there is an error determining a Git repository target."""
111
112
113+class NoSuchGitRepository(NameLookupFailed):
114+ """Raised when we try to load a Git repository that does not exist."""
115+
116+ _message_prefix = "No such Git repository"
117+
118+
119 @error_status(httplib.CONFLICT)
120 class GitDefaultConflict(Exception):
121 """Raised when trying to set a Git repository as the default for
122
123=== added file 'lib/lp/code/interfaces/gitnamespace.py'
124--- lib/lp/code/interfaces/gitnamespace.py 1970-01-01 00:00:00 +0000
125+++ lib/lp/code/interfaces/gitnamespace.py 2015-02-13 18:35:27 +0000
126@@ -0,0 +1,249 @@
127+# Copyright 2015 Canonical Ltd. This software is licensed under the
128+# GNU Affero General Public License version 3 (see the file LICENSE).
129+
130+"""Interface for a Git repository namespace."""
131+
132+__metaclass__ = type
133+__all__ = [
134+ 'get_git_namespace',
135+ 'IGitNamespace',
136+ 'IGitNamespacePolicy',
137+ 'IGitNamespaceSet',
138+ 'split_git_unique_name',
139+ ]
140+
141+from zope.component import getUtility
142+from zope.interface import (
143+ Attribute,
144+ Interface,
145+ )
146+
147+from lp.code.errors import InvalidNamespace
148+from lp.registry.interfaces.distributionsourcepackage import (
149+ IDistributionSourcePackage,
150+ )
151+from lp.registry.interfaces.product import IProduct
152+
153+
154+class IGitNamespace(Interface):
155+ """A namespace that a Git repository lives in."""
156+
157+ name = Attribute(
158+ "The name of the namespace. This is prepended to the repository name.")
159+
160+ target = Attribute("The `IHasGitRepositories` for this namespace.")
161+
162+ def createRepository(registrant, name, information_type=None,
163+ date_created=None):
164+ """Create and return an `IGitRepository` in this namespace."""
165+
166+ def isNameUsed(name):
167+ """Is 'name' already used in this namespace?"""
168+
169+ def findUnusedName(prefix):
170+ """Find an unused repository name starting with 'prefix'.
171+
172+ Note that there is no guarantee that the name returned by this method
173+ will remain unused for very long.
174+ """
175+
176+ def moveRepository(repository, mover, new_name=None,
177+ rename_if_necessary=False):
178+ """Move the repository into this namespace.
179+
180+ :param repository: The `IGitRepository` to move.
181+ :param mover: The `IPerson` doing the moving.
182+ :param new_name: A new name for the repository.
183+ :param rename_if_necessary: Rename the repository if the repository
184+ name already exists in this namespace.
185+ :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
186+ owner is a team and 'mover' is not in that team.
187+ :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
188+ individual and 'mover' is not the owner.
189+ :raises GitRepositoryCreationForbidden: if 'mover' is not allowed to
190+ create a repository in this namespace due to privacy rules.
191+ :raises GitRepositoryExists: if a repository with the new name
192+ already exists in the namespace, and 'rename_if_necessary' is
193+ False.
194+ """
195+
196+ def getRepositories():
197+ """Return the repositories in this namespace."""
198+
199+ def getByName(repository_name, default=None):
200+ """Find the repository in this namespace called 'repository_name'.
201+
202+ :return: `IGitRepository` if found, otherwise 'default'.
203+ """
204+
205+ def __eq__(other):
206+ """Is this namespace the same as another namespace?"""
207+
208+ def __ne__(other):
209+ """Is this namespace not the same as another namespace?"""
210+
211+
212+class IGitNamespacePolicy(Interface):
213+ """Methods relating to Git repository creation and validation."""
214+
215+ def getAllowedInformationTypes(who):
216+ """Get the information types that a repository in this namespace can
217+ have.
218+
219+ :param who: The user making the request.
220+ :return: A sequence of `InformationType`s.
221+ """
222+
223+ def getDefaultInformationType(who):
224+ """Get the default information type for repositories in this namespace.
225+
226+ :param who: The user to return the information type for.
227+ :return: An `InformationType`.
228+ """
229+
230+ def validateRegistrant(registrant):
231+ """Check that the registrant can create a repository in this namespace.
232+
233+ :param registrant: An `IPerson`.
234+ :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
235+ owner is a team and the registrant is not in that team.
236+ :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
237+ individual and the registrant is not the owner.
238+ :raises GitRepositoryCreationForbidden: if the registrant is not
239+ allowed to create a repository in this namespace due to privacy
240+ rules.
241+ """
242+
243+ def validateRepositoryName(name):
244+ """Check the repository `name`.
245+
246+ :param name: A branch name, either string or unicode.
247+ :raises GitRepositoryExists: if a branch with the `name` already
248+ exists in the namespace.
249+ :raises LaunchpadValidationError: if the name doesn't match the
250+ validation constraints on IGitRepository.name.
251+ """
252+
253+ def validateMove(repository, mover, name=None):
254+ """Check that 'mover' can move 'repository' into this namespace.
255+
256+ :param repository: An `IGitRepository` that might be moved.
257+ :param mover: The `IPerson` who would move it.
258+ :param name: A new name for the repository. If None, the repository
259+ name is used.
260+ :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
261+ owner is a team and 'mover' is not in that team.
262+ :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
263+ individual and 'mover' is not the owner.
264+ :raises GitRepositoryCreationForbidden: if 'mover' is not allowed to
265+ create a repository in this namespace due to privacy rules.
266+ :raises GitRepositoryExists: if a repository with the new name
267+ already exists in the namespace.
268+ """
269+
270+
271+class IGitNamespaceSet(Interface):
272+ """Interface for getting Git repository namespaces."""
273+
274+ def get(person, project=None, distribution=None, sourcepackagename=None):
275+ """Return the appropriate `IGitNamespace` for the given objects."""
276+
277+ def interpret(person, project, distribution, sourcepackagename):
278+ """Like `get`, but takes names of objects.
279+
280+ :raise NoSuchPerson: If the person referred to cannot be found.
281+ :raise NoSuchProduct: If the project referred to cannot be found.
282+ :raise NoSuchDistribution: If the distribution referred to cannot be
283+ found.
284+ :raise NoSuchSourcePackageName: If the sourcepackagename referred to
285+ cannot be found.
286+ :return: An `IGitNamespace`.
287+ """
288+
289+ def parse(namespace_name):
290+ """Parse 'namespace_name' into its components.
291+
292+ The name of a namespace is actually a path containing many elements,
293+ each of which maps to a particular kind of object in Launchpad.
294+ Elements that can appear in a namespace name are: 'person',
295+ 'project', 'distribution', and 'sourcepackagename'.
296+
297+ `parse` returns a dict which maps the names of these elements (e.g.
298+ 'person', 'project') to the values of these elements (e.g. 'mark',
299+ 'firefox'). If the given path doesn't include a particular kind of
300+ element, the dict maps that element name to None.
301+
302+ For example::
303+ parse('~foo/bar') => {
304+ 'person': 'foo', 'project': 'bar', 'distribution': None,
305+ 'sourcepackagename': None,
306+ }
307+
308+ If the given 'namespace_name' cannot be parsed, then we raise an
309+ `InvalidNamespace` error.
310+
311+ :raise InvalidNamespace: If the name is too long, too short, or
312+ malformed.
313+ :return: A dict with keys matching each component in
314+ 'namespace_name'.
315+ """
316+
317+ def lookup(namespace_name):
318+ """Return the `IGitNamespace` for 'namespace_name'.
319+
320+ :raise InvalidNamespace: if namespace_name cannot be parsed.
321+ :raise NoSuchPerson: if the person referred to cannot be found.
322+ :raise NoSuchProduct: if the project referred to cannot be found.
323+ :raise NoSuchDistribution: if the distribution referred to cannot be
324+ found.
325+ :raise NoSuchSourcePackageName: if the sourcepackagename referred to
326+ cannot be found.
327+ :return: An `IGitNamespace`.
328+ """
329+
330+ def traverse(segments):
331+ """Look up the Git repository at the path given by 'segments'.
332+
333+ The iterable 'segments' will be consumed until a repository is
334+ found. As soon as a repository is found, the repository will be
335+ returned and the consumption of segments will stop. Thus, there
336+ will often be unconsumed segments that can be used for further
337+ traversal.
338+
339+ :param segments: An iterable of URL segments, a prefix of which
340+ identifies a Git repository. The first segment is the username,
341+ *not* preceded by a '~`.
342+ :raise InvalidNamespace: if there are not enough segments to define a
343+ repository.
344+ :raise NoSuchPerson: if the person referred to cannot be found.
345+ :raise NoSuchProduct: if the product or distro referred to cannot be
346+ found.
347+ :raise NoSuchDistribution: if the distribution referred to cannot be
348+ found.
349+ :raise NoSuchSourcePackageName: if the sourcepackagename referred to
350+ cannot be found.
351+ :return: `IGitRepository`.
352+ """
353+
354+
355+def get_git_namespace(target, owner):
356+ if IProduct.providedBy(target):
357+ return getUtility(IGitNamespaceSet).get(owner, project=target)
358+ elif IDistributionSourcePackage.providedBy(target):
359+ return getUtility(IGitNamespaceSet).get(
360+ owner, distribution=target.distribution,
361+ sourcepackagename=target.sourcepackagename)
362+ else:
363+ return getUtility(IGitNamespaceSet).get(owner)
364+
365+
366+# Marker for references to Git URL layouts: ##GITNAMESPACE##
367+def split_git_unique_name(unique_name):
368+ """Return the namespace and repository names of a unique name."""
369+ try:
370+ namespace_name, literal, repository_name = unique_name.rsplit("/", 2)
371+ except ValueError:
372+ raise InvalidNamespace(unique_name)
373+ if literal != "+git":
374+ raise InvalidNamespace(unique_name)
375+ return namespace_name, repository_name
376
377=== modified file 'lib/lp/code/interfaces/gitrepository.py'
378--- lib/lp/code/interfaces/gitrepository.py 2015-02-13 18:35:27 +0000
379+++ lib/lp/code/interfaces/gitrepository.py 2015-02-13 18:35:27 +0000
380@@ -105,6 +105,9 @@
381 schema=IHasGitRepositories,
382 description=_("The target of the repository."))
383
384+ namespace = Attribute(
385+ "The namespace of this repository, as an `IGitNamespace`.")
386+
387 information_type = Choice(
388 title=_("Information Type"), vocabulary=InformationType,
389 required=True, readonly=True, default=InformationType.PUBLIC,
390
391=== modified file 'lib/lp/code/model/branchnamespace.py'
392--- lib/lp/code/model/branchnamespace.py 2015-02-09 11:38:30 +0000
393+++ lib/lp/code/model/branchnamespace.py 2015-02-13 18:35:27 +0000
394@@ -7,6 +7,8 @@
395 __all__ = [
396 'BranchNamespaceSet',
397 'BRANCH_POLICY_ALLOWED_TYPES',
398+ 'BRANCH_POLICY_DEFAULT_TYPES',
399+ 'BRANCH_POLICY_REQUIRED_GRANTS',
400 'PackageBranchNamespace',
401 'PersonalBranchNamespace',
402 'ProjectBranchNamespace',
403
404=== added file 'lib/lp/code/model/gitnamespace.py'
405--- lib/lp/code/model/gitnamespace.py 1970-01-01 00:00:00 +0000
406+++ lib/lp/code/model/gitnamespace.py 2015-02-13 18:35:27 +0000
407@@ -0,0 +1,538 @@
408+# Copyright 2015 Canonical Ltd. This software is licensed under the
409+# GNU Affero General Public License version 3 (see the file LICENSE).
410+
411+"""Implementations of `IGitNamespace`."""
412+
413+__metaclass__ = type
414+__all__ = [
415+ 'GitNamespaceSet',
416+ 'PackageGitNamespace',
417+ 'PersonalGitNamespace',
418+ 'ProjectGitNamespace',
419+ ]
420+
421+from lazr.lifecycle.event import ObjectCreatedEvent
422+from storm.locals import And
423+from zope.component import getUtility
424+from zope.event import notify
425+from zope.interface import implements
426+from zope.security.proxy import removeSecurityProxy
427+
428+from lp.app.enums import (
429+ FREE_INFORMATION_TYPES,
430+ InformationType,
431+ NON_EMBARGOED_INFORMATION_TYPES,
432+ PUBLIC_INFORMATION_TYPES,
433+ )
434+from lp.app.interfaces.services import IService
435+from lp.code.errors import (
436+ GitRepositoryCreationForbidden,
437+ GitRepositoryCreatorNotMemberOfOwnerTeam,
438+ GitRepositoryCreatorNotOwner,
439+ GitRepositoryExists,
440+ InvalidNamespace,
441+ NoSuchGitRepository,
442+ )
443+from lp.code.interfaces.gitnamespace import (
444+ IGitNamespace,
445+ IGitNamespacePolicy,
446+ IGitNamespaceSet,
447+ )
448+from lp.code.interfaces.gitrepository import (
449+ IGitRepository,
450+ user_has_special_git_repository_access,
451+ )
452+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
453+from lp.code.model.branchnamespace import (
454+ BRANCH_POLICY_ALLOWED_TYPES,
455+ BRANCH_POLICY_DEFAULT_TYPES,
456+ BRANCH_POLICY_REQUIRED_GRANTS,
457+ )
458+from lp.code.model.gitrepository import GitRepository
459+from lp.registry.enums import PersonVisibility
460+from lp.registry.errors import NoSuchSourcePackageName
461+from lp.registry.interfaces.distribution import (
462+ IDistribution,
463+ IDistributionSet,
464+ NoSuchDistribution,
465+ )
466+from lp.registry.interfaces.distributionsourcepackage import (
467+ IDistributionSourcePackage,
468+ )
469+from lp.registry.interfaces.person import (
470+ IPersonSet,
471+ NoSuchPerson,
472+ )
473+from lp.registry.interfaces.pillar import IPillarNameSet
474+from lp.registry.interfaces.product import (
475+ IProduct,
476+ IProductSet,
477+ NoSuchProduct,
478+ )
479+from lp.registry.interfaces.projectgroup import IProjectGroup
480+from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
481+from lp.services.database.constants import DEFAULT
482+from lp.services.database.interfaces import IStore
483+from lp.services.propertycache import get_property_cache
484+
485+
486+class _BaseGitNamespace:
487+ """Common code for Git repository namespaces."""
488+
489+ def createRepository(self, registrant, name, information_type=None,
490+ date_created=DEFAULT):
491+ """See `IGitNamespace`."""
492+
493+ self.validateRegistrant(registrant)
494+ self.validateRepositoryName(name)
495+
496+ if information_type is None:
497+ information_type = self.getDefaultInformationType(registrant)
498+ if information_type is None:
499+ raise GitRepositoryCreationForbidden()
500+
501+ repository = GitRepository(
502+ registrant, self.owner, self.target, name, information_type,
503+ date_created)
504+ repository._reconcileAccess()
505+
506+ notify(ObjectCreatedEvent(repository))
507+
508+ return repository
509+
510+ def isNameUsed(self, repository_name):
511+ """See `IGitNamespace`."""
512+ return self.getByName(repository_name) is not None
513+
514+ def findUnusedName(self, prefix):
515+ """See `IGitNamespace`."""
516+ name = prefix
517+ count = 0
518+ while self.isNameUsed(name):
519+ count += 1
520+ name = "%s-%s" % (prefix, count)
521+ return name
522+
523+ def validateRegistrant(self, registrant):
524+ """See `IGitNamespace`."""
525+ if user_has_special_git_repository_access(registrant):
526+ return
527+ owner = self.owner
528+ if not registrant.inTeam(owner):
529+ if owner.is_team:
530+ raise GitRepositoryCreatorNotMemberOfOwnerTeam(
531+ "%s is not a member of %s"
532+ % (registrant.displayname, owner.displayname))
533+ else:
534+ raise GitRepositoryCreatorNotOwner(
535+ "%s cannot create Git repositories owned by %s"
536+ % (registrant.displayname, owner.displayname))
537+
538+ if not self.getAllowedInformationTypes(registrant):
539+ raise GitRepositoryCreationForbidden(
540+ 'You cannot create Git repositories in "%s"' % self.name)
541+
542+ def validateRepositoryName(self, name):
543+ """See `IGitNamespace`."""
544+ existing_repository = self.getByName(name)
545+ if existing_repository is not None:
546+ raise GitRepositoryExists(existing_repository)
547+
548+ # Not all code paths that lead to Git repository creation go via a
549+ # schema-validated form, so we validate the repository name here to
550+ # give a nicer error message than 'ERROR: new row for relation
551+ # "gitrepository" violates check constraint "valid_name"...'.
552+ IGitRepository['name'].validate(unicode(name))
553+
554+ def validateMove(self, repository, mover, name=None):
555+ """See `IGitNamespace`."""
556+ if name is None:
557+ name = repository.name
558+ self.validateRepositoryName(name)
559+ self.validateRegistrant(mover)
560+
561+ def moveRepository(self, repository, mover, new_name=None,
562+ rename_if_necessary=False):
563+ """See `IGitNamespace`."""
564+ # Check to see if the repository is already in this namespace.
565+ old_namespace = repository.namespace
566+ if self.name == old_namespace.name:
567+ return
568+ if new_name is None:
569+ new_name = repository.name
570+ if rename_if_necessary:
571+ new_name = self.findUnusedName(new_name)
572+ self.validateMove(repository, mover, new_name)
573+ # Remove the security proxy of the repository as the owner and
574+ # target attributes are read-only through the interface.
575+ naked_repository = removeSecurityProxy(repository)
576+ naked_repository.owner = self.owner
577+ self._retargetRepository(naked_repository)
578+ del get_property_cache(naked_repository).target
579+ naked_repository.name = new_name
580+
581+ def getRepositories(self):
582+ """See `IGitNamespace`."""
583+ return IStore(GitRepository).find(
584+ GitRepository, self._getRepositoriesClause())
585+
586+ def getByName(self, repository_name, default=None):
587+ """See `IGitNamespace`."""
588+ match = IStore(GitRepository).find(
589+ GitRepository, self._getRepositoriesClause(),
590+ GitRepository.name == repository_name).one()
591+ if match is None:
592+ match = default
593+ return match
594+
595+ def getAllowedInformationTypes(self, who=None):
596+ """See `IGitNamespace`."""
597+ raise NotImplementedError
598+
599+ def getDefaultInformationType(self, who=None):
600+ """See `IGitNamespace`."""
601+ raise NotImplementedError
602+
603+ def __eq__(self, other):
604+ """See `IGitNamespace`."""
605+ return self.target == other.target
606+
607+ def __ne__(self, other):
608+ """See `IGitNamespace`."""
609+ return not self == other
610+
611+
612+class PersonalGitNamespace(_BaseGitNamespace):
613+ """A namespace for personal repositories.
614+
615+ Repositories in this namespace have names like "~foo/+git/bar".
616+ """
617+
618+ implements(IGitNamespace, IGitNamespacePolicy)
619+
620+ def __init__(self, person):
621+ self.owner = person
622+
623+ def _getRepositoriesClause(self):
624+ return And(
625+ GitRepository.owner == self.owner,
626+ GitRepository.project == None,
627+ GitRepository.distribution == None,
628+ GitRepository.sourcepackagename == None)
629+
630+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
631+ @property
632+ def name(self):
633+ """See `IGitNamespace`."""
634+ return "~%s" % self.owner.name
635+
636+ @property
637+ def target(self):
638+ """See `IGitNamespace`."""
639+ return IHasGitRepositories(self.owner)
640+
641+ def _retargetRepository(self, repository):
642+ repository.project = None
643+ repository.distribution = None
644+ repository.sourcepackagename = None
645+
646+ @property
647+ def _is_private_team(self):
648+ return (
649+ self.owner.is_team
650+ and self.owner.visibility == PersonVisibility.PRIVATE)
651+
652+ def getAllowedInformationTypes(self, who=None):
653+ """See `IGitNamespace`."""
654+ # Private teams get private branches, everyone else gets public ones.
655+ if self._is_private_team:
656+ return NON_EMBARGOED_INFORMATION_TYPES
657+ else:
658+ return FREE_INFORMATION_TYPES
659+
660+ def getDefaultInformationType(self, who=None):
661+ """See `IGitNamespace`."""
662+ if self._is_private_team:
663+ return InformationType.PROPRIETARY
664+ else:
665+ return InformationType.PUBLIC
666+
667+
668+class ProjectGitNamespace(_BaseGitNamespace):
669+ """A namespace for project repositories.
670+
671+ This namespace is for all the repositories owned by a particular person
672+ in a particular project.
673+ """
674+
675+ implements(IGitNamespace, IGitNamespacePolicy)
676+
677+ def __init__(self, person, project):
678+ self.owner = person
679+ self.project = project
680+
681+ def _getRepositoriesClause(self):
682+ return And(
683+ GitRepository.owner == self.owner,
684+ GitRepository.project == self.project)
685+
686+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
687+ @property
688+ def name(self):
689+ """See `IGitNamespace`."""
690+ return '~%s/%s' % (self.owner.name, self.project.name)
691+
692+ @property
693+ def target(self):
694+ """See `IGitNamespace`."""
695+ return IHasGitRepositories(self.project)
696+
697+ def _retargetRepository(self, repository):
698+ repository.project = self.project
699+ repository.distribution = None
700+ repository.sourcepackagename = None
701+
702+ def getAllowedInformationTypes(self, who=None):
703+ """See `IGitNamespace`."""
704+ # Some policies require that the repository owner or current user
705+ # have full access to an information type. If it's required and the
706+ # user doesn't hold it, no information types are legal.
707+ required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
708+ self.project.branch_sharing_policy]
709+ if (required_grant is not None
710+ and not getUtility(IService, 'sharing').checkPillarAccess(
711+ [self.project], required_grant, self.owner)
712+ and (who is None
713+ or not getUtility(IService, 'sharing').checkPillarAccess(
714+ [self.project], required_grant, who))):
715+ return []
716+
717+ return BRANCH_POLICY_ALLOWED_TYPES[self.project.branch_sharing_policy]
718+
719+ def getDefaultInformationType(self, who=None):
720+ """See `IGitNamespace`."""
721+ default_type = BRANCH_POLICY_DEFAULT_TYPES[
722+ self.project.branch_sharing_policy]
723+ if default_type not in self.getAllowedInformationTypes(who):
724+ return None
725+ return default_type
726+
727+
728+class PackageGitNamespace(_BaseGitNamespace):
729+ """A namespace for distribution source package repositories.
730+
731+ This namespace is for all the repositories owned by a particular person
732+ in a particular source package in a particular distribution.
733+ """
734+
735+ implements(IGitNamespace, IGitNamespacePolicy)
736+
737+ def __init__(self, person, distro_source_package):
738+ self.owner = person
739+ self.distro_source_package = distro_source_package
740+
741+ def _getRepositoriesClause(self):
742+ dsp = self.distro_source_package
743+ return And(
744+ GitRepository.owner == self.owner,
745+ GitRepository.distribution == dsp.distribution,
746+ GitRepository.sourcepackagename == dsp.sourcepackagename)
747+
748+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
749+ @property
750+ def name(self):
751+ """See `IGitNamespace`."""
752+ dsp = self.distro_source_package
753+ return '~%s/%s/+source/%s' % (
754+ self.owner.name, dsp.distribution.name, dsp.sourcepackagename.name)
755+
756+ @property
757+ def target(self):
758+ """See `IGitNamespace`."""
759+ return IHasGitRepositories(self.distro_source_package)
760+
761+ def _retargetRepository(self, repository):
762+ dsp = self.distro_source_package
763+ repository.project = None
764+ repository.distribution = dsp.distribution
765+ repository.sourcepackagename = dsp.sourcepackagename
766+
767+ def getAllowedInformationTypes(self, who=None):
768+ """See `IGitNamespace`."""
769+ return PUBLIC_INFORMATION_TYPES
770+
771+ def getDefaultInformationType(self, who=None):
772+ """See `IGitNamespace`."""
773+ return InformationType.PUBLIC
774+
775+ def __eq__(self, other):
776+ """See `IGitNamespace`."""
777+ # We may have different DSP objects that are functionally the same.
778+ self_dsp = self.distro_source_package
779+ other_dsp = IDistributionSourcePackage(other.target)
780+ return (
781+ self_dsp.distribution == other_dsp.distribution and
782+ self_dsp.sourcepackagename == other_dsp.sourcepackagename)
783+
784+
785+class GitNamespaceSet:
786+ """Only implementation of `IGitNamespaceSet`."""
787+
788+ implements(IGitNamespaceSet)
789+
790+ def get(self, person, project=None, distribution=None,
791+ sourcepackagename=None):
792+ """See `IGitNamespaceSet`."""
793+ if project is not None:
794+ assert distribution is None and sourcepackagename is None, (
795+ "project implies no distribution or sourcepackagename. "
796+ "Got %r, %r, %r."
797+ % (project, distribution, sourcepackagename))
798+ return ProjectGitNamespace(person, project)
799+ elif distribution is not None:
800+ assert sourcepackagename is not None, (
801+ "distribution implies sourcepackagename. Got %r, %r"
802+ % (distribution, sourcepackagename))
803+ return PackageGitNamespace(
804+ person, distribution.getSourcePackage(sourcepackagename))
805+ else:
806+ return PersonalGitNamespace(person)
807+
808+ def _findOrRaise(self, error, name, finder, *args):
809+ if name is None:
810+ return None
811+ args = list(args)
812+ args.append(name)
813+ result = finder(*args)
814+ if result is None:
815+ raise error(name)
816+ return result
817+
818+ def _findPerson(self, person_name):
819+ return self._findOrRaise(
820+ NoSuchPerson, person_name, getUtility(IPersonSet).getByName)
821+
822+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
823+ def _findPillar(self, pillar_name):
824+ """Find and return the pillar with the given name.
825+
826+ If the given name is '+git' (indicating a personal repository) or
827+ None, return None.
828+
829+ :raise NoSuchProduct if there's no pillar with the given name or it
830+ is a project group.
831+ """
832+ if pillar_name == "+git":
833+ return None
834+ pillar = self._findOrRaise(
835+ NoSuchProduct, pillar_name, getUtility(IPillarNameSet).getByName)
836+ if IProjectGroup.providedBy(pillar):
837+ raise NoSuchProduct(pillar_name)
838+ return pillar
839+
840+ def _findProject(self, project_name):
841+ return self._findOrRaise(
842+ NoSuchProduct, project_name, getUtility(IProductSet).getByName)
843+
844+ def _findDistribution(self, distribution_name):
845+ return self._findOrRaise(
846+ NoSuchDistribution, distribution_name,
847+ getUtility(IDistributionSet).getByName)
848+
849+ def _findSourcePackageName(self, sourcepackagename_name):
850+ return self._findOrRaise(
851+ NoSuchSourcePackageName, sourcepackagename_name,
852+ getUtility(ISourcePackageNameSet).queryByName)
853+
854+ def _realize(self, names):
855+ """Turn a dict of object names into a dict of objects.
856+
857+ Takes the results of `IGitNamespaceSet.parse` and turns them into a
858+ dict where the values are Launchpad objects.
859+ """
860+ data = {}
861+ data["person"] = self._findPerson(names["person"])
862+ data["project"] = self._findProject(names["project"])
863+ data["distribution"] = self._findDistribution(names["distribution"])
864+ data["sourcepackagename"] = self._findSourcePackageName(
865+ names["sourcepackagename"])
866+ return data
867+
868+ def interpret(self, person, project, distribution, sourcepackagename):
869+ names = dict(
870+ person=person, project=project, distribution=distribution,
871+ sourcepackagename=sourcepackagename)
872+ data = self._realize(names)
873+ return self.get(**data)
874+
875+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
876+ def parse(self, namespace_name):
877+ """See `IGitNamespaceSet`."""
878+ data = dict(
879+ person=None, project=None, distribution=None,
880+ sourcepackagename=None)
881+ tokens = namespace_name.split("/")
882+ if len(tokens) == 1:
883+ data["person"] = tokens[0]
884+ elif len(tokens) == 2:
885+ data["person"] = tokens[0]
886+ data["project"] = tokens[1]
887+ elif len(tokens) == 4 and tokens[2] == "+source":
888+ data["person"] = tokens[0]
889+ data["distribution"] = tokens[1]
890+ data["sourcepackagename"] = tokens[3]
891+ else:
892+ raise InvalidNamespace(namespace_name)
893+ if not data["person"].startswith("~"):
894+ raise InvalidNamespace(namespace_name)
895+ data["person"] = data["person"][1:]
896+ return data
897+
898+ def lookup(self, namespace_name):
899+ """See `IGitNamespaceSet`."""
900+ names = self.parse(namespace_name)
901+ return self.interpret(**names)
902+
903+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
904+ def traverse(self, segments):
905+ """See `IGitNamespaceSet`."""
906+ traversed_segments = []
907+
908+ def get_next_segment():
909+ try:
910+ result = segments.next()
911+ except StopIteration:
912+ raise InvalidNamespace("/".join(traversed_segments))
913+ if result is None:
914+ raise AssertionError("None segment passed to traverse()")
915+ if not isinstance(result, unicode):
916+ result = result.decode("US-ASCII")
917+ traversed_segments.append(result)
918+ return result
919+
920+ person_name = get_next_segment()
921+ person = self._findPerson(person_name)
922+ pillar_name = get_next_segment()
923+ pillar = self._findPillar(pillar_name)
924+ if pillar is None:
925+ namespace = self.get(person)
926+ git_literal = pillar_name
927+ elif IProduct.providedBy(pillar):
928+ namespace = self.get(person, project=pillar)
929+ git_literal = get_next_segment()
930+ else:
931+ source_literal = get_next_segment()
932+ if source_literal != "+source":
933+ raise InvalidNamespace("/".join(traversed_segments))
934+ sourcepackagename_name = get_next_segment()
935+ sourcepackagename = self._findSourcePackageName(
936+ sourcepackagename_name)
937+ namespace = self.get(
938+ person, distribution=IDistribution(pillar),
939+ sourcepackagename=sourcepackagename)
940+ git_literal = get_next_segment()
941+ if git_literal != "+git":
942+ raise InvalidNamespace("/".join(traversed_segments))
943+ repository_name = get_next_segment()
944+ return self._findOrRaise(
945+ NoSuchGitRepository, repository_name, namespace.getByName)
946
947=== modified file 'lib/lp/code/model/gitrepository.py'
948--- lib/lp/code/model/gitrepository.py 2015-02-13 18:35:27 +0000
949+++ lib/lp/code/model/gitrepository.py 2015-02-13 18:35:27 +0000
950@@ -40,12 +40,17 @@
951 GitDefaultConflict,
952 GitTargetError,
953 )
954+from lp.code.interfaces.gitnamespace import (
955+ get_git_namespace,
956+ IGitNamespacePolicy,
957+ )
958 from lp.code.interfaces.gitrepository import (
959 GitIdentityMixin,
960 IGitRepository,
961 IGitRepositorySet,
962 user_has_special_git_repository_access,
963 )
964+from lp.registry.enums import PersonVisibility
965 from lp.registry.errors import CannotChangeInformationType
966 from lp.registry.interfaces.accesspolicy import (
967 IAccessArtifactSource,
968@@ -54,6 +59,7 @@
969 from lp.registry.interfaces.distributionsourcepackage import (
970 IDistributionSourcePackage,
971 )
972+from lp.registry.interfaces.person import IPerson
973 from lp.registry.interfaces.product import IProduct
974 from lp.registry.interfaces.role import IHasOwner
975 from lp.registry.interfaces.sharingjob import (
976@@ -178,9 +184,27 @@
977
978 def setTarget(self, target, user):
979 """See `IGitRepository`."""
980- # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
981- # place.
982- raise NotImplementedError
983+ if IPerson.providedBy(target):
984+ owner = IPerson(target)
985+ if (self.information_type in PRIVATE_INFORMATION_TYPES and
986+ (not owner.is_team or
987+ owner.visibility != PersonVisibility.PRIVATE)):
988+ raise GitTargetError(
989+ "Only private teams may have personal private "
990+ "repositories.")
991+ namespace = get_git_namespace(target, self.owner)
992+ if (self.information_type not in
993+ namespace.getAllowedInformationTypes(user)):
994+ raise GitTargetError(
995+ "%s repositories are not allowed for target %s." % (
996+ self.information_type.title, target.displayname))
997+ namespace.moveRepository(self, user, rename_if_necessary=True)
998+ self._reconcileAccess()
999+
1000+ @property
1001+ def namespace(self):
1002+ """See `IGitRepository`."""
1003+ return get_git_namespace(self.target, self.owner)
1004
1005 def setOwnerDefault(self, value):
1006 """See `IGitRepository`."""
1007@@ -290,9 +314,8 @@
1008 types = set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
1009 else:
1010 # Otherwise the permitted types are defined by the namespace.
1011- # XXX cjwatson 2015-01-19: Define permitted types properly. For
1012- # now, non-admins only get public repository access.
1013- types = set(PUBLIC_INFORMATION_TYPES)
1014+ policy = IGitNamespacePolicy(self.namespace)
1015+ types = set(policy.getAllowedInformationTypes(user))
1016 return types
1017
1018 def transitionToInformationType(self, information_type, user,
1019@@ -325,9 +348,8 @@
1020
1021 def setOwner(self, new_owner, user):
1022 """See `IGitRepository`."""
1023- # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
1024- # place.
1025- raise NotImplementedError
1026+ new_namespace = get_git_namespace(self.target, new_owner)
1027+ new_namespace.moveRepository(self, user, rename_if_necessary=True)
1028
1029 def destroySelf(self):
1030 raise NotImplementedError
1031@@ -341,9 +363,10 @@
1032 def new(self, registrant, owner, target, name, information_type=None,
1033 date_created=DEFAULT):
1034 """See `IGitRepositorySet`."""
1035- # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
1036- # place.
1037- raise NotImplementedError
1038+ namespace = get_git_namespace(target, owner)
1039+ return namespace.createRepository(
1040+ registrant, name, information_type=information_type,
1041+ date_created=date_created)
1042
1043 def getByPath(self, user, path):
1044 """See `IGitRepositorySet`."""