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

Proposed by Colin Watson
Status: Superseded
Proposed branch: lp:~cjwatson/launchpad/git-sharing
Merge into: lp:launchpad
Diff against target: 3194 lines (+2170/-115)
34 files modified
configs/development/launchpad-lazr.conf (+4/-0)
lib/lp/blueprints/model/specification.py (+2/-2)
lib/lp/blueprints/tests/test_specification.py (+4/-4)
lib/lp/bugs/model/bug.py (+3/-3)
lib/lp/code/browser/branchsubscription.py (+3/-3)
lib/lp/code/configure.zcml (+51/-0)
lib/lp/code/errors.py (+94/-0)
lib/lp/code/interfaces/gitnamespace.py (+249/-0)
lib/lp/code/interfaces/gitrepository.py (+375/-0)
lib/lp/code/interfaces/hasgitrepositories.py (+40/-0)
lib/lp/code/model/branch.py (+4/-5)
lib/lp/code/model/branchnamespace.py (+2/-0)
lib/lp/code/model/gitnamespace.py (+538/-0)
lib/lp/code/model/gitrepository.py (+428/-0)
lib/lp/code/model/hasgitrepositories.py (+28/-0)
lib/lp/code/model/tests/test_branchsubscription.py (+3/-3)
lib/lp/code/model/tests/test_hasgitrepositories.py (+34/-0)
lib/lp/registry/browser/pillar.py (+4/-2)
lib/lp/registry/configure.zcml (+5/-0)
lib/lp/registry/interfaces/accesspolicy.py (+2/-1)
lib/lp/registry/interfaces/distributionsourcepackage.py (+3/-1)
lib/lp/registry/interfaces/person.py (+2/-1)
lib/lp/registry/interfaces/product.py (+2/-1)
lib/lp/registry/interfaces/sharingservice.py (+30/-11)
lib/lp/registry/model/accesspolicy.py (+16/-6)
lib/lp/registry/model/distributionsourcepackage.py (+3/-1)
lib/lp/registry/model/person.py (+2/-1)
lib/lp/registry/model/product.py (+3/-1)
lib/lp/registry/model/sharingjob.py (+27/-3)
lib/lp/registry/services/sharingservice.py (+77/-31)
lib/lp/registry/services/tests/test_sharingservice.py (+19/-14)
lib/lp/registry/tests/test_product.py (+6/-5)
lib/lp/security.py (+86/-16)
lib/lp/services/config/schema-lazr.conf (+21/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-sharing
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+249815@code.launchpad.net

This proposal has been superseded by a proposal from 2015-02-16.

Commit message

Add sharing service support for Git repositories.

Description of the change

In https://code.launchpad.net/~cjwatson/launchpad/git-namespace/+merge/248995 I said that the next branch in this series would bring up test infrastructure. That turns out to have been a slight lie, because I remembered that creating test objects tends to require the ability to set their information type as well, so the sharing infrastructure needs to be in place. This branch sets that up. It's largely straightforward by analogy with branch handling, although I also did a bit of canonicalisation of argument ordering and ensuring that all calls to the methods in question use keyword arguments (oh for Python 3 and keyword-only arguments!).

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/development/launchpad-lazr.conf'
2--- configs/development/launchpad-lazr.conf 2014-02-27 08:39:44 +0000
3+++ configs/development/launchpad-lazr.conf 2015-02-16 13:40:19 +0000
4@@ -48,6 +48,10 @@
5 access_log: /var/tmp/bazaar.launchpad.dev/codehosting-access.log
6 blacklisted_hostnames:
7 use_forking_daemon: True
8+internal_git_endpoint: http://git.launchpad.dev:19417/
9+git_browse_root: https://git.launchpad.dev/
10+git_anon_root: git://git.launchpad.dev/
11+git_ssh_root: git+ssh://git.launchpad.dev/
12
13 [codeimport]
14 bazaar_branch_store: file:///tmp/bazaar-branches
15
16=== modified file 'lib/lp/blueprints/model/specification.py'
17--- lib/lp/blueprints/model/specification.py 2014-06-19 02:12:50 +0000
18+++ lib/lp/blueprints/model/specification.py 2015-02-16 13:40:19 +0000
19@@ -1,4 +1,4 @@
20-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
21+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
22 # GNU Affero General Public License version 3 (see the file LICENSE).
23
24 __metaclass__ = type
25@@ -758,7 +758,7 @@
26 # Grant the subscriber access if they can't see the
27 # specification.
28 service = getUtility(IService, 'sharing')
29- ignored, ignored, shared_specs = service.getVisibleArtifacts(
30+ _, _, _, shared_specs = service.getVisibleArtifacts(
31 person, specifications=[self], ignore_permissions=True)
32 if not shared_specs:
33 service.ensureAccessGrants(
34
35=== modified file 'lib/lp/blueprints/tests/test_specification.py'
36--- lib/lp/blueprints/tests/test_specification.py 2015-01-06 04:52:44 +0000
37+++ lib/lp/blueprints/tests/test_specification.py 2015-02-16 13:40:19 +0000
38@@ -1,4 +1,4 @@
39-# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
40+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
41 # GNU Affero General Public License version 3 (see the file LICENSE).
42
43 """Unit tests for Specification."""
44@@ -490,7 +490,7 @@
45 product=product, information_type=InformationType.PROPRIETARY)
46 spec.subscribe(user, subscribed_by=owner)
47 service = getUtility(IService, 'sharing')
48- ignored, ignored, shared_specs = service.getVisibleArtifacts(
49+ _, _, _, shared_specs = service.getVisibleArtifacts(
50 user, specifications=[spec])
51 self.assertEqual([spec], shared_specs)
52 # The spec is also returned by getSharedSpecifications(),
53@@ -507,7 +507,7 @@
54 service.sharePillarInformation(
55 product, user_2, owner, permissions)
56 spec.subscribe(user_2, subscribed_by=owner)
57- ignored, ignored, shared_specs = service.getVisibleArtifacts(
58+ _, _, _, shared_specs = service.getVisibleArtifacts(
59 user_2, specifications=[spec])
60 self.assertEqual([spec], shared_specs)
61 self.assertEqual(
62@@ -527,7 +527,7 @@
63 spec.subscribe(user, subscribed_by=owner)
64 spec.unsubscribe(user, unsubscribed_by=owner)
65 service = getUtility(IService, 'sharing')
66- ignored, ignored, shared_specs = service.getVisibleArtifacts(
67+ _, _, _, shared_specs = service.getVisibleArtifacts(
68 user, specifications=[spec])
69 self.assertEqual([], shared_specs)
70
71
72=== modified file 'lib/lp/bugs/model/bug.py'
73--- lib/lp/bugs/model/bug.py 2014-11-14 22:10:03 +0000
74+++ lib/lp/bugs/model/bug.py 2015-02-16 13:40:19 +0000
75@@ -1,4 +1,4 @@
76-# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
77+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
78 # GNU Affero General Public License version 3 (see the file LICENSE).
79
80 """Launchpad bug-related database table classes."""
81@@ -836,7 +836,7 @@
82 # there is at least one bugtask for which access can be checked.
83 if self.default_bugtask:
84 service = getUtility(IService, 'sharing')
85- bugs, ignored, ignored = service.getVisibleArtifacts(
86+ bugs, _, _, _ = service.getVisibleArtifacts(
87 person, bugs=[self], ignore_permissions=True)
88 if not bugs:
89 service.ensureAccessGrants(
90@@ -1774,7 +1774,7 @@
91 if information_type in PRIVATE_INFORMATION_TYPES:
92 service = getUtility(IService, 'sharing')
93 for person in (who, self.owner):
94- bugs, ignored, ignored = service.getVisibleArtifacts(
95+ bugs, _, _, _ = service.getVisibleArtifacts(
96 person, bugs=[self], ignore_permissions=True)
97 if not bugs:
98 # subscribe() isn't sufficient if a subscription
99
100=== modified file 'lib/lp/code/browser/branchsubscription.py'
101--- lib/lp/code/browser/branchsubscription.py 2014-11-28 22:07:05 +0000
102+++ lib/lp/code/browser/branchsubscription.py 2015-02-16 13:40:19 +0000
103@@ -1,4 +1,4 @@
104-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
105+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
106 # GNU Affero General Public License version 3 (see the file LICENSE).
107
108 __metaclass__ = type
109@@ -200,7 +200,7 @@
110 page_title = label = "Subscribe to branch"
111
112 def validate(self, data):
113- if data.has_key('person'):
114+ if 'person' in data:
115 person = data['person']
116 subscription = self.context.getSubscription(person)
117 if subscription is None and not self.context.userCanBeSubscribed(
118@@ -279,7 +279,7 @@
119 url = canonical_url(self.branch)
120 # If the subscriber can no longer see the branch, redirect them away.
121 service = getUtility(IService, 'sharing')
122- ignored, branches, ignored = service.getVisibleArtifacts(
123+ _, branches, _, _ = service.getVisibleArtifacts(
124 self.person, branches=[self.branch], ignore_permissions=True)
125 if not branches:
126 url = canonical_url(self.branch.target)
127
128=== modified file 'lib/lp/code/configure.zcml'
129--- lib/lp/code/configure.zcml 2015-02-09 11:38:30 +0000
130+++ lib/lp/code/configure.zcml 2015-02-16 13:40:19 +0000
131@@ -807,6 +807,57 @@
132 <adapter factory="lp.code.model.linkedbranch.PackageLinkedBranch" />
133 <adapter factory="lp.code.model.linkedbranch.DistributionPackageLinkedBranch" />
134
135+ <!-- GitRepository -->
136+
137+ <class class="lp.code.model.gitrepository.GitRepository">
138+ <require
139+ permission="launchpad.View"
140+ interface="lp.app.interfaces.launchpad.IPrivacy
141+ lp.code.interfaces.gitrepository.IGitRepositoryView
142+ lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
143+ <require
144+ permission="launchpad.Moderate"
145+ interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate"
146+ set_schema="lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
147+ <require
148+ permission="launchpad.Edit"
149+ interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" />
150+ </class>
151+ <subscriber
152+ for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent"
153+ handler="lp.code.model.gitrepository.git_repository_modified"/>
154+
155+ <!-- GitRepositorySet -->
156+
157+ <class class="lp.code.model.gitrepository.GitRepositorySet">
158+ <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
159+ </class>
160+ <securedutility
161+ class="lp.code.model.gitrepository.GitRepositorySet"
162+ provides="lp.code.interfaces.gitrepository.IGitRepositorySet">
163+ <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
164+ </securedutility>
165+
166+ <!-- GitNamespace -->
167+
168+ <class class="lp.code.model.gitnamespace.PackageGitNamespace">
169+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
170+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
171+ </class>
172+ <class class="lp.code.model.gitnamespace.PersonalGitNamespace">
173+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
174+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
175+ </class>
176+ <class class="lp.code.model.gitnamespace.ProjectGitNamespace">
177+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespace" />
178+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespacePolicy" />
179+ </class>
180+ <securedutility
181+ class="lp.code.model.gitnamespace.GitNamespaceSet"
182+ provides="lp.code.interfaces.gitnamespace.IGitNamespaceSet">
183+ <allow interface="lp.code.interfaces.gitnamespace.IGitNamespaceSet" />
184+ </securedutility>
185+
186 <lp:help-folder folder="help" name="+help-code" />
187
188 <!-- Diffs -->
189
190=== modified file 'lib/lp/code/errors.py'
191--- lib/lp/code/errors.py 2013-12-20 05:38:18 +0000
192+++ lib/lp/code/errors.py 2015-02-16 13:40:19 +0000
193@@ -28,11 +28,20 @@
194 'CodeImportNotInReviewedState',
195 'ClaimReviewFailed',
196 'DiffNotFound',
197+ 'GitDefaultConflict',
198+ 'GitRepositoryCreationException',
199+ 'GitRepositoryCreationFault',
200+ 'GitRepositoryCreationForbidden',
201+ 'GitRepositoryCreatorNotMemberOfOwnerTeam',
202+ 'GitRepositoryCreatorNotOwner',
203+ 'GitRepositoryExists',
204+ 'GitTargetError',
205 'InvalidBranchMergeProposal',
206 'InvalidMergeQueueConfig',
207 'InvalidNamespace',
208 'NoLinkedBranch',
209 'NoSuchBranch',
210+ 'NoSuchGitRepository',
211 'PrivateBranchRecipe',
212 'ReviewNotPending',
213 'StaleLastMirrored',
214@@ -312,6 +321,91 @@
215 """Raised when the user specifies an unrecognized branch type."""
216
217
218+class GitRepositoryCreationException(Exception):
219+ """Base class for Git repository creation exceptions."""
220+
221+
222+@error_status(httplib.CONFLICT)
223+class GitRepositoryExists(GitRepositoryCreationException):
224+ """Raised when creating a Git repository that already exists."""
225+
226+ def __init__(self, existing_repository):
227+ params = {
228+ "name": existing_repository.name,
229+ "context": existing_repository.namespace.name,
230+ }
231+ message = (
232+ 'A Git repository with the name "%(name)s" already exists for '
233+ '%(context)s.' % params)
234+ self.existing_repository = existing_repository
235+ GitRepositoryCreationException.__init__(self, message)
236+
237+
238+class GitRepositoryCreationForbidden(GitRepositoryCreationException):
239+ """A visibility policy forbids Git repository creation.
240+
241+ The exception is raised if the policy for the project does not allow the
242+ creator of the repository to create a repository for that project.
243+ """
244+
245+
246+@error_status(httplib.BAD_REQUEST)
247+class GitRepositoryCreatorNotMemberOfOwnerTeam(GitRepositoryCreationException):
248+ """Git repository creator is not a member of the owner team.
249+
250+ Raised when a user is attempting to create a repository and set the
251+ owner of the repository to a team that they are not a member of.
252+ """
253+
254+
255+@error_status(httplib.BAD_REQUEST)
256+class GitRepositoryCreatorNotOwner(GitRepositoryCreationException):
257+ """A user cannot create a Git repository belonging to another user.
258+
259+ Raised when a user is attempting to create a repository and set the
260+ owner of the repository to another user.
261+ """
262+
263+
264+class GitRepositoryCreationFault(GitRepositoryCreationException):
265+ """Raised when there is a hosting fault creating a Git repository."""
266+
267+
268+class GitTargetError(Exception):
269+ """Raised when there is an error determining a Git repository target."""
270+
271+
272+class NoSuchGitRepository(NameLookupFailed):
273+ """Raised when we try to load a Git repository that does not exist."""
274+
275+ _message_prefix = "No such Git repository"
276+
277+
278+@error_status(httplib.CONFLICT)
279+class GitDefaultConflict(Exception):
280+ """Raised when trying to set a Git repository as the default for
281+ something that already has a default."""
282+
283+ def __init__(self, existing_repository, target, owner=None):
284+ params = {
285+ "unique_name": existing_repository.unique_name,
286+ "target": target.displayname,
287+ "owner": owner.displayname,
288+ }
289+ if owner is None:
290+ message = (
291+ "The default repository for '%(target)s' is already set to "
292+ "%(unique_name)s." % params)
293+ else:
294+ message = (
295+ "%(owner)'s default repository for '%(target)s' is already "
296+ "set to %(unique_name)s." % params)
297+ self.existing_repository = existing_repository
298+ self.target = target
299+ self.owner = owner
300+ Exception.__init__(self, message)
301+
302+
303 @error_status(httplib.BAD_REQUEST)
304 class CodeImportNotInReviewedState(Exception):
305 """Raised when the user requests an import of a non-automatic import."""
306
307=== added file 'lib/lp/code/interfaces/gitnamespace.py'
308--- lib/lp/code/interfaces/gitnamespace.py 1970-01-01 00:00:00 +0000
309+++ lib/lp/code/interfaces/gitnamespace.py 2015-02-16 13:40:19 +0000
310@@ -0,0 +1,249 @@
311+# Copyright 2015 Canonical Ltd. This software is licensed under the
312+# GNU Affero General Public License version 3 (see the file LICENSE).
313+
314+"""Interface for a Git repository namespace."""
315+
316+__metaclass__ = type
317+__all__ = [
318+ 'get_git_namespace',
319+ 'IGitNamespace',
320+ 'IGitNamespacePolicy',
321+ 'IGitNamespaceSet',
322+ 'split_git_unique_name',
323+ ]
324+
325+from zope.component import getUtility
326+from zope.interface import (
327+ Attribute,
328+ Interface,
329+ )
330+
331+from lp.code.errors import InvalidNamespace
332+from lp.registry.interfaces.distributionsourcepackage import (
333+ IDistributionSourcePackage,
334+ )
335+from lp.registry.interfaces.product import IProduct
336+
337+
338+class IGitNamespace(Interface):
339+ """A namespace that a Git repository lives in."""
340+
341+ name = Attribute(
342+ "The name of the namespace. This is prepended to the repository name.")
343+
344+ target = Attribute("The `IHasGitRepositories` for this namespace.")
345+
346+ def createRepository(registrant, name, information_type=None,
347+ date_created=None):
348+ """Create and return an `IGitRepository` in this namespace."""
349+
350+ def isNameUsed(name):
351+ """Is 'name' already used in this namespace?"""
352+
353+ def findUnusedName(prefix):
354+ """Find an unused repository name starting with 'prefix'.
355+
356+ Note that there is no guarantee that the name returned by this method
357+ will remain unused for very long.
358+ """
359+
360+ def moveRepository(repository, mover, new_name=None,
361+ rename_if_necessary=False):
362+ """Move the repository into this namespace.
363+
364+ :param repository: The `IGitRepository` to move.
365+ :param mover: The `IPerson` doing the moving.
366+ :param new_name: A new name for the repository.
367+ :param rename_if_necessary: Rename the repository if the repository
368+ name already exists in this namespace.
369+ :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
370+ owner is a team and 'mover' is not in that team.
371+ :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
372+ individual and 'mover' is not the owner.
373+ :raises GitRepositoryCreationForbidden: if 'mover' is not allowed to
374+ create a repository in this namespace due to privacy rules.
375+ :raises GitRepositoryExists: if a repository with the new name
376+ already exists in the namespace, and 'rename_if_necessary' is
377+ False.
378+ """
379+
380+ def getRepositories():
381+ """Return the repositories in this namespace."""
382+
383+ def getByName(repository_name, default=None):
384+ """Find the repository in this namespace called 'repository_name'.
385+
386+ :return: `IGitRepository` if found, otherwise 'default'.
387+ """
388+
389+ def __eq__(other):
390+ """Is this namespace the same as another namespace?"""
391+
392+ def __ne__(other):
393+ """Is this namespace not the same as another namespace?"""
394+
395+
396+class IGitNamespacePolicy(Interface):
397+ """Methods relating to Git repository creation and validation."""
398+
399+ def getAllowedInformationTypes(who):
400+ """Get the information types that a repository in this namespace can
401+ have.
402+
403+ :param who: The user making the request.
404+ :return: A sequence of `InformationType`s.
405+ """
406+
407+ def getDefaultInformationType(who):
408+ """Get the default information type for repositories in this namespace.
409+
410+ :param who: The user to return the information type for.
411+ :return: An `InformationType`.
412+ """
413+
414+ def validateRegistrant(registrant):
415+ """Check that the registrant can create a repository in this namespace.
416+
417+ :param registrant: An `IPerson`.
418+ :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
419+ owner is a team and the registrant is not in that team.
420+ :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
421+ individual and the registrant is not the owner.
422+ :raises GitRepositoryCreationForbidden: if the registrant is not
423+ allowed to create a repository in this namespace due to privacy
424+ rules.
425+ """
426+
427+ def validateRepositoryName(name):
428+ """Check the repository `name`.
429+
430+ :param name: A branch name, either string or unicode.
431+ :raises GitRepositoryExists: if a branch with the `name` already
432+ exists in the namespace.
433+ :raises LaunchpadValidationError: if the name doesn't match the
434+ validation constraints on IGitRepository.name.
435+ """
436+
437+ def validateMove(repository, mover, name=None):
438+ """Check that 'mover' can move 'repository' into this namespace.
439+
440+ :param repository: An `IGitRepository` that might be moved.
441+ :param mover: The `IPerson` who would move it.
442+ :param name: A new name for the repository. If None, the repository
443+ name is used.
444+ :raises GitRepositoryCreatorNotMemberOfOwnerTeam: if the namespace
445+ owner is a team and 'mover' is not in that team.
446+ :raises GitRepositoryCreatorNotOwner: if the namespace owner is an
447+ individual and 'mover' is not the owner.
448+ :raises GitRepositoryCreationForbidden: if 'mover' is not allowed to
449+ create a repository in this namespace due to privacy rules.
450+ :raises GitRepositoryExists: if a repository with the new name
451+ already exists in the namespace.
452+ """
453+
454+
455+class IGitNamespaceSet(Interface):
456+ """Interface for getting Git repository namespaces."""
457+
458+ def get(person, project=None, distribution=None, sourcepackagename=None):
459+ """Return the appropriate `IGitNamespace` for the given objects."""
460+
461+ def interpret(person, project, distribution, sourcepackagename):
462+ """Like `get`, but takes names of objects.
463+
464+ :raise NoSuchPerson: If the person referred to cannot be found.
465+ :raise NoSuchProduct: If the project referred to cannot be found.
466+ :raise NoSuchDistribution: If the distribution referred to cannot be
467+ found.
468+ :raise NoSuchSourcePackageName: If the sourcepackagename referred to
469+ cannot be found.
470+ :return: An `IGitNamespace`.
471+ """
472+
473+ def parse(namespace_name):
474+ """Parse 'namespace_name' into its components.
475+
476+ The name of a namespace is actually a path containing many elements,
477+ each of which maps to a particular kind of object in Launchpad.
478+ Elements that can appear in a namespace name are: 'person',
479+ 'project', 'distribution', and 'sourcepackagename'.
480+
481+ `parse` returns a dict which maps the names of these elements (e.g.
482+ 'person', 'project') to the values of these elements (e.g. 'mark',
483+ 'firefox'). If the given path doesn't include a particular kind of
484+ element, the dict maps that element name to None.
485+
486+ For example::
487+ parse('~foo/bar') => {
488+ 'person': 'foo', 'project': 'bar', 'distribution': None,
489+ 'sourcepackagename': None,
490+ }
491+
492+ If the given 'namespace_name' cannot be parsed, then we raise an
493+ `InvalidNamespace` error.
494+
495+ :raise InvalidNamespace: If the name is too long, too short, or
496+ malformed.
497+ :return: A dict with keys matching each component in
498+ 'namespace_name'.
499+ """
500+
501+ def lookup(namespace_name):
502+ """Return the `IGitNamespace` for 'namespace_name'.
503+
504+ :raise InvalidNamespace: if namespace_name cannot be parsed.
505+ :raise NoSuchPerson: if the person referred to cannot be found.
506+ :raise NoSuchProduct: if the project referred to cannot be found.
507+ :raise NoSuchDistribution: if the distribution referred to cannot be
508+ found.
509+ :raise NoSuchSourcePackageName: if the sourcepackagename referred to
510+ cannot be found.
511+ :return: An `IGitNamespace`.
512+ """
513+
514+ def traverse(segments):
515+ """Look up the Git repository at the path given by 'segments'.
516+
517+ The iterable 'segments' will be consumed until a repository is
518+ found. As soon as a repository is found, the repository will be
519+ returned and the consumption of segments will stop. Thus, there
520+ will often be unconsumed segments that can be used for further
521+ traversal.
522+
523+ :param segments: An iterable of URL segments, a prefix of which
524+ identifies a Git repository. The first segment is the username,
525+ *not* preceded by a '~`.
526+ :raise InvalidNamespace: if there are not enough segments to define a
527+ repository.
528+ :raise NoSuchPerson: if the person referred to cannot be found.
529+ :raise NoSuchProduct: if the product or distro referred to cannot be
530+ found.
531+ :raise NoSuchDistribution: if the distribution referred to cannot be
532+ found.
533+ :raise NoSuchSourcePackageName: if the sourcepackagename referred to
534+ cannot be found.
535+ :return: `IGitRepository`.
536+ """
537+
538+
539+def get_git_namespace(target, owner):
540+ if IProduct.providedBy(target):
541+ return getUtility(IGitNamespaceSet).get(owner, project=target)
542+ elif IDistributionSourcePackage.providedBy(target):
543+ return getUtility(IGitNamespaceSet).get(
544+ owner, distribution=target.distribution,
545+ sourcepackagename=target.sourcepackagename)
546+ else:
547+ return getUtility(IGitNamespaceSet).get(owner)
548+
549+
550+# Marker for references to Git URL layouts: ##GITNAMESPACE##
551+def split_git_unique_name(unique_name):
552+ """Return the namespace and repository names of a unique name."""
553+ try:
554+ namespace_name, literal, repository_name = unique_name.rsplit("/", 2)
555+ except ValueError:
556+ raise InvalidNamespace(unique_name)
557+ if literal != "+git":
558+ raise InvalidNamespace(unique_name)
559+ return namespace_name, repository_name
560
561=== added file 'lib/lp/code/interfaces/gitrepository.py'
562--- lib/lp/code/interfaces/gitrepository.py 1970-01-01 00:00:00 +0000
563+++ lib/lp/code/interfaces/gitrepository.py 2015-02-16 13:40:19 +0000
564@@ -0,0 +1,375 @@
565+# Copyright 2015 Canonical Ltd. This software is licensed under the
566+# GNU Affero General Public License version 3 (see the file LICENSE).
567+
568+"""Git repository interfaces."""
569+
570+__metaclass__ = type
571+
572+__all__ = [
573+ 'GitIdentityMixin',
574+ 'git_repository_name_validator',
575+ 'IGitRepository',
576+ 'IGitRepositorySet',
577+ 'user_has_special_git_repository_access',
578+ ]
579+
580+import re
581+
582+from lazr.restful.fields import Reference
583+from zope.interface import (
584+ Attribute,
585+ Interface,
586+ )
587+from zope.schema import (
588+ Bool,
589+ Choice,
590+ Datetime,
591+ Int,
592+ Text,
593+ TextLine,
594+ )
595+
596+from lp import _
597+from lp.app.enums import InformationType
598+from lp.app.validators import LaunchpadValidationError
599+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
600+from lp.registry.interfaces.role import IPersonRoles
601+from lp.services.fields import (
602+ PersonChoice,
603+ PublicPersonChoice,
604+ )
605+
606+
607+GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE = _(
608+ "Git repository names must start with a number or letter. The characters "
609+ "+, -, _, . and @ are also allowed after the first character. Repository "
610+ "names must not end with \".git\".")
611+
612+
613+# This is a copy of the pattern in database/schema/patch-2209-61-0.sql.
614+# Don't change it without changing that.
615+valid_git_repository_name_pattern = re.compile(
616+ r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z")
617+
618+
619+def valid_git_repository_name(name):
620+ """Return True iff the name is valid as a Git repository name.
621+
622+ The rules for what is a valid Git repository name are described in
623+ GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE.
624+ """
625+ if (not name.endswith(".git") and
626+ valid_git_repository_name_pattern.match(name)):
627+ return True
628+ return False
629+
630+
631+def git_repository_name_validator(name):
632+ """Return True if the name is valid, or raise a LaunchpadValidationError.
633+ """
634+ if not valid_git_repository_name(name):
635+ raise LaunchpadValidationError(
636+ _("Invalid Git repository name '${name}'. ${message}",
637+ mapping={
638+ "name": name,
639+ "message": GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,
640+ }))
641+ return True
642+
643+
644+class IGitRepositoryView(Interface):
645+ """IGitRepository attributes that require launchpad.View permission."""
646+
647+ id = Int(title=_("ID"), readonly=True, required=True)
648+
649+ date_created = Datetime(
650+ title=_("Date created"), required=True, readonly=True)
651+
652+ date_last_modified = Datetime(
653+ title=_("Date last modified"), required=True, readonly=True)
654+
655+ registrant = PublicPersonChoice(
656+ title=_("Registrant"), required=True, readonly=True,
657+ vocabulary="ValidPersonOrTeam",
658+ description=_("The person who registered this Git repository."))
659+
660+ owner = PersonChoice(
661+ title=_("Owner"), required=True, readonly=False,
662+ vocabulary="AllUserTeamsParticipationPlusSelf",
663+ description=_(
664+ "The owner of this Git repository. This controls who can modify "
665+ "the repository."))
666+
667+ target = Reference(
668+ title=_("Target"), required=True, readonly=True,
669+ schema=IHasGitRepositories,
670+ description=_("The target of the repository."))
671+
672+ namespace = Attribute(
673+ "The namespace of this repository, as an `IGitNamespace`.")
674+
675+ information_type = Choice(
676+ title=_("Information Type"), vocabulary=InformationType,
677+ required=True, readonly=True, default=InformationType.PUBLIC,
678+ description=_(
679+ "The type of information contained in this repository."))
680+
681+ owner_default = Bool(
682+ title=_("Owner default"), required=True, readonly=True,
683+ description=_(
684+ "Whether this repository is the default for its owner and "
685+ "target."))
686+
687+ target_default = Bool(
688+ title=_("Target default"), required=True, readonly=True,
689+ description=_(
690+ "Whether this repository is the default for its target."))
691+
692+ unique_name = Text(
693+ title=_("Unique name"), readonly=True,
694+ description=_(
695+ "Unique name of the repository, including the owner and project "
696+ "names."))
697+
698+ displayname = Text(
699+ title=_("Display name"), readonly=True,
700+ description=_("Display name of the repository."))
701+
702+ shortened_path = Attribute(
703+ "The shortest reasonable version of the path to this repository.")
704+
705+ git_identity = Text(
706+ title=_("Git identity"), readonly=True,
707+ description=_(
708+ "If this is the default repository for some target, then this is "
709+ "'lp:' plus a shortcut version of the path via that target. "
710+ "Otherwise it is simply 'lp:' plus the unique name."))
711+
712+ def setOwnerDefault(value):
713+ """Set whether this repository is the default for its owner-target.
714+
715+ This is for internal use; the caller should ensure permission to edit
716+ the owner, should arrange to remove any existing owner-target default
717+ (including any target default with the same owner), and should check
718+ that this repository is attached to the desired target.
719+
720+ :raises Unauthorized: if lacking permission to edit the owner.
721+ :param value: True if this repository should be the owner-target
722+ default, otherwise False.
723+ """
724+
725+ def setTargetDefault(value):
726+ """Set whether this repository is the default for its target.
727+
728+ This is for internal use; the caller should ensure permission to edit
729+ the target, should arrange to remove any existing target default, and
730+ should check that this repository is attached to the desired target.
731+
732+ :raises Unauthorized: if lacking permission to edit the target.
733+ :param value: True if this repository should be the target default,
734+ otherwise False.
735+ """
736+
737+ def getCodebrowseUrl():
738+ """Construct a browsing URL for this Git repository."""
739+
740+ def visibleByUser(user):
741+ """Can the specified user see this repository?"""
742+
743+ def getAllowedInformationTypes(user):
744+ """Get a list of acceptable `InformationType`s for this repository.
745+
746+ If the user is a Launchpad admin, any type is acceptable.
747+ """
748+
749+ def getInternalPath():
750+ """Get the internal path to this repository.
751+
752+ This is used on the storage backend.
753+ """
754+
755+ def getRepositoryDefaults():
756+ """Return a sorted list of `ICanHasDefaultGitRepository` objects.
757+
758+ There is one result for each related object for which this
759+ repository is the default. For example, in the case where a
760+ repository is the default for a project and is also its owner's
761+ default repository for that project, the objects for both the
762+ project and the person-project are returned.
763+
764+ More important related objects are sorted first.
765+ """
766+
767+ def getRepositoryIdentities():
768+ """A list of aliases for a repository.
769+
770+ Returns a list of tuples of path and context object. There is at
771+ least one alias for any repository, and that is the repository
772+ itself. For default repositories, the context object is the
773+ appropriate default object.
774+
775+ Where a repository is the default for a product or a distribution
776+ source package, the repository is available through a number of
777+ different URLs. These URLs are the aliases for the repository.
778+
779+ For example, a repository which is the default for the 'fooix'
780+ project and which is also its owner's default repository for that
781+ project is accessible using:
782+ fooix - the context object is the project fooix
783+ ~fooix-owner/fooix - the context object is the person-project
784+ ~fooix-owner and fooix
785+ ~fooix-owner/fooix/+git/fooix - the unique name of the repository
786+ where the context object is the repository itself.
787+ """
788+
789+
790+class IGitRepositoryModerateAttributes(Interface):
791+ """IGitRepository attributes that can be edited by more than one community.
792+ """
793+
794+ # XXX cjwatson 2015-01-29: Add some advice about default repository
795+ # naming.
796+ name = TextLine(
797+ title=_("Name"), required=True,
798+ constraint=git_repository_name_validator,
799+ description=_(
800+ "The repository name. Keep very short, unique, and descriptive, "
801+ "because it will be used in URLs."))
802+
803+
804+class IGitRepositoryModerate(Interface):
805+ """IGitRepository methods that can be called by more than one community."""
806+
807+ def transitionToInformationType(information_type, user,
808+ verify_policy=True):
809+ """Set the information type for this repository.
810+
811+ :param information_type: The `InformationType` to transition to.
812+ :param user: The `IPerson` who is making the change.
813+ :param verify_policy: Check if the new information type complies
814+ with the `IGitNamespacePolicy`.
815+ """
816+
817+
818+class IGitRepositoryEdit(Interface):
819+ """IGitRepository methods that require launchpad.Edit permission."""
820+
821+ def setOwner(new_owner, user):
822+ """Set the owner of the repository to be `new_owner`."""
823+
824+ def setTarget(target, user):
825+ """Set the target of the repository."""
826+
827+ def destroySelf():
828+ """Delete the specified repository."""
829+
830+
831+class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes,
832+ IGitRepositoryModerate, IGitRepositoryEdit):
833+ """A Git repository."""
834+
835+ private = Bool(
836+ title=_("Repository is confidential"), required=False, readonly=True,
837+ description=_("This repository is visible only to its subscribers."))
838+
839+
840+class IGitRepositorySet(Interface):
841+ """Interface representing the set of Git repositories."""
842+
843+ def new(registrant, owner, target, name, information_type=None,
844+ date_created=None):
845+ """Create a Git repository and return it.
846+
847+ :param registrant: The `IPerson` who registered the new repository.
848+ :param owner: The `IPerson` who owns the new repository.
849+ :param target: The `IProduct`, `IDistributionSourcePackage`, or
850+ `IPerson` that the new repository is associated with.
851+ :param name: The repository name.
852+ :param information_type: Set the repository's information type to
853+ one different from the target's default. The type must conform
854+ to the target's code sharing policy. (optional)
855+ """
856+
857+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
858+ def getByPath(user, path):
859+ """Find a repository by its path.
860+
861+ Any of these forms may be used, with or without a leading slash:
862+ Unique names:
863+ ~OWNER/PROJECT/+git/NAME
864+ ~OWNER/DISTRO/+source/SOURCE/+git/NAME
865+ ~OWNER/+git/NAME
866+ Owner-target default aliases:
867+ ~OWNER/PROJECT
868+ ~OWNER/DISTRO/+source/SOURCE
869+ Official aliases:
870+ PROJECT
871+ DISTRO/+source/SOURCE
872+
873+ Return None if no match was found.
874+ """
875+
876+ def getDefaultRepository(target, owner=None):
877+ """Get the default repository for a target or owner-target.
878+
879+ :param target: An `IHasGitRepositories`.
880+ :param owner: An `IPerson`, in which case search for that person's
881+ default repository for this target; or None, in which case
882+ search for the overall default repository for this target.
883+
884+ :raises GitTargetError: if `target` is an `IPerson`.
885+ :return: An `IGitRepository`, or None.
886+ """
887+
888+ def getRepositories():
889+ """Return an empty collection of repositories.
890+
891+ This only exists to keep lazr.restful happy.
892+ """
893+
894+
895+class GitIdentityMixin:
896+ """This mixin class determines Git repository paths.
897+
898+ Used by both the model GitRepository class and the browser repository
899+ listing item. This allows the browser code to cache the associated
900+ context objects which reduces query counts.
901+ """
902+
903+ @property
904+ def shortened_path(self):
905+ """See `IGitRepository`."""
906+ path, context = self.getRepositoryIdentities()[0]
907+ return path
908+
909+ @property
910+ def git_identity(self):
911+ """See `IGitRepository`."""
912+ return "lp:" + self.shortened_path
913+
914+ def getRepositoryDefaults(self):
915+ """See `IGitRepository`."""
916+ # XXX cjwatson 2015-02-06: This will return shortcut defaults once
917+ # they're implemented.
918+ return []
919+
920+ def getRepositoryIdentities(self):
921+ """See `IGitRepository`."""
922+ identities = [
923+ (default.path, default.context)
924+ for default in self.getRepositoryDefaults()]
925+ identities.append((self.unique_name, self))
926+ return identities
927+
928+
929+def user_has_special_git_repository_access(user):
930+ """Admins have special access.
931+
932+ :param user: An `IPerson` or None.
933+ """
934+ if user is None:
935+ return False
936+ roles = IPersonRoles(user)
937+ if roles.in_admin:
938+ return True
939+ return False
940
941=== added file 'lib/lp/code/interfaces/hasgitrepositories.py'
942--- lib/lp/code/interfaces/hasgitrepositories.py 1970-01-01 00:00:00 +0000
943+++ lib/lp/code/interfaces/hasgitrepositories.py 2015-02-16 13:40:19 +0000
944@@ -0,0 +1,40 @@
945+# Copyright 2015 Canonical Ltd. This software is licensed under the
946+# GNU Affero General Public License version 3 (see the file LICENSE).
947+
948+"""Interfaces relating to targets of Git repositories."""
949+
950+__metaclass__ = type
951+
952+__all__ = [
953+ 'IHasGitRepositories',
954+ ]
955+
956+from zope.interface import Interface
957+
958+
959+class IHasGitRepositories(Interface):
960+ """An object that has related Git repositories.
961+
962+ A project contains Git repositories, a source package on a distribution
963+ contains branches, and a person contains "personal" branches.
964+ """
965+
966+ def getGitRepositories(visible_by_user=None, eager_load=False):
967+ """Returns all Git repositories related to this object.
968+
969+ :param visible_by_user: Normally the user who is asking.
970+ :param eager_load: If True, load related objects for the whole
971+ collection.
972+ :returns: A list of `IGitRepository` objects.
973+ """
974+
975+ def createGitRepository(registrant, owner, name, information_type=None):
976+ """Create a Git repository for this target and return it.
977+
978+ :param registrant: The `IPerson` who registered the new repository.
979+ :param owner: The `IPerson` who owns the new repository.
980+ :param name: The repository name.
981+ :param information_type: Set the repository's information type to
982+ one different from the target's default. The type must conform
983+ to the target's code sharing policy. (optional)
984+ """
985
986=== modified file 'lib/lp/code/model/branch.py'
987--- lib/lp/code/model/branch.py 2014-01-15 00:59:48 +0000
988+++ lib/lp/code/model/branch.py 2015-02-16 13:40:19 +0000
989@@ -1,4 +1,4 @@
990-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
991+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
992 # GNU Affero General Public License version 3 (see the file LICENSE).
993
994 __metaclass__ = type
995@@ -208,7 +208,6 @@
996 mirror_status_message = StringCol(default=None)
997 information_type = EnumCol(
998 enum=InformationType, default=InformationType.PUBLIC)
999- access_policy = IntCol()
1000
1001 @property
1002 def private(self):
1003@@ -915,7 +914,7 @@
1004 subscription.review_level = code_review_level
1005 # Grant the subscriber access if they can't see the branch.
1006 service = getUtility(IService, 'sharing')
1007- ignored, branches, ignored = service.getVisibleArtifacts(
1008+ _, branches, _, _ = service.getVisibleArtifacts(
1009 person, branches=[self], ignore_permissions=True)
1010 if not branches:
1011 service.ensureAccessGrants(
1012@@ -929,7 +928,7 @@
1013 # currently accessible to the person but which the subscribed_by user
1014 # has edit permissions for.
1015 service = getUtility(IService, 'sharing')
1016- ignored, invisible_stacked_branches = service.getInvisibleArtifacts(
1017+ _, invisible_stacked_branches, _ = service.getInvisibleArtifacts(
1018 person, branches=self.getStackedOnBranches())
1019 editable_stacked_on_branches = [
1020 branch for branch in invisible_stacked_branches
1021@@ -1661,7 +1660,7 @@
1022
1023 policy_grant_query = Coalesce(
1024 ArrayIntersects(
1025- Array(branch_class.access_policy),
1026+ Array(SQL('%s.access_policy' % branch_class.__storm_table__)),
1027 Select(
1028 ArrayAgg(AccessPolicyGrant.policy_id),
1029 tables=(AccessPolicyGrant,
1030
1031=== modified file 'lib/lp/code/model/branchnamespace.py'
1032--- lib/lp/code/model/branchnamespace.py 2015-02-09 11:38:30 +0000
1033+++ lib/lp/code/model/branchnamespace.py 2015-02-16 13:40:19 +0000
1034@@ -7,6 +7,8 @@
1035 __all__ = [
1036 'BranchNamespaceSet',
1037 'BRANCH_POLICY_ALLOWED_TYPES',
1038+ 'BRANCH_POLICY_DEFAULT_TYPES',
1039+ 'BRANCH_POLICY_REQUIRED_GRANTS',
1040 'PackageBranchNamespace',
1041 'PersonalBranchNamespace',
1042 'ProjectBranchNamespace',
1043
1044=== added file 'lib/lp/code/model/gitnamespace.py'
1045--- lib/lp/code/model/gitnamespace.py 1970-01-01 00:00:00 +0000
1046+++ lib/lp/code/model/gitnamespace.py 2015-02-16 13:40:19 +0000
1047@@ -0,0 +1,538 @@
1048+# Copyright 2015 Canonical Ltd. This software is licensed under the
1049+# GNU Affero General Public License version 3 (see the file LICENSE).
1050+
1051+"""Implementations of `IGitNamespace`."""
1052+
1053+__metaclass__ = type
1054+__all__ = [
1055+ 'GitNamespaceSet',
1056+ 'PackageGitNamespace',
1057+ 'PersonalGitNamespace',
1058+ 'ProjectGitNamespace',
1059+ ]
1060+
1061+from lazr.lifecycle.event import ObjectCreatedEvent
1062+from storm.locals import And
1063+from zope.component import getUtility
1064+from zope.event import notify
1065+from zope.interface import implements
1066+from zope.security.proxy import removeSecurityProxy
1067+
1068+from lp.app.enums import (
1069+ FREE_INFORMATION_TYPES,
1070+ InformationType,
1071+ NON_EMBARGOED_INFORMATION_TYPES,
1072+ PUBLIC_INFORMATION_TYPES,
1073+ )
1074+from lp.app.interfaces.services import IService
1075+from lp.code.errors import (
1076+ GitRepositoryCreationForbidden,
1077+ GitRepositoryCreatorNotMemberOfOwnerTeam,
1078+ GitRepositoryCreatorNotOwner,
1079+ GitRepositoryExists,
1080+ InvalidNamespace,
1081+ NoSuchGitRepository,
1082+ )
1083+from lp.code.interfaces.gitnamespace import (
1084+ IGitNamespace,
1085+ IGitNamespacePolicy,
1086+ IGitNamespaceSet,
1087+ )
1088+from lp.code.interfaces.gitrepository import (
1089+ IGitRepository,
1090+ user_has_special_git_repository_access,
1091+ )
1092+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
1093+from lp.code.model.branchnamespace import (
1094+ BRANCH_POLICY_ALLOWED_TYPES,
1095+ BRANCH_POLICY_DEFAULT_TYPES,
1096+ BRANCH_POLICY_REQUIRED_GRANTS,
1097+ )
1098+from lp.code.model.gitrepository import GitRepository
1099+from lp.registry.enums import PersonVisibility
1100+from lp.registry.errors import NoSuchSourcePackageName
1101+from lp.registry.interfaces.distribution import (
1102+ IDistribution,
1103+ IDistributionSet,
1104+ NoSuchDistribution,
1105+ )
1106+from lp.registry.interfaces.distributionsourcepackage import (
1107+ IDistributionSourcePackage,
1108+ )
1109+from lp.registry.interfaces.person import (
1110+ IPersonSet,
1111+ NoSuchPerson,
1112+ )
1113+from lp.registry.interfaces.pillar import IPillarNameSet
1114+from lp.registry.interfaces.product import (
1115+ IProduct,
1116+ IProductSet,
1117+ NoSuchProduct,
1118+ )
1119+from lp.registry.interfaces.projectgroup import IProjectGroup
1120+from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
1121+from lp.services.database.constants import DEFAULT
1122+from lp.services.database.interfaces import IStore
1123+from lp.services.propertycache import get_property_cache
1124+
1125+
1126+class _BaseGitNamespace:
1127+ """Common code for Git repository namespaces."""
1128+
1129+ def createRepository(self, registrant, name, information_type=None,
1130+ date_created=DEFAULT):
1131+ """See `IGitNamespace`."""
1132+
1133+ self.validateRegistrant(registrant)
1134+ self.validateRepositoryName(name)
1135+
1136+ if information_type is None:
1137+ information_type = self.getDefaultInformationType(registrant)
1138+ if information_type is None:
1139+ raise GitRepositoryCreationForbidden()
1140+
1141+ repository = GitRepository(
1142+ registrant, self.owner, self.target, name, information_type,
1143+ date_created)
1144+ repository._reconcileAccess()
1145+
1146+ notify(ObjectCreatedEvent(repository))
1147+
1148+ return repository
1149+
1150+ def isNameUsed(self, repository_name):
1151+ """See `IGitNamespace`."""
1152+ return self.getByName(repository_name) is not None
1153+
1154+ def findUnusedName(self, prefix):
1155+ """See `IGitNamespace`."""
1156+ name = prefix
1157+ count = 0
1158+ while self.isNameUsed(name):
1159+ count += 1
1160+ name = "%s-%s" % (prefix, count)
1161+ return name
1162+
1163+ def validateRegistrant(self, registrant):
1164+ """See `IGitNamespace`."""
1165+ if user_has_special_git_repository_access(registrant):
1166+ return
1167+ owner = self.owner
1168+ if not registrant.inTeam(owner):
1169+ if owner.is_team:
1170+ raise GitRepositoryCreatorNotMemberOfOwnerTeam(
1171+ "%s is not a member of %s"
1172+ % (registrant.displayname, owner.displayname))
1173+ else:
1174+ raise GitRepositoryCreatorNotOwner(
1175+ "%s cannot create Git repositories owned by %s"
1176+ % (registrant.displayname, owner.displayname))
1177+
1178+ if not self.getAllowedInformationTypes(registrant):
1179+ raise GitRepositoryCreationForbidden(
1180+ 'You cannot create Git repositories in "%s"' % self.name)
1181+
1182+ def validateRepositoryName(self, name):
1183+ """See `IGitNamespace`."""
1184+ existing_repository = self.getByName(name)
1185+ if existing_repository is not None:
1186+ raise GitRepositoryExists(existing_repository)
1187+
1188+ # Not all code paths that lead to Git repository creation go via a
1189+ # schema-validated form, so we validate the repository name here to
1190+ # give a nicer error message than 'ERROR: new row for relation
1191+ # "gitrepository" violates check constraint "valid_name"...'.
1192+ IGitRepository['name'].validate(unicode(name))
1193+
1194+ def validateMove(self, repository, mover, name=None):
1195+ """See `IGitNamespace`."""
1196+ if name is None:
1197+ name = repository.name
1198+ self.validateRepositoryName(name)
1199+ self.validateRegistrant(mover)
1200+
1201+ def moveRepository(self, repository, mover, new_name=None,
1202+ rename_if_necessary=False):
1203+ """See `IGitNamespace`."""
1204+ # Check to see if the repository is already in this namespace.
1205+ old_namespace = repository.namespace
1206+ if self.name == old_namespace.name:
1207+ return
1208+ if new_name is None:
1209+ new_name = repository.name
1210+ if rename_if_necessary:
1211+ new_name = self.findUnusedName(new_name)
1212+ self.validateMove(repository, mover, new_name)
1213+ # Remove the security proxy of the repository as the owner and
1214+ # target attributes are read-only through the interface.
1215+ naked_repository = removeSecurityProxy(repository)
1216+ naked_repository.owner = self.owner
1217+ self._retargetRepository(naked_repository)
1218+ del get_property_cache(naked_repository).target
1219+ naked_repository.name = new_name
1220+
1221+ def getRepositories(self):
1222+ """See `IGitNamespace`."""
1223+ return IStore(GitRepository).find(
1224+ GitRepository, self._getRepositoriesClause())
1225+
1226+ def getByName(self, repository_name, default=None):
1227+ """See `IGitNamespace`."""
1228+ match = IStore(GitRepository).find(
1229+ GitRepository, self._getRepositoriesClause(),
1230+ GitRepository.name == repository_name).one()
1231+ if match is None:
1232+ match = default
1233+ return match
1234+
1235+ def getAllowedInformationTypes(self, who=None):
1236+ """See `IGitNamespace`."""
1237+ raise NotImplementedError
1238+
1239+ def getDefaultInformationType(self, who=None):
1240+ """See `IGitNamespace`."""
1241+ raise NotImplementedError
1242+
1243+ def __eq__(self, other):
1244+ """See `IGitNamespace`."""
1245+ return self.target == other.target
1246+
1247+ def __ne__(self, other):
1248+ """See `IGitNamespace`."""
1249+ return not self == other
1250+
1251+
1252+class PersonalGitNamespace(_BaseGitNamespace):
1253+ """A namespace for personal repositories.
1254+
1255+ Repositories in this namespace have names like "~foo/+git/bar".
1256+ """
1257+
1258+ implements(IGitNamespace, IGitNamespacePolicy)
1259+
1260+ def __init__(self, person):
1261+ self.owner = person
1262+
1263+ def _getRepositoriesClause(self):
1264+ return And(
1265+ GitRepository.owner == self.owner,
1266+ GitRepository.project == None,
1267+ GitRepository.distribution == None,
1268+ GitRepository.sourcepackagename == None)
1269+
1270+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1271+ @property
1272+ def name(self):
1273+ """See `IGitNamespace`."""
1274+ return "~%s" % self.owner.name
1275+
1276+ @property
1277+ def target(self):
1278+ """See `IGitNamespace`."""
1279+ return IHasGitRepositories(self.owner)
1280+
1281+ def _retargetRepository(self, repository):
1282+ repository.project = None
1283+ repository.distribution = None
1284+ repository.sourcepackagename = None
1285+
1286+ @property
1287+ def _is_private_team(self):
1288+ return (
1289+ self.owner.is_team
1290+ and self.owner.visibility == PersonVisibility.PRIVATE)
1291+
1292+ def getAllowedInformationTypes(self, who=None):
1293+ """See `IGitNamespace`."""
1294+ # Private teams get private branches, everyone else gets public ones.
1295+ if self._is_private_team:
1296+ return NON_EMBARGOED_INFORMATION_TYPES
1297+ else:
1298+ return FREE_INFORMATION_TYPES
1299+
1300+ def getDefaultInformationType(self, who=None):
1301+ """See `IGitNamespace`."""
1302+ if self._is_private_team:
1303+ return InformationType.PROPRIETARY
1304+ else:
1305+ return InformationType.PUBLIC
1306+
1307+
1308+class ProjectGitNamespace(_BaseGitNamespace):
1309+ """A namespace for project repositories.
1310+
1311+ This namespace is for all the repositories owned by a particular person
1312+ in a particular project.
1313+ """
1314+
1315+ implements(IGitNamespace, IGitNamespacePolicy)
1316+
1317+ def __init__(self, person, project):
1318+ self.owner = person
1319+ self.project = project
1320+
1321+ def _getRepositoriesClause(self):
1322+ return And(
1323+ GitRepository.owner == self.owner,
1324+ GitRepository.project == self.project)
1325+
1326+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1327+ @property
1328+ def name(self):
1329+ """See `IGitNamespace`."""
1330+ return '~%s/%s' % (self.owner.name, self.project.name)
1331+
1332+ @property
1333+ def target(self):
1334+ """See `IGitNamespace`."""
1335+ return IHasGitRepositories(self.project)
1336+
1337+ def _retargetRepository(self, repository):
1338+ repository.project = self.project
1339+ repository.distribution = None
1340+ repository.sourcepackagename = None
1341+
1342+ def getAllowedInformationTypes(self, who=None):
1343+ """See `IGitNamespace`."""
1344+ # Some policies require that the repository owner or current user
1345+ # have full access to an information type. If it's required and the
1346+ # user doesn't hold it, no information types are legal.
1347+ required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
1348+ self.project.branch_sharing_policy]
1349+ if (required_grant is not None
1350+ and not getUtility(IService, 'sharing').checkPillarAccess(
1351+ [self.project], required_grant, self.owner)
1352+ and (who is None
1353+ or not getUtility(IService, 'sharing').checkPillarAccess(
1354+ [self.project], required_grant, who))):
1355+ return []
1356+
1357+ return BRANCH_POLICY_ALLOWED_TYPES[self.project.branch_sharing_policy]
1358+
1359+ def getDefaultInformationType(self, who=None):
1360+ """See `IGitNamespace`."""
1361+ default_type = BRANCH_POLICY_DEFAULT_TYPES[
1362+ self.project.branch_sharing_policy]
1363+ if default_type not in self.getAllowedInformationTypes(who):
1364+ return None
1365+ return default_type
1366+
1367+
1368+class PackageGitNamespace(_BaseGitNamespace):
1369+ """A namespace for distribution source package repositories.
1370+
1371+ This namespace is for all the repositories owned by a particular person
1372+ in a particular source package in a particular distribution.
1373+ """
1374+
1375+ implements(IGitNamespace, IGitNamespacePolicy)
1376+
1377+ def __init__(self, person, distro_source_package):
1378+ self.owner = person
1379+ self.distro_source_package = distro_source_package
1380+
1381+ def _getRepositoriesClause(self):
1382+ dsp = self.distro_source_package
1383+ return And(
1384+ GitRepository.owner == self.owner,
1385+ GitRepository.distribution == dsp.distribution,
1386+ GitRepository.sourcepackagename == dsp.sourcepackagename)
1387+
1388+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1389+ @property
1390+ def name(self):
1391+ """See `IGitNamespace`."""
1392+ dsp = self.distro_source_package
1393+ return '~%s/%s/+source/%s' % (
1394+ self.owner.name, dsp.distribution.name, dsp.sourcepackagename.name)
1395+
1396+ @property
1397+ def target(self):
1398+ """See `IGitNamespace`."""
1399+ return IHasGitRepositories(self.distro_source_package)
1400+
1401+ def _retargetRepository(self, repository):
1402+ dsp = self.distro_source_package
1403+ repository.project = None
1404+ repository.distribution = dsp.distribution
1405+ repository.sourcepackagename = dsp.sourcepackagename
1406+
1407+ def getAllowedInformationTypes(self, who=None):
1408+ """See `IGitNamespace`."""
1409+ return PUBLIC_INFORMATION_TYPES
1410+
1411+ def getDefaultInformationType(self, who=None):
1412+ """See `IGitNamespace`."""
1413+ return InformationType.PUBLIC
1414+
1415+ def __eq__(self, other):
1416+ """See `IGitNamespace`."""
1417+ # We may have different DSP objects that are functionally the same.
1418+ self_dsp = self.distro_source_package
1419+ other_dsp = IDistributionSourcePackage(other.target)
1420+ return (
1421+ self_dsp.distribution == other_dsp.distribution and
1422+ self_dsp.sourcepackagename == other_dsp.sourcepackagename)
1423+
1424+
1425+class GitNamespaceSet:
1426+ """Only implementation of `IGitNamespaceSet`."""
1427+
1428+ implements(IGitNamespaceSet)
1429+
1430+ def get(self, person, project=None, distribution=None,
1431+ sourcepackagename=None):
1432+ """See `IGitNamespaceSet`."""
1433+ if project is not None:
1434+ assert distribution is None and sourcepackagename is None, (
1435+ "project implies no distribution or sourcepackagename. "
1436+ "Got %r, %r, %r."
1437+ % (project, distribution, sourcepackagename))
1438+ return ProjectGitNamespace(person, project)
1439+ elif distribution is not None:
1440+ assert sourcepackagename is not None, (
1441+ "distribution implies sourcepackagename. Got %r, %r"
1442+ % (distribution, sourcepackagename))
1443+ return PackageGitNamespace(
1444+ person, distribution.getSourcePackage(sourcepackagename))
1445+ else:
1446+ return PersonalGitNamespace(person)
1447+
1448+ def _findOrRaise(self, error, name, finder, *args):
1449+ if name is None:
1450+ return None
1451+ args = list(args)
1452+ args.append(name)
1453+ result = finder(*args)
1454+ if result is None:
1455+ raise error(name)
1456+ return result
1457+
1458+ def _findPerson(self, person_name):
1459+ return self._findOrRaise(
1460+ NoSuchPerson, person_name, getUtility(IPersonSet).getByName)
1461+
1462+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1463+ def _findPillar(self, pillar_name):
1464+ """Find and return the pillar with the given name.
1465+
1466+ If the given name is '+git' (indicating a personal repository) or
1467+ None, return None.
1468+
1469+ :raise NoSuchProduct if there's no pillar with the given name or it
1470+ is a project group.
1471+ """
1472+ if pillar_name == "+git":
1473+ return None
1474+ pillar = self._findOrRaise(
1475+ NoSuchProduct, pillar_name, getUtility(IPillarNameSet).getByName)
1476+ if IProjectGroup.providedBy(pillar):
1477+ raise NoSuchProduct(pillar_name)
1478+ return pillar
1479+
1480+ def _findProject(self, project_name):
1481+ return self._findOrRaise(
1482+ NoSuchProduct, project_name, getUtility(IProductSet).getByName)
1483+
1484+ def _findDistribution(self, distribution_name):
1485+ return self._findOrRaise(
1486+ NoSuchDistribution, distribution_name,
1487+ getUtility(IDistributionSet).getByName)
1488+
1489+ def _findSourcePackageName(self, sourcepackagename_name):
1490+ return self._findOrRaise(
1491+ NoSuchSourcePackageName, sourcepackagename_name,
1492+ getUtility(ISourcePackageNameSet).queryByName)
1493+
1494+ def _realize(self, names):
1495+ """Turn a dict of object names into a dict of objects.
1496+
1497+ Takes the results of `IGitNamespaceSet.parse` and turns them into a
1498+ dict where the values are Launchpad objects.
1499+ """
1500+ data = {}
1501+ data["person"] = self._findPerson(names["person"])
1502+ data["project"] = self._findProject(names["project"])
1503+ data["distribution"] = self._findDistribution(names["distribution"])
1504+ data["sourcepackagename"] = self._findSourcePackageName(
1505+ names["sourcepackagename"])
1506+ return data
1507+
1508+ def interpret(self, person, project, distribution, sourcepackagename):
1509+ names = dict(
1510+ person=person, project=project, distribution=distribution,
1511+ sourcepackagename=sourcepackagename)
1512+ data = self._realize(names)
1513+ return self.get(**data)
1514+
1515+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1516+ def parse(self, namespace_name):
1517+ """See `IGitNamespaceSet`."""
1518+ data = dict(
1519+ person=None, project=None, distribution=None,
1520+ sourcepackagename=None)
1521+ tokens = namespace_name.split("/")
1522+ if len(tokens) == 1:
1523+ data["person"] = tokens[0]
1524+ elif len(tokens) == 2:
1525+ data["person"] = tokens[0]
1526+ data["project"] = tokens[1]
1527+ elif len(tokens) == 4 and tokens[2] == "+source":
1528+ data["person"] = tokens[0]
1529+ data["distribution"] = tokens[1]
1530+ data["sourcepackagename"] = tokens[3]
1531+ else:
1532+ raise InvalidNamespace(namespace_name)
1533+ if not data["person"].startswith("~"):
1534+ raise InvalidNamespace(namespace_name)
1535+ data["person"] = data["person"][1:]
1536+ return data
1537+
1538+ def lookup(self, namespace_name):
1539+ """See `IGitNamespaceSet`."""
1540+ names = self.parse(namespace_name)
1541+ return self.interpret(**names)
1542+
1543+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1544+ def traverse(self, segments):
1545+ """See `IGitNamespaceSet`."""
1546+ traversed_segments = []
1547+
1548+ def get_next_segment():
1549+ try:
1550+ result = segments.next()
1551+ except StopIteration:
1552+ raise InvalidNamespace("/".join(traversed_segments))
1553+ if result is None:
1554+ raise AssertionError("None segment passed to traverse()")
1555+ if not isinstance(result, unicode):
1556+ result = result.decode("US-ASCII")
1557+ traversed_segments.append(result)
1558+ return result
1559+
1560+ person_name = get_next_segment()
1561+ person = self._findPerson(person_name)
1562+ pillar_name = get_next_segment()
1563+ pillar = self._findPillar(pillar_name)
1564+ if pillar is None:
1565+ namespace = self.get(person)
1566+ git_literal = pillar_name
1567+ elif IProduct.providedBy(pillar):
1568+ namespace = self.get(person, project=pillar)
1569+ git_literal = get_next_segment()
1570+ else:
1571+ source_literal = get_next_segment()
1572+ if source_literal != "+source":
1573+ raise InvalidNamespace("/".join(traversed_segments))
1574+ sourcepackagename_name = get_next_segment()
1575+ sourcepackagename = self._findSourcePackageName(
1576+ sourcepackagename_name)
1577+ namespace = self.get(
1578+ person, distribution=IDistribution(pillar),
1579+ sourcepackagename=sourcepackagename)
1580+ git_literal = get_next_segment()
1581+ if git_literal != "+git":
1582+ raise InvalidNamespace("/".join(traversed_segments))
1583+ repository_name = get_next_segment()
1584+ return self._findOrRaise(
1585+ NoSuchGitRepository, repository_name, namespace.getByName)
1586
1587=== added file 'lib/lp/code/model/gitrepository.py'
1588--- lib/lp/code/model/gitrepository.py 1970-01-01 00:00:00 +0000
1589+++ lib/lp/code/model/gitrepository.py 2015-02-16 13:40:19 +0000
1590@@ -0,0 +1,428 @@
1591+# Copyright 2015 Canonical Ltd. This software is licensed under the
1592+# GNU Affero General Public License version 3 (see the file LICENSE).
1593+
1594+__metaclass__ = type
1595+__all__ = [
1596+ 'get_git_repository_privacy_filter',
1597+ 'GitRepository',
1598+ 'GitRepositorySet',
1599+ ]
1600+
1601+from bzrlib import urlutils
1602+import pytz
1603+from storm.expr import (
1604+ Coalesce,
1605+ Join,
1606+ Or,
1607+ Select,
1608+ SQL,
1609+ )
1610+from storm.locals import (
1611+ Bool,
1612+ DateTime,
1613+ Int,
1614+ Reference,
1615+ Unicode,
1616+ )
1617+from zope.component import getUtility
1618+from zope.interface import implements
1619+from zope.security.interfaces import Unauthorized
1620+
1621+from lp.app.enums import (
1622+ InformationType,
1623+ PRIVATE_INFORMATION_TYPES,
1624+ PUBLIC_INFORMATION_TYPES,
1625+ )
1626+from lp.app.interfaces.informationtype import IInformationType
1627+from lp.app.interfaces.launchpad import IPrivacy
1628+from lp.app.interfaces.services import IService
1629+from lp.code.errors import (
1630+ GitDefaultConflict,
1631+ GitTargetError,
1632+ )
1633+from lp.code.interfaces.gitnamespace import (
1634+ get_git_namespace,
1635+ IGitNamespacePolicy,
1636+ )
1637+from lp.code.interfaces.gitrepository import (
1638+ GitIdentityMixin,
1639+ IGitRepository,
1640+ IGitRepositorySet,
1641+ user_has_special_git_repository_access,
1642+ )
1643+from lp.registry.enums import PersonVisibility
1644+from lp.registry.errors import CannotChangeInformationType
1645+from lp.registry.interfaces.accesspolicy import (
1646+ IAccessArtifactSource,
1647+ IAccessPolicySource,
1648+ )
1649+from lp.registry.interfaces.distributionsourcepackage import (
1650+ IDistributionSourcePackage,
1651+ )
1652+from lp.registry.interfaces.person import IPerson
1653+from lp.registry.interfaces.product import IProduct
1654+from lp.registry.interfaces.role import IHasOwner
1655+from lp.registry.interfaces.sharingjob import (
1656+ IRemoveArtifactSubscriptionsJobSource,
1657+ )
1658+from lp.registry.model.accesspolicy import (
1659+ AccessPolicyGrant,
1660+ reconcile_access_for_artifact,
1661+ )
1662+from lp.registry.model.teammembership import TeamParticipation
1663+from lp.services.config import config
1664+from lp.services.database.constants import (
1665+ DEFAULT,
1666+ UTC_NOW,
1667+ )
1668+from lp.services.database.enumcol import EnumCol
1669+from lp.services.database.interfaces import IStore
1670+from lp.services.database.stormbase import StormBase
1671+from lp.services.database.stormexpr import (
1672+ Array,
1673+ ArrayAgg,
1674+ ArrayIntersects,
1675+ )
1676+from lp.services.propertycache import cachedproperty
1677+from lp.services.webapp.authorization import check_permission
1678+
1679+
1680+def git_repository_modified(repository, event):
1681+ """Update the date_last_modified property when a GitRepository is modified.
1682+
1683+ This method is registered as a subscriber to `IObjectModifiedEvent`
1684+ events on Git repositories.
1685+ """
1686+ repository.date_last_modified = UTC_NOW
1687+
1688+
1689+class GitRepository(StormBase, GitIdentityMixin):
1690+ """See `IGitRepository`."""
1691+
1692+ __storm_table__ = 'GitRepository'
1693+
1694+ implements(IGitRepository, IHasOwner, IPrivacy, IInformationType)
1695+
1696+ id = Int(primary=True)
1697+
1698+ date_created = DateTime(
1699+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
1700+ date_last_modified = DateTime(
1701+ name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
1702+
1703+ registrant_id = Int(name='registrant', allow_none=False)
1704+ registrant = Reference(registrant_id, 'Person.id')
1705+
1706+ owner_id = Int(name='owner', allow_none=False)
1707+ owner = Reference(owner_id, 'Person.id')
1708+
1709+ project_id = Int(name='project', allow_none=True)
1710+ project = Reference(project_id, 'Product.id')
1711+
1712+ distribution_id = Int(name='distribution', allow_none=True)
1713+ distribution = Reference(distribution_id, 'Distribution.id')
1714+
1715+ sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
1716+ sourcepackagename = Reference(sourcepackagename_id, 'SourcePackageName.id')
1717+
1718+ name = Unicode(name='name', allow_none=False)
1719+
1720+ information_type = EnumCol(enum=InformationType, notNull=True)
1721+ owner_default = Bool(name='owner_default', allow_none=False)
1722+ target_default = Bool(name='target_default', allow_none=False)
1723+
1724+ def __init__(self, registrant, owner, target, name, information_type,
1725+ date_created):
1726+ super(GitRepository, self).__init__()
1727+ self.registrant = registrant
1728+ self.owner = owner
1729+ self.name = name
1730+ self.information_type = information_type
1731+ self.date_created = date_created
1732+ self.date_last_modified = date_created
1733+ self.project = None
1734+ self.distribution = None
1735+ self.sourcepackagename = None
1736+ if IProduct.providedBy(target):
1737+ self.project = target
1738+ elif IDistributionSourcePackage.providedBy(target):
1739+ self.distribution = target.distribution
1740+ self.sourcepackagename = target.sourcepackagename
1741+ self.owner_default = False
1742+ self.target_default = False
1743+
1744+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
1745+ @property
1746+ def unique_name(self):
1747+ names = {"owner": self.owner.name, "repository": self.name}
1748+ if self.project is not None:
1749+ fmt = "~%(owner)s/%(project)s"
1750+ names["project"] = self.project.name
1751+ elif self.distribution is not None:
1752+ fmt = "~%(owner)s/%(distribution)s/+source/%(source)s"
1753+ names["distribution"] = self.distribution.name
1754+ names["source"] = self.sourcepackagename.name
1755+ else:
1756+ fmt = "~%(owner)s"
1757+ fmt += "/+git/%(repository)s"
1758+ return fmt % names
1759+
1760+ def __repr__(self):
1761+ return "<GitRepository %r (%d)>" % (self.unique_name, self.id)
1762+
1763+ @cachedproperty
1764+ def target(self):
1765+ """See `IGitRepository`."""
1766+ if self.project is None:
1767+ if self.distribution is None:
1768+ return self.owner
1769+ else:
1770+ return self.distribution.getSourcePackage(
1771+ self.sourcepackagename)
1772+ else:
1773+ return self.project
1774+
1775+ def setTarget(self, target, user):
1776+ """See `IGitRepository`."""
1777+ if IPerson.providedBy(target):
1778+ owner = IPerson(target)
1779+ if (self.information_type in PRIVATE_INFORMATION_TYPES and
1780+ (not owner.is_team or
1781+ owner.visibility != PersonVisibility.PRIVATE)):
1782+ raise GitTargetError(
1783+ "Only private teams may have personal private "
1784+ "repositories.")
1785+ namespace = get_git_namespace(target, self.owner)
1786+ if (self.information_type not in
1787+ namespace.getAllowedInformationTypes(user)):
1788+ raise GitTargetError(
1789+ "%s repositories are not allowed for target %s." % (
1790+ self.information_type.title, target.displayname))
1791+ namespace.moveRepository(self, user, rename_if_necessary=True)
1792+ self._reconcileAccess()
1793+
1794+ @property
1795+ def namespace(self):
1796+ """See `IGitRepository`."""
1797+ return get_git_namespace(self.target, self.owner)
1798+
1799+ def setOwnerDefault(self, value):
1800+ """See `IGitRepository`."""
1801+ if not check_permission("launchpad.Edit", self.owner):
1802+ raise Unauthorized(
1803+ "You don't have permission to change the default repository "
1804+ "for %s on '%s'." %
1805+ (self.owner.displayname, self.target.displayname))
1806+ if value:
1807+ # Check for an existing owner-target default.
1808+ existing = getUtility(IGitRepositorySet).getDefaultRepository(
1809+ self.target, owner=self.owner)
1810+ if existing is not None:
1811+ raise GitDefaultConflict(
1812+ existing, self.target, owner=self.owner)
1813+ self.owner_default = value
1814+
1815+ def setTargetDefault(self, value):
1816+ """See `IGitRepository`."""
1817+ if not check_permission("launchpad.Edit", self.target):
1818+ raise Unauthorized(
1819+ "You don't have permission to change the default repository "
1820+ "for '%s'." % self.target.displayname)
1821+ if value:
1822+ # Any target default must also be an owner-target default.
1823+ self.setOwnerDefault(True)
1824+ # Check for an existing target default.
1825+ existing = getUtility(IGitRepositorySet).getDefaultRepository(
1826+ self.target)
1827+ if existing is not None:
1828+ raise GitDefaultConflict(existing, self.target)
1829+ self.target_default = value
1830+
1831+ @property
1832+ def displayname(self):
1833+ return self.git_identity
1834+
1835+ def getInternalPath(self):
1836+ """See `IGitRepository`."""
1837+ # This may need to change later to improve support for sharding.
1838+ return str(self.id)
1839+
1840+ def getCodebrowseUrl(self):
1841+ """See `IGitRepository`."""
1842+ return urlutils.join(
1843+ config.codehosting.git_browse_root, self.unique_name)
1844+
1845+ @property
1846+ def private(self):
1847+ return self.information_type in PRIVATE_INFORMATION_TYPES
1848+
1849+ def _reconcileAccess(self):
1850+ """Reconcile the repository's sharing information.
1851+
1852+ Takes the information_type and target and makes the related
1853+ AccessArtifact and AccessPolicyArtifacts match.
1854+ """
1855+ wanted_links = None
1856+ pillars = []
1857+ # For private personal repositories, we calculate the wanted grants.
1858+ if (not self.project and not self.distribution and
1859+ not self.information_type in PUBLIC_INFORMATION_TYPES):
1860+ aasource = getUtility(IAccessArtifactSource)
1861+ [abstract_artifact] = aasource.ensure([self])
1862+ wanted_links = set(
1863+ (abstract_artifact, policy) for policy in
1864+ getUtility(IAccessPolicySource).findByTeam([self.owner]))
1865+ else:
1866+ # We haven't yet quite worked out how distribution privacy
1867+ # works, so only work for projects for now.
1868+ if self.project is not None:
1869+ pillars = [self.project]
1870+ reconcile_access_for_artifact(
1871+ self, self.information_type, pillars, wanted_links)
1872+
1873+ @cachedproperty
1874+ def _known_viewers(self):
1875+ """A set of known persons able to view this repository.
1876+
1877+ This method must return an empty set or repository searches will
1878+ trigger late evaluation. Any 'should be set on load' properties
1879+ must be done by the repository search.
1880+
1881+ If you are tempted to change this method, don't. Instead see
1882+ visibleByUser which defines the just-in-time policy for repository
1883+ visibility, and IGitCollection which honours visibility rules.
1884+ """
1885+ return set()
1886+
1887+ def visibleByUser(self, user):
1888+ """See `IGitRepository`."""
1889+ if self.information_type in PUBLIC_INFORMATION_TYPES:
1890+ return True
1891+ elif user is None:
1892+ return False
1893+ elif user.id in self._known_viewers:
1894+ return True
1895+ else:
1896+ # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is
1897+ # in place.
1898+ return False
1899+
1900+ def getAllowedInformationTypes(self, user):
1901+ """See `IGitRepository`."""
1902+ if user_has_special_git_repository_access(user):
1903+ # Admins can set any type.
1904+ types = set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
1905+ else:
1906+ # Otherwise the permitted types are defined by the namespace.
1907+ policy = IGitNamespacePolicy(self.namespace)
1908+ types = set(policy.getAllowedInformationTypes(user))
1909+ return types
1910+
1911+ def transitionToInformationType(self, information_type, user,
1912+ verify_policy=True):
1913+ """See `IGitRepository`."""
1914+ if self.information_type == information_type:
1915+ return
1916+ if (verify_policy and
1917+ information_type not in self.getAllowedInformationTypes(user)):
1918+ raise CannotChangeInformationType("Forbidden by project policy.")
1919+ self.information_type = information_type
1920+ self._reconcileAccess()
1921+ # XXX cjwatson 2015-02-05: Once we have repository subscribers, we
1922+ # need to grant them access if necessary. For now, treat the owner
1923+ # as always subscribed, which is just about enough to make the
1924+ # GitCollection tests pass.
1925+ if information_type in PRIVATE_INFORMATION_TYPES:
1926+ # Grant the subscriber access if they can't see the repository.
1927+ service = getUtility(IService, "sharing")
1928+ blind_subscribers = service.getPeopleWithoutAccess(
1929+ self, [self.owner])
1930+ if len(blind_subscribers):
1931+ service.ensureAccessGrants(
1932+ blind_subscribers, user, gitrepositories=[self],
1933+ ignore_permissions=True)
1934+ # As a result of the transition, some subscribers may no longer have
1935+ # access to the repository. We need to run a job to remove any such
1936+ # subscriptions.
1937+ getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])
1938+
1939+ def setOwner(self, new_owner, user):
1940+ """See `IGitRepository`."""
1941+ new_namespace = get_git_namespace(self.target, new_owner)
1942+ new_namespace.moveRepository(self, user, rename_if_necessary=True)
1943+
1944+ def destroySelf(self):
1945+ raise NotImplementedError
1946+
1947+
1948+class GitRepositorySet:
1949+ """See `IGitRepositorySet`."""
1950+
1951+ implements(IGitRepositorySet)
1952+
1953+ def new(self, registrant, owner, target, name, information_type=None,
1954+ date_created=DEFAULT):
1955+ """See `IGitRepositorySet`."""
1956+ namespace = get_git_namespace(target, owner)
1957+ return namespace.createRepository(
1958+ registrant, name, information_type=information_type,
1959+ date_created=date_created)
1960+
1961+ def getByPath(self, user, path):
1962+ """See `IGitRepositorySet`."""
1963+ # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
1964+ raise NotImplementedError
1965+
1966+ def getDefaultRepository(self, target, owner=None):
1967+ """See `IGitRepositorySet`."""
1968+ clauses = []
1969+ if IProduct.providedBy(target):
1970+ clauses.append(GitRepository.project == target)
1971+ elif IDistributionSourcePackage.providedBy(target):
1972+ clauses.append(GitRepository.distribution == target.distribution)
1973+ clauses.append(
1974+ GitRepository.sourcepackagename == target.sourcepackagename)
1975+ else:
1976+ raise GitTargetError(
1977+ "Personal repositories cannot be defaults for any target.")
1978+ if owner is not None:
1979+ clauses.append(GitRepository.owner == owner)
1980+ clauses.append(GitRepository.owner_default == True)
1981+ else:
1982+ clauses.append(GitRepository.target_default == True)
1983+ return IStore(GitRepository).find(GitRepository, *clauses).one()
1984+
1985+ def getRepositories(self):
1986+ """See `IGitRepositorySet`."""
1987+ return []
1988+
1989+
1990+def get_git_repository_privacy_filter(user):
1991+ public_filter = GitRepository.information_type.is_in(
1992+ PUBLIC_INFORMATION_TYPES)
1993+
1994+ if user is None:
1995+ return [public_filter]
1996+
1997+ artifact_grant_query = Coalesce(
1998+ ArrayIntersects(
1999+ SQL("GitRepository.access_grants"),
2000+ Select(
2001+ ArrayAgg(TeamParticipation.teamID),
2002+ tables=TeamParticipation,
2003+ where=(TeamParticipation.person == user)
2004+ )), False)
2005+
2006+ policy_grant_query = Coalesce(
2007+ ArrayIntersects(
2008+ Array(SQL("GitRepository.access_policy")),
2009+ Select(
2010+ ArrayAgg(AccessPolicyGrant.policy_id),
2011+ tables=(AccessPolicyGrant,
2012+ Join(TeamParticipation,
2013+ TeamParticipation.teamID ==
2014+ AccessPolicyGrant.grantee_id)),
2015+ where=(TeamParticipation.person == user)
2016+ )), False)
2017+
2018+ return [Or(public_filter, artifact_grant_query, policy_grant_query)]
2019
2020=== added file 'lib/lp/code/model/hasgitrepositories.py'
2021--- lib/lp/code/model/hasgitrepositories.py 1970-01-01 00:00:00 +0000
2022+++ lib/lp/code/model/hasgitrepositories.py 2015-02-16 13:40:19 +0000
2023@@ -0,0 +1,28 @@
2024+# Copyright 2015 Canonical Ltd. This software is licensed under the
2025+# GNU Affero General Public License version 3 (see the file LICENSE).
2026+
2027+__metaclass__ = type
2028+__all__ = [
2029+ 'HasGitRepositoriesMixin',
2030+ ]
2031+
2032+from zope.component import getUtility
2033+
2034+from lp.code.interfaces.gitrepository import IGitRepositorySet
2035+
2036+
2037+class HasGitRepositoriesMixin:
2038+ """A mixin implementation for `IHasGitRepositories`."""
2039+
2040+ def createGitRepository(self, registrant, owner, name,
2041+ information_type=None):
2042+ """See `IHasGitRepositories`."""
2043+ return getUtility(IGitRepositorySet).new(
2044+ registrant, owner, self, name,
2045+ information_type=information_type)
2046+
2047+ def getGitRepositories(self, visible_by_user=None, eager_load=False):
2048+ """See `IHasGitRepositories`."""
2049+ # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is in
2050+ # place.
2051+ raise NotImplementedError
2052
2053=== modified file 'lib/lp/code/model/tests/test_branchsubscription.py'
2054--- lib/lp/code/model/tests/test_branchsubscription.py 2012-09-19 13:22:42 +0000
2055+++ lib/lp/code/model/tests/test_branchsubscription.py 2015-02-16 13:40:19 +0000
2056@@ -1,4 +1,4 @@
2057-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
2058+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
2059 # GNU Affero General Public License version 3 (see the file LICENSE).
2060
2061 """Tests for the BranchSubscrptions model object.."""
2062@@ -133,7 +133,7 @@
2063 None, CodeReviewNotificationLevel.NOEMAIL, owner)
2064 # The stacked on branch should be visible.
2065 service = getUtility(IService, 'sharing')
2066- ignored, visible_branches, ignored = service.getVisibleArtifacts(
2067+ _, visible_branches, _, _ = service.getVisibleArtifacts(
2068 grantee, branches=[private_stacked_on_branch])
2069 self.assertContentEqual(
2070 [private_stacked_on_branch], visible_branches)
2071@@ -161,7 +161,7 @@
2072 grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
2073 None, CodeReviewNotificationLevel.NOEMAIL, owner)
2074 # The stacked on branch should not be visible.
2075- ignored, visible_branches, ignored = service.getVisibleArtifacts(
2076+ _, visible_branches, _, _ = service.getVisibleArtifacts(
2077 grantee, branches=[private_stacked_on_branch])
2078 self.assertContentEqual([], visible_branches)
2079 self.assertIn(
2080
2081=== added file 'lib/lp/code/model/tests/test_hasgitrepositories.py'
2082--- lib/lp/code/model/tests/test_hasgitrepositories.py 1970-01-01 00:00:00 +0000
2083+++ lib/lp/code/model/tests/test_hasgitrepositories.py 2015-02-16 13:40:19 +0000
2084@@ -0,0 +1,34 @@
2085+# Copyright 2015 Canonical Ltd. This software is licensed under the
2086+# GNU Affero General Public License version 3 (see the file LICENSE).
2087+
2088+"""Tests for classes that implement IHasGitRepositories."""
2089+
2090+__metaclass__ = type
2091+
2092+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
2093+from lp.testing import (
2094+ TestCaseWithFactory,
2095+ verifyObject,
2096+ )
2097+from lp.testing.layers import DatabaseFunctionalLayer
2098+
2099+
2100+class TestIHasGitRepositories(TestCaseWithFactory):
2101+ """Test that the correct objects implement the interface."""
2102+
2103+ layer = DatabaseFunctionalLayer
2104+
2105+ def test_project_implements_hasgitrepositories(self):
2106+ # Projects should implement IHasGitRepositories.
2107+ project = self.factory.makeProduct()
2108+ verifyObject(IHasGitRepositories, project)
2109+
2110+ def test_dsp_implements_hasgitrepositories(self):
2111+ # DistributionSourcePackages should implement IHasGitRepositories.
2112+ dsp = self.factory.makeDistributionSourcePackage()
2113+ verifyObject(IHasGitRepositories, dsp)
2114+
2115+ def test_person_implements_hasgitrepositories(self):
2116+ # People should implement IHasGitRepositories.
2117+ person = self.factory.makePerson()
2118+ verifyObject(IHasGitRepositories, person)
2119
2120=== modified file 'lib/lp/registry/browser/pillar.py'
2121--- lib/lp/registry/browser/pillar.py 2014-11-24 01:20:26 +0000
2122+++ lib/lp/registry/browser/pillar.py 2015-02-16 13:40:19 +0000
2123@@ -1,4 +1,4 @@
2124-# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2125+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2126 # GNU Affero General Public License version 3 (see the file LICENSE).
2127
2128 """Common views for objects that implement `IPillar`."""
2129@@ -444,12 +444,14 @@
2130 def _loadSharedArtifacts(self):
2131 # As a concrete can by linked via more than one policy, we use sets to
2132 # filter out dupes.
2133- self.bugtasks, self.branches, self.specifications = (
2134+ (self.bugtasks, self.branches, self.gitrepositories,
2135+ self.specifications) = (
2136 self.sharing_service.getSharedArtifacts(
2137 self.pillar, self.person, self.user))
2138 bug_ids = set([bugtask.bug.id for bugtask in self.bugtasks])
2139 self.shared_bugs_count = len(bug_ids)
2140 self.shared_branches_count = len(self.branches)
2141+ self.shared_gitrepositories_count = len(self.gitrepositories)
2142 self.shared_specifications_count = len(self.specifications)
2143
2144 def _build_specification_template_data(self, specs, request):
2145
2146=== modified file 'lib/lp/registry/configure.zcml'
2147--- lib/lp/registry/configure.zcml 2015-02-09 17:42:48 +0000
2148+++ lib/lp/registry/configure.zcml 2015-02-16 13:40:19 +0000
2149@@ -556,6 +556,11 @@
2150 bug_reporting_guidelines
2151 enable_bugfiling_duplicate_search
2152 "/>
2153+
2154+ <!-- IHasGitRepositories -->
2155+
2156+ <allow
2157+ interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositories" />
2158 </class>
2159 <adapter
2160 provides="lp.registry.interfaces.distribution.IDistribution"
2161
2162=== modified file 'lib/lp/registry/interfaces/accesspolicy.py'
2163--- lib/lp/registry/interfaces/accesspolicy.py 2012-09-21 11:41:56 +0000
2164+++ lib/lp/registry/interfaces/accesspolicy.py 2015-02-16 13:40:19 +0000
2165@@ -1,4 +1,4 @@
2166-# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
2167+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
2168 # GNU Affero General Public License version 3 (see the file LICENSE).
2169
2170 """Interfaces for pillar and artifact access policies."""
2171@@ -35,6 +35,7 @@
2172 concrete_artifact = Attribute("Concrete artifact")
2173 bug_id = Attribute("bug_id")
2174 branch_id = Attribute("branch_id")
2175+ gitrepository_id = Attribute("gitrepository_id")
2176 specification_id = Attribute("specification_id")
2177
2178
2179
2180=== modified file 'lib/lp/registry/interfaces/distributionsourcepackage.py'
2181--- lib/lp/registry/interfaces/distributionsourcepackage.py 2014-11-28 22:28:40 +0000
2182+++ lib/lp/registry/interfaces/distributionsourcepackage.py 2015-02-16 13:40:19 +0000
2183@@ -34,6 +34,7 @@
2184 IHasBranches,
2185 IHasMergeProposals,
2186 )
2187+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
2188 from lp.registry.interfaces.distribution import IDistribution
2189 from lp.registry.interfaces.role import IHasDrivers
2190 from lp.soyuz.enums import ArchivePurpose
2191@@ -42,7 +43,8 @@
2192 class IDistributionSourcePackage(IHeadingContext, IBugTarget, IHasBranches,
2193 IHasMergeProposals, IHasOfficialBugTags,
2194 IStructuralSubscriptionTarget,
2195- IQuestionTarget, IHasDrivers):
2196+ IQuestionTarget, IHasDrivers,
2197+ IHasGitRepositories):
2198 """Represents a source package in a distribution.
2199
2200 Create IDistributionSourcePackages by invoking
2201
2202=== modified file 'lib/lp/registry/interfaces/person.py'
2203--- lib/lp/registry/interfaces/person.py 2015-01-30 18:24:07 +0000
2204+++ lib/lp/registry/interfaces/person.py 2015-02-16 13:40:19 +0000
2205@@ -111,6 +111,7 @@
2206 IHasMergeProposals,
2207 IHasRequestedReviews,
2208 )
2209+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
2210 from lp.code.interfaces.hasrecipes import IHasRecipes
2211 from lp.registry.enums import (
2212 EXCLUSIVE_TEAM_POLICY,
2213@@ -688,7 +689,7 @@
2214 IHasMergeProposals, IHasMugshot,
2215 IHasLocation, IHasRequestedReviews, IObjectWithLocation,
2216 IHasBugs, IHasRecipes, IHasTranslationImports,
2217- IPersonSettings, IQuestionsPerson):
2218+ IPersonSettings, IQuestionsPerson, IHasGitRepositories):
2219 """IPerson attributes that require launchpad.View permission."""
2220 account = Object(schema=IAccount)
2221 accountID = Int(title=_('Account ID'), required=True, readonly=True)
2222
2223=== modified file 'lib/lp/registry/interfaces/product.py'
2224--- lib/lp/registry/interfaces/product.py 2015-01-30 18:24:07 +0000
2225+++ lib/lp/registry/interfaces/product.py 2015-02-16 13:40:19 +0000
2226@@ -102,6 +102,7 @@
2227 IHasCodeImports,
2228 IHasMergeProposals,
2229 )
2230+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
2231 from lp.code.interfaces.hasrecipes import IHasRecipes
2232 from lp.registry.enums import (
2233 BranchSharingPolicy,
2234@@ -475,7 +476,7 @@
2235 IHasMugshot, IHasSprints, IHasTranslationImports,
2236 ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
2237 IOfficialBugTagTargetPublic, IHasOOPSReferences,
2238- IHasRecipes, IHasCodeImports, IServiceUsage):
2239+ IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories):
2240 """Public IProduct properties."""
2241
2242 registrant = exported(
2243
2244=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
2245--- lib/lp/registry/interfaces/sharingservice.py 2015-02-06 15:17:07 +0000
2246+++ lib/lp/registry/interfaces/sharingservice.py 2015-02-16 13:40:19 +0000
2247@@ -1,4 +1,4 @@
2248-# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
2249+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2250 # GNU Affero General Public License version 3 (see the file LICENSE).
2251
2252 """Interfaces for sharing service."""
2253@@ -108,7 +108,7 @@
2254
2255 :param user: the user making the request. Only artifacts visible to the
2256 user will be included in the result.
2257- :return: a (bugtasks, branches, specifications) tuple
2258+ :return: a (bugtasks, branches, gitrepositories, specifications) tuple
2259 """
2260
2261 def checkPillarArtifactAccess(pillar, user):
2262@@ -148,6 +148,14 @@
2263 :return: a collection of branches
2264 """
2265
2266+ def getSharedGitRepositories(pillar, person, user):
2267+ """Return the Git repositories shared between the pillar and person.
2268+
2269+ :param user: the user making the request. Only Git repositories
2270+ visible to the user will be included in the result.
2271+ :return: a collection of Git repositories.
2272+ """
2273+
2274 @export_read_operation()
2275 @call_with(user=REQUEST_USER)
2276 @operation_parameters(
2277@@ -163,19 +171,25 @@
2278 :return: a collection of specifications.
2279 """
2280
2281- def getVisibleArtifacts(person, branches=None, bugs=None):
2282+ def getVisibleArtifacts(person, bugs=None, branches=None,
2283+ gitrepositories=None, specifications=None):
2284 """Return the artifacts shared with person.
2285
2286 Given lists of artifacts, return those a person has access to either
2287 via a policy grant or artifact grant.
2288
2289 :param person: the person whose access is being checked.
2290+ :param bugs: the bugs to check for which a person has access.
2291 :param branches: the branches to check for which a person has access.
2292- :param bugs: the bugs to check for which a person has access.
2293+ :param gitrepositories: the Git repositories to check for which a
2294+ person has access.
2295+ :param specifications: the specifications to check for which a
2296+ person has access.
2297 :return: a collection of artifacts the person can see.
2298 """
2299
2300- def getInvisibleArtifacts(person, branches=None, bugs=None):
2301+ def getInvisibleArtifacts(person, bugs=None, branches=None,
2302+ gitrepositories=None):
2303 """Return the artifacts which are not shared with person.
2304
2305 Given lists of artifacts, return those a person does not have access to
2306@@ -184,8 +198,10 @@
2307 access to private information. Internal use only. *
2308
2309 :param person: the person whose access is being checked.
2310+ :param bugs: the bugs to check for which a person has access.
2311 :param branches: the branches to check for which a person has access.
2312- :param bugs: the bugs to check for which a person has access.
2313+ :param gitrepositories: the Git repositories to check for which a
2314+ person has access.
2315 :return: a collection of artifacts the person can not see.
2316 """
2317
2318@@ -304,10 +320,11 @@
2319 branches=List(
2320 Reference(schema=IBranch), title=_('Branches'), required=False),
2321 specifications=List(
2322- Reference(schema=ISpecification), title=_('Specifications'), required=False))
2323+ Reference(schema=ISpecification), title=_('Specifications'),
2324+ required=False))
2325 @operation_for_version('devel')
2326- def revokeAccessGrants(pillar, grantee, user, branches=None, bugs=None,
2327- specifications=None):
2328+ def revokeAccessGrants(pillar, grantee, user, bugs=None, branches=None,
2329+ gitrepositories=None, specifications=None):
2330 """Remove a grantee's access to the specified artifacts.
2331
2332 :param pillar: the pillar from which to remove access
2333@@ -315,6 +332,7 @@
2334 :param user: the user making the request
2335 :param bugs: the bugs for which to revoke access
2336 :param branches: the branches for which to revoke access
2337+ :param gitrepositories: the Git repositories for which to revoke access
2338 :param specifications: the specifications for which to revoke access
2339 """
2340
2341@@ -328,14 +346,15 @@
2342 branches=List(
2343 Reference(schema=IBranch), title=_('Branches'), required=False))
2344 @operation_for_version('devel')
2345- def ensureAccessGrants(grantees, user, branches=None, bugs=None,
2346- specifications=None):
2347+ def ensureAccessGrants(grantees, user, bugs=None, branches=None,
2348+ gitrepositories=None, specifications=None):
2349 """Ensure a grantee has an access grant to the specified artifacts.
2350
2351 :param grantees: the people or teams for whom to grant access
2352 :param user: the user making the request
2353 :param bugs: the bugs for which to grant access
2354 :param branches: the branches for which to grant access
2355+ :param gitrepositories: the Git repositories for which to grant access
2356 :param specifications: the specifications for which to grant access
2357 """
2358
2359
2360=== modified file 'lib/lp/registry/model/accesspolicy.py'
2361--- lib/lp/registry/model/accesspolicy.py 2013-06-20 05:50:00 +0000
2362+++ lib/lp/registry/model/accesspolicy.py 2015-02-16 13:40:19 +0000
2363@@ -1,4 +1,4 @@
2364-# Copyright 2011-2012 Canonical Ltd. This software is licensed under the
2365+# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
2366 # GNU Affero General Public License version 3 (see the file LICENSE).
2367
2368 """Model classes for pillar and artifact access policies."""
2369@@ -98,12 +98,16 @@
2370 bug = Reference(bug_id, 'Bug.id')
2371 branch_id = Int(name='branch')
2372 branch = Reference(branch_id, 'Branch.id')
2373+ gitrepository_id = Int(name='gitrepository')
2374+ gitrepository = Reference(gitrepository_id, 'GitRepository.id')
2375 specification_id = Int(name='specification')
2376 specification = Reference(specification_id, 'Specification.id')
2377
2378 @property
2379 def concrete_artifact(self):
2380- artifact = self.bug or self.branch or self.specification
2381+ artifact = (
2382+ self.bug or self.branch or self.gitrepository or
2383+ self.specification)
2384 return artifact
2385
2386 @classmethod
2387@@ -111,10 +115,13 @@
2388 from lp.blueprints.interfaces.specification import ISpecification
2389 from lp.bugs.interfaces.bug import IBug
2390 from lp.code.interfaces.branch import IBranch
2391+ from lp.code.interfaces.gitrepository import IGitRepository
2392 if IBug.providedBy(concrete_artifact):
2393 col = cls.bug
2394 elif IBranch.providedBy(concrete_artifact):
2395 col = cls.branch
2396+ elif IGitRepository.providedBy(concrete_artifact):
2397+ col = cls.gitrepository
2398 elif ISpecification.providedBy(concrete_artifact):
2399 col = cls.specification
2400 else:
2401@@ -137,6 +144,7 @@
2402 from lp.blueprints.interfaces.specification import ISpecification
2403 from lp.bugs.interfaces.bug import IBug
2404 from lp.code.interfaces.branch import IBranch
2405+ from lp.code.interfaces.gitrepository import IGitRepository
2406
2407 existing = list(cls.find(concrete_artifacts))
2408 if len(existing) == len(concrete_artifacts):
2409@@ -150,15 +158,17 @@
2410 insert_values = []
2411 for concrete in needed:
2412 if IBug.providedBy(concrete):
2413- insert_values.append((concrete, None, None))
2414+ insert_values.append((concrete, None, None, None))
2415 elif IBranch.providedBy(concrete):
2416- insert_values.append((None, concrete, None))
2417+ insert_values.append((None, concrete, None, None))
2418+ elif IGitRepository.providedBy(concrete):
2419+ insert_values.append((None, None, concrete, None))
2420 elif ISpecification.providedBy(concrete):
2421- insert_values.append((None, None, concrete))
2422+ insert_values.append((None, None, None, concrete))
2423 else:
2424 raise ValueError("%r is not a supported artifact" % concrete)
2425 new = create(
2426- (cls.bug, cls.branch, cls.specification),
2427+ (cls.bug, cls.branch, cls.gitrepository, cls.specification),
2428 insert_values, get_objects=True)
2429 return list(existing) + new
2430
2431
2432=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
2433--- lib/lp/registry/model/distributionsourcepackage.py 2014-11-27 20:52:37 +0000
2434+++ lib/lp/registry/model/distributionsourcepackage.py 2015-02-16 13:40:19 +0000
2435@@ -43,6 +43,7 @@
2436 HasBranchesMixin,
2437 HasMergeProposalsMixin,
2438 )
2439+from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
2440 from lp.registry.interfaces.distributionsourcepackage import (
2441 IDistributionSourcePackage,
2442 )
2443@@ -119,7 +120,8 @@
2444 HasBranchesMixin,
2445 HasCustomLanguageCodesMixin,
2446 HasMergeProposalsMixin,
2447- HasDriversMixin):
2448+ HasDriversMixin,
2449+ HasGitRepositoriesMixin):
2450 """This is a "Magic Distribution Source Package". It is not an
2451 SQLObject, but instead it represents a source package with a particular
2452 name in a particular distribution. You can then ask it all sorts of
2453
2454=== modified file 'lib/lp/registry/model/person.py'
2455--- lib/lp/registry/model/person.py 2015-01-28 16:10:51 +0000
2456+++ lib/lp/registry/model/person.py 2015-02-16 13:40:19 +0000
2457@@ -146,6 +146,7 @@
2458 HasMergeProposalsMixin,
2459 HasRequestedReviewsMixin,
2460 )
2461+from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
2462 from lp.registry.enums import (
2463 EXCLUSIVE_TEAM_POLICY,
2464 INCLUSIVE_TEAM_POLICY,
2465@@ -476,7 +477,7 @@
2466 class Person(
2467 SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,
2468 HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,
2469- QuestionsPersonMixin):
2470+ QuestionsPersonMixin, HasGitRepositoriesMixin):
2471 """A Person."""
2472
2473 implements(IPerson, IHasIcon, IHasLogo, IHasMugshot)
2474
2475=== modified file 'lib/lp/registry/model/product.py'
2476--- lib/lp/registry/model/product.py 2015-01-29 16:28:30 +0000
2477+++ lib/lp/registry/model/product.py 2015-02-16 13:40:19 +0000
2478@@ -124,6 +124,7 @@
2479 HasCodeImportsMixin,
2480 HasMergeProposalsMixin,
2481 )
2482+from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
2483 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
2484 from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
2485 from lp.registry.enums import (
2486@@ -361,7 +362,8 @@
2487 OfficialBugTagTargetMixin, HasBranchesMixin,
2488 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
2489 HasCodeImportsMixin, InformationTypeMixin,
2490- TranslationPolicyMixin):
2491+ TranslationPolicyMixin,
2492+ HasGitRepositoriesMixin):
2493 """A Product."""
2494
2495 implements(
2496
2497=== modified file 'lib/lp/registry/model/sharingjob.py'
2498--- lib/lp/registry/model/sharingjob.py 2013-07-04 08:32:03 +0000
2499+++ lib/lp/registry/model/sharingjob.py 2015-02-16 13:40:19 +0000
2500@@ -1,4 +1,4 @@
2501-# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
2502+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2503 # GNU Affero General Public License version 3 (see the file LICENSE).
2504
2505 """Job classes related to the sharing feature are in here."""
2506@@ -10,7 +10,6 @@
2507 'RemoveArtifactSubscriptionsJob',
2508 ]
2509
2510-import contextlib
2511 import logging
2512
2513 from lazr.delegates import delegates
2514@@ -58,11 +57,13 @@
2515 from lp.bugs.model.bugtasksearch import get_bug_privacy_filter_terms
2516 from lp.code.interfaces.branch import IBranch
2517 from lp.code.interfaces.branchlookup import IBranchLookup
2518+from lp.code.interfaces.gitrepository import IGitRepository
2519 from lp.code.model.branch import (
2520 Branch,
2521 get_branch_privacy_filter,
2522 )
2523 from lp.code.model.branchsubscription import BranchSubscription
2524+from lp.code.model.gitrepository import GitRepository
2525 from lp.registry.interfaces.person import IPersonSet
2526 from lp.registry.interfaces.product import IProduct
2527 from lp.registry.interfaces.sharingjob import (
2528@@ -85,7 +86,6 @@
2529 )
2530 from lp.services.job.runner import BaseRunnableJob
2531 from lp.services.mail.sendmail import format_address_for_person
2532-from lp.services.webapp import errorlog
2533
2534
2535 class SharingJobType(DBEnumeratedType):
2536@@ -265,6 +265,7 @@
2537
2538 bug_ids = []
2539 branch_ids = []
2540+ gitrepository_ids = []
2541 specification_ids = []
2542 if artifacts:
2543 for artifact in artifacts:
2544@@ -272,6 +273,8 @@
2545 bug_ids.append(artifact.id)
2546 elif IBranch.providedBy(artifact):
2547 branch_ids.append(artifact.id)
2548+ elif IGitRepository.providedBy(artifact):
2549+ gitrepository_ids.append(artifact.id)
2550 elif ISpecification.providedBy(artifact):
2551 specification_ids.append(artifact.id)
2552 else:
2553@@ -283,6 +286,7 @@
2554 metadata = {
2555 'bug_ids': bug_ids,
2556 'branch_ids': branch_ids,
2557+ 'gitrepository_ids': gitrepository_ids,
2558 'specification_ids': specification_ids,
2559 'information_types': information_types,
2560 'requestor.id': requestor.id
2561@@ -315,6 +319,10 @@
2562 return [getUtility(IBranchLookup).get(id) for id in self.branch_ids]
2563
2564 @property
2565+ def gitrepository_ids(self):
2566+ return self.metadata.get('gitrepository_ids', [])
2567+
2568+ @property
2569 def specification_ids(self):
2570 return self.metadata.get('specification_ids', [])
2571
2572@@ -343,6 +351,7 @@
2573 'requestor': self.requestor.name,
2574 'bug_ids': self.bug_ids,
2575 'branch_ids': self.branch_ids,
2576+ 'gitrepository_ids': self.gitrepository_ids,
2577 'specification_ids': self.specification_ids,
2578 'pillar': getattr(self.pillar, 'name', None),
2579 'grantee': getattr(self.grantee, 'name', None)
2580@@ -358,10 +367,14 @@
2581
2582 bug_filters = []
2583 branch_filters = []
2584+ gitrepository_filters = []
2585 specification_filters = []
2586
2587 if self.branch_ids:
2588 branch_filters.append(Branch.id.is_in(self.branch_ids))
2589+ if self.gitrepository_ids:
2590+ gitrepository_filters.append(GitRepository.id.is_in(
2591+ self.gitrepository_ids))
2592 if self.specification_ids:
2593 specification_filters.append(Specification.id.is_in(
2594 self.specification_ids))
2595@@ -374,6 +387,9 @@
2596 self.information_types))
2597 branch_filters.append(
2598 Branch.information_type.is_in(self.information_types))
2599+ gitrepository_filters.append(
2600+ GitRepository.information_type.is_in(
2601+ self.information_types))
2602 specification_filters.append(
2603 Specification.information_type.is_in(
2604 self.information_types))
2605@@ -381,12 +397,16 @@
2606 bug_filters.append(
2607 BugTaskFlat.product == self.product)
2608 branch_filters.append(Branch.product == self.product)
2609+ gitrepository_filters.append(
2610+ GitRepository.project == self.product)
2611 specification_filters.append(
2612 Specification.product == self.product)
2613 if self.distro:
2614 bug_filters.append(
2615 BugTaskFlat.distribution == self.distro)
2616 branch_filters.append(Branch.distribution == self.distro)
2617+ gitrepository_filters.append(
2618+ GitRepository.distribution == self.distro)
2619 specification_filters.append(
2620 Specification.distribution == self.distro)
2621
2622@@ -401,6 +421,8 @@
2623 Select(
2624 TeamParticipation.personID,
2625 where=TeamParticipation.team == self.grantee)))
2626+ # XXX cjwatson 2015-02-05: Fill this in once we have
2627+ # GitRepositorySubscription.
2628 specification_filters.append(
2629 In(SpecificationSubscription.personID,
2630 Select(
2631@@ -430,6 +452,8 @@
2632 for sub in branch_subscriptions:
2633 sub.branch.unsubscribe(
2634 sub.person, self.requestor, ignore_permissions=True)
2635+ # XXX cjwatson 2015-02-05: Fill this in once we have
2636+ # GitRepositorySubscription.
2637 if specification_filters:
2638 specification_filters.append(Not(*get_specification_privacy_filter(
2639 SpecificationSubscription.personID)))
2640
2641=== modified file 'lib/lp/registry/services/sharingservice.py'
2642--- lib/lp/registry/services/sharingservice.py 2013-06-20 05:50:00 +0000
2643+++ lib/lp/registry/services/sharingservice.py 2015-02-16 13:40:19 +0000
2644@@ -1,4 +1,4 @@
2645-# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
2646+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2647 # GNU Affero General Public License version 3 (see the file LICENSE).
2648
2649 """Classes for pillar and artifact sharing service."""
2650@@ -194,10 +194,12 @@
2651
2652 @available_with_permission('launchpad.Driver', 'pillar')
2653 def getSharedArtifacts(self, pillar, person, user, include_bugs=True,
2654- include_branches=True, include_specifications=True):
2655+ include_branches=True, include_gitrepositories=True,
2656+ include_specifications=True):
2657 """See `ISharingService`."""
2658 bug_ids = set()
2659 branch_ids = set()
2660+ gitrepository_ids = set()
2661 specification_ids = set()
2662 for artifact in self.getArtifactGrantsForPersonOnPillar(
2663 pillar, person):
2664@@ -205,6 +207,8 @@
2665 bug_ids.add(artifact.bug_id)
2666 elif artifact.branch_id and include_branches:
2667 branch_ids.add(artifact.branch_id)
2668+ elif artifact.gitrepository_id and include_gitrepositories:
2669+ gitrepository_ids.add(artifact.gitrepository_id)
2670 elif artifact.specification_id and include_specifications:
2671 specification_ids.add(artifact.specification_id)
2672
2673@@ -221,11 +225,14 @@
2674 wanted_branches = all_branches.visibleByUser(user).withIds(
2675 *branch_ids)
2676 branches = list(wanted_branches.getBranches())
2677+ # Load the Git repositories.
2678+ gitrepositories = []
2679+ # XXX cjwatson 2015-02-16: Fill in once IGitCollection is in place.
2680 specifications = []
2681 if specification_ids:
2682 specifications = load(Specification, specification_ids)
2683
2684- return bugtasks, branches, specifications
2685+ return bugtasks, branches, gitrepositories, specifications
2686
2687 def checkPillarArtifactAccess(self, pillar, user):
2688 """See `ISharingService`."""
2689@@ -245,25 +252,33 @@
2690 @available_with_permission('launchpad.Driver', 'pillar')
2691 def getSharedBugs(self, pillar, person, user):
2692 """See `ISharingService`."""
2693- bugtasks, ignore, ignore = self.getSharedArtifacts(
2694+ bugtasks, _, _, _ = self.getSharedArtifacts(
2695 pillar, person, user, include_branches=False,
2696- include_specifications=False)
2697+ include_gitrepositories=False, include_specifications=False)
2698 return bugtasks
2699
2700 @available_with_permission('launchpad.Driver', 'pillar')
2701 def getSharedBranches(self, pillar, person, user):
2702 """See `ISharingService`."""
2703- ignore, branches, ignore = self.getSharedArtifacts(
2704+ _, branches, _, _ = self.getSharedArtifacts(
2705 pillar, person, user, include_bugs=False,
2706- include_specifications=False)
2707+ include_gitrepositories=False, include_specifications=False)
2708 return branches
2709
2710 @available_with_permission('launchpad.Driver', 'pillar')
2711+ def getSharedGitRepositories(self, pillar, person, user):
2712+ """See `ISharingService`."""
2713+ _, _, gitrepositories, _ = self.getSharedArtifacts(
2714+ pillar, person, user, include_bugs=False, include_branches=False,
2715+ include_specifications=False)
2716+ return gitrepositories
2717+
2718+ @available_with_permission('launchpad.Driver', 'pillar')
2719 def getSharedSpecifications(self, pillar, person, user):
2720 """See `ISharingService`."""
2721- ignore, ignore, specifications = self.getSharedArtifacts(
2722- pillar, person, user, include_bugs=False,
2723- include_branches=False)
2724+ _, _, _, specifications = self.getSharedArtifacts(
2725+ pillar, person, user, include_bugs=False, include_branches=False,
2726+ include_gitrepositories=False)
2727 return specifications
2728
2729 def _getVisiblePrivateSpecificationIDs(self, person, specifications):
2730@@ -300,11 +315,13 @@
2731 TeamParticipation.personID == person.id,
2732 In(Specification.id, spec_ids)))
2733
2734- def getVisibleArtifacts(self, person, branches=None, bugs=None,
2735- specifications=None, ignore_permissions=False):
2736+ def getVisibleArtifacts(self, person, bugs=None, branches=None,
2737+ gitrepositories=None, specifications=None,
2738+ ignore_permissions=False):
2739 """See `ISharingService`."""
2740 bugs_by_id = {}
2741 branches_by_id = {}
2742+ gitrepositories_by_id = {}
2743 for bug in bugs or []:
2744 if (not ignore_permissions
2745 and not check_permission('launchpad.View', bug)):
2746@@ -315,6 +332,11 @@
2747 and not check_permission('launchpad.View', branch)):
2748 raise Unauthorized
2749 branches_by_id[branch.id] = branch
2750+ for gitrepository in gitrepositories or []:
2751+ if (not ignore_permissions
2752+ and not check_permission('launchpad.View', gitrepository)):
2753+ raise Unauthorized
2754+ gitrepositories_by_id[gitrepository.id] = gitrepository
2755 for spec in specifications or []:
2756 if (not ignore_permissions
2757 and not check_permission('launchpad.View', spec)):
2758@@ -336,6 +358,11 @@
2759 *branches_by_id.keys())
2760 visible_branches = list(wanted_branches.getBranches())
2761
2762+ # Load the Git repositories.
2763+ visible_gitrepositories = []
2764+ # XXX cjwatson 2015-02-16: Fill in once IGitCollection is in place.
2765+
2766+ # Load the specifications.
2767 visible_specs = []
2768 if specifications:
2769 visible_private_spec_ids = self._getVisiblePrivateSpecificationIDs(
2770@@ -344,16 +371,22 @@
2771 spec for spec in specifications
2772 if spec.id in visible_private_spec_ids or not spec.private]
2773
2774- return visible_bugs, visible_branches, visible_specs
2775+ return (
2776+ visible_bugs, visible_branches, visible_gitrepositories,
2777+ visible_specs)
2778
2779- def getInvisibleArtifacts(self, person, branches=None, bugs=None):
2780+ def getInvisibleArtifacts(self, person, bugs=None, branches=None,
2781+ gitrepositories=None):
2782 """See `ISharingService`."""
2783 bugs_by_id = {}
2784 branches_by_id = {}
2785+ gitrepositories_by_id = {}
2786 for bug in bugs or []:
2787 bugs_by_id[bug.id] = bug
2788 for branch in branches or []:
2789 branches_by_id[branch.id] = branch
2790+ for gitrepository in gitrepositories or []:
2791+ gitrepositories_by_id[gitrepository.id] = gitrepository
2792
2793 # Load the bugs.
2794 visible_bug_ids = set()
2795@@ -376,7 +409,11 @@
2796 branches_by_id[branch_id]
2797 for branch_id in invisible_branch_ids]
2798
2799- return invisible_bugs, invisible_branches
2800+ # Load the Git repositories.
2801+ invisible_gitrepositories = []
2802+ # XXX cjwatson 2015-02-16: Fill in once IGitCollection is in place.
2803+
2804+ return invisible_bugs, invisible_branches, invisible_gitrepositories
2805
2806 def getPeopleWithoutAccess(self, concrete_artifact, people):
2807 """See `ISharingService`."""
2808@@ -722,42 +759,51 @@
2809 return invisible_types
2810
2811 @available_with_permission('launchpad.Edit', 'pillar')
2812- def revokeAccessGrants(self, pillar, grantee, user, branches=None,
2813- bugs=None, specifications=None):
2814+ def revokeAccessGrants(self, pillar, grantee, user, bugs=None,
2815+ branches=None, gitrepositories=None,
2816+ specifications=None):
2817 """See `ISharingService`."""
2818
2819- if not branches and not bugs and not specifications:
2820+ if (not bugs and not branches and not gitrepositories and
2821+ not specifications):
2822 raise ValueError(
2823- "Either bugs, branches or specifications must be specified")
2824+ "Either bugs, branches, gitrepositories, or specifications "
2825+ "must be specified")
2826
2827 artifacts = []
2828+ if bugs:
2829+ artifacts.extend(bugs)
2830 if branches:
2831 artifacts.extend(branches)
2832- if bugs:
2833- artifacts.extend(bugs)
2834+ if gitrepositories:
2835+ artifacts.extend(gitrepositories)
2836 if specifications:
2837 artifacts.extend(specifications)
2838- # Find the access artifacts associated with the bugs and branches.
2839+ # Find the access artifacts associated with the bugs, branches, Git
2840+ # repositories, and specifications.
2841 accessartifact_source = getUtility(IAccessArtifactSource)
2842 artifacts_to_delete = accessartifact_source.find(artifacts)
2843- # Revoke access to bugs/branches for the specified grantee.
2844+ # Revoke access to artifacts for the specified grantee.
2845 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
2846 artifacts_to_delete, [grantee])
2847
2848 # Create a job to remove subscriptions for artifacts the grantee can no
2849 # longer see.
2850- getUtility(IRemoveArtifactSubscriptionsJobSource).create(
2851+ return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
2852 user, artifacts, grantee=grantee, pillar=pillar)
2853
2854- def ensureAccessGrants(self, grantees, user, branches=None, bugs=None,
2855- specifications=None, ignore_permissions=False):
2856+ def ensureAccessGrants(self, grantees, user, bugs=None, branches=None,
2857+ gitrepositories=None, specifications=None,
2858+ ignore_permissions=False):
2859 """See `ISharingService`."""
2860
2861 artifacts = []
2862+ if bugs:
2863+ artifacts.extend(bugs)
2864 if branches:
2865 artifacts.extend(branches)
2866- if bugs:
2867- artifacts.extend(bugs)
2868+ if gitrepositories:
2869+ artifacts.extend(gitrepositories)
2870 if specifications:
2871 artifacts.extend(specifications)
2872 if not ignore_permissions:
2873@@ -767,15 +813,15 @@
2874 if not check_permission('launchpad.Edit', artifact):
2875 raise Unauthorized
2876
2877- # Ensure there are access artifacts associated with the bugs and
2878- # branches.
2879+ # Ensure there are access artifacts associated with the bugs,
2880+ # branches, Git repositories, and specifications.
2881 artifacts = getUtility(IAccessArtifactSource).ensure(artifacts)
2882 aagsource = getUtility(IAccessArtifactGrantSource)
2883 artifacts_with_grants = [
2884 artifact_grant.abstract_artifact
2885 for artifact_grant in
2886 aagsource.find(product(artifacts, grantees))]
2887- # Create access to bugs/branches for the specified grantee for which a
2888+ # Create access to artifacts for the specified grantee for which a
2889 # grant does not already exist.
2890 missing_artifacts = set(artifacts) - set(artifacts_with_grants)
2891 getUtility(IAccessArtifactGrantSource).grant(
2892
2893=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
2894--- lib/lp/registry/services/tests/test_sharingservice.py 2015-02-06 15:17:07 +0000
2895+++ lib/lp/registry/services/tests/test_sharingservice.py 2015-02-16 13:40:19 +0000
2896@@ -1,4 +1,4 @@
2897-# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
2898+# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
2899 # GNU Affero General Public License version 3 (see the file LICENSE).
2900
2901 __metaclass__ = type
2902@@ -1075,9 +1075,10 @@
2903
2904 # Check that grantees have expected access grants and subscriptions.
2905 for person in [team_grantee, person_grantee]:
2906- visible_bugs, visible_branches, visible_specs = (
2907+ visible_bugs, visible_branches, _, visible_specs = (
2908 self.service.getVisibleArtifacts(
2909- person, branches, bugs, specifications))
2910+ person, bugs=bugs, branches=branches,
2911+ specifications=specifications))
2912 self.assertContentEqual(bugs or [], visible_bugs)
2913 self.assertContentEqual(branches or [], visible_branches)
2914 self.assertContentEqual(specifications or [], visible_specs)
2915@@ -1102,8 +1103,9 @@
2916 for person in [team_grantee, person_grantee]:
2917 for bug in bugs or []:
2918 self.assertNotIn(person, bug.getDirectSubscribers())
2919- visible_bugs, visible_branches, visible_specs = (
2920- self.service.getVisibleArtifacts(person, branches, bugs))
2921+ visible_bugs, visible_branches, _, visible_specs = (
2922+ self.service.getVisibleArtifacts(
2923+ person, bugs=bugs, branches=branches))
2924 self.assertContentEqual([], visible_bugs)
2925 self.assertContentEqual([], visible_branches)
2926 self.assertContentEqual([], visible_specs)
2927@@ -1386,7 +1388,7 @@
2928 product, grantee, user)
2929
2930 # Check the results.
2931- shared_bugtasks, shared_branches, shared_specs = (
2932+ shared_bugtasks, shared_branches, _, shared_specs = (
2933 self.service.getSharedArtifacts(product, grantee, user))
2934 self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
2935 self.assertContentEqual(branches[:9], shared_branches)
2936@@ -1673,8 +1675,9 @@
2937 # Test the getVisibleArtifacts method.
2938 grantee, ignore, branches, bugs, specs = self._make_Artifacts()
2939 # Check the results.
2940- shared_bugs, shared_branches, shared_specs = (
2941- self.service.getVisibleArtifacts(grantee, branches, bugs, specs))
2942+ shared_bugs, shared_branches, _, shared_specs = (
2943+ self.service.getVisibleArtifacts(
2944+ grantee, bugs=bugs, branches=branches, specifications=specs))
2945 self.assertContentEqual(bugs[:5], shared_bugs)
2946 self.assertContentEqual(branches[:5], shared_branches)
2947 self.assertContentEqual(specs[:5], shared_specs)
2948@@ -1683,8 +1686,9 @@
2949 # getVisibleArtifacts() returns private specifications if
2950 # user has a policy grant for the pillar of the specification.
2951 ignore, owner, branches, bugs, specs = self._make_Artifacts()
2952- shared_bugs, shared_branches, shared_specs = (
2953- self.service.getVisibleArtifacts(owner, branches, bugs, specs))
2954+ shared_bugs, shared_branches, _, shared_specs = (
2955+ self.service.getVisibleArtifacts(
2956+ owner, bugs=bugs, branches=branches, specifications=specs))
2957 self.assertContentEqual(bugs, shared_bugs)
2958 self.assertContentEqual(branches, shared_branches)
2959 self.assertContentEqual(specs, shared_specs)
2960@@ -1693,8 +1697,9 @@
2961 # Test the getInvisibleArtifacts method.
2962 grantee, ignore, branches, bugs, specs = self._make_Artifacts()
2963 # Check the results.
2964- not_shared_bugs, not_shared_branches = (
2965- self.service.getInvisibleArtifacts(grantee, branches, bugs))
2966+ not_shared_bugs, not_shared_branches, _ = (
2967+ self.service.getInvisibleArtifacts(
2968+ grantee, bugs=bugs, branches=branches))
2969 self.assertContentEqual(bugs[5:], not_shared_bugs)
2970 self.assertContentEqual(branches[5:], not_shared_branches)
2971
2972@@ -1718,7 +1723,7 @@
2973 information_type=InformationType.USERDATA)
2974 bugs.append(bug)
2975
2976- shared_bugs, shared_branches, shared_specs = (
2977+ shared_bugs, shared_branches, _, shared_specs = (
2978 self.service.getVisibleArtifacts(grantee, bugs=bugs))
2979 self.assertContentEqual(bugs, shared_bugs)
2980
2981@@ -1726,7 +1731,7 @@
2982 for x in range(0, 5):
2983 change_callback(bugs[x], owner)
2984 # Check the results.
2985- shared_bugs, shared_branches, shared_specs = (
2986+ shared_bugs, shared_branches, _, shared_specs = (
2987 self.service.getVisibleArtifacts(grantee, bugs=bugs))
2988 self.assertContentEqual(bugs[5:], shared_bugs)
2989
2990
2991=== modified file 'lib/lp/registry/tests/test_product.py'
2992--- lib/lp/registry/tests/test_product.py 2015-01-29 16:28:30 +0000
2993+++ lib/lp/registry/tests/test_product.py 2015-02-16 13:40:19 +0000
2994@@ -858,10 +858,10 @@
2995 'getCustomLanguageCode', 'getDefaultBugInformationType',
2996 'getDefaultSpecificationInformationType',
2997 'getEffectiveTranslationPermission', 'getExternalBugTracker',
2998- 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
2999- 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
3000- 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
3001- 'getSeries', 'getSubscription',
3002+ 'getFAQ', 'getFirstEntryToImport', 'getGitRepositories',
3003+ 'getLinkedBugWatches', 'getMergeProposals', 'getMilestone',
3004+ 'getMilestonesAndReleases', 'getQuestion', 'getQuestionLanguages',
3005+ 'getPackage', 'getRelease', 'getSeries', 'getSubscription',
3006 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
3007 'getTopContributors', 'getTopContributorsGroupedByCategory',
3008 'getTranslationGroups', 'getTranslationImportQueueEntries',
3009@@ -902,7 +902,8 @@
3010 'launchpad.Edit': set((
3011 'addOfficialBugTag', 'removeOfficialBugTag',
3012 'setBranchSharingPolicy', 'setBugSharingPolicy',
3013- 'setSpecificationSharingPolicy', 'checkInformationType')),
3014+ 'setSpecificationSharingPolicy', 'checkInformationType',
3015+ 'createGitRepository')),
3016 'launchpad.Moderate': set((
3017 'is_permitted', 'license_approved', 'project_reviewed',
3018 'reviewer_whiteboard', 'setAliases')),
3019
3020=== modified file 'lib/lp/security.py'
3021--- lib/lp/security.py 2015-01-06 04:52:44 +0000
3022+++ lib/lp/security.py 2015-02-16 13:40:19 +0000
3023@@ -83,6 +83,10 @@
3024 )
3025 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
3026 from lp.code.interfaces.diff import IPreviewDiff
3027+from lp.code.interfaces.gitrepository import (
3028+ IGitRepository,
3029+ user_has_special_git_repository_access,
3030+ )
3031 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
3032 from lp.code.interfaces.sourcepackagerecipebuild import (
3033 ISourcePackageRecipeBuild,
3034@@ -1151,14 +1155,37 @@
3035
3036
3037 class EditDistributionSourcePackage(AuthorizationBase):
3038- """DistributionSourcePackage is not editable.
3039-
3040- But EditStructuralSubscription needs launchpad.Edit defined on all
3041- targets.
3042- """
3043 permission = 'launchpad.Edit'
3044 usedfor = IDistributionSourcePackage
3045
3046+ def _checkUpload(self, user, archive, distroseries):
3047+ # We use verifyUpload() instead of checkUpload() because we don't
3048+ # have a pocket. It returns the reason the user can't upload or
3049+ # None if they are allowed.
3050+ if distroseries is None:
3051+ return False
3052+ reason = archive.verifyUpload(
3053+ user.person, sourcepackagename=self.obj.sourcepackagename,
3054+ component=None, distroseries=distroseries, strict_component=False)
3055+ return reason is None
3056+
3057+ def checkAuthenticated(self, user):
3058+ """Anyone who can upload a package can edit it.
3059+
3060+ Checking upload permission requires a distroseries; a reasonable
3061+ approximation is to check whether the user can upload the package to
3062+ the current series.
3063+ """
3064+ if user.in_admin:
3065+ return True
3066+
3067+ distribution = self.obj.distribution
3068+ if user.inTeam(distribution.owner):
3069+ return True
3070+
3071+ return self._checkUpload(
3072+ user, distribution.main_archive, distribution.currentseries)
3073+
3074
3075 class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
3076 """Product's owner and bug supervisor can set official bug tags."""
3077@@ -2176,6 +2203,57 @@
3078 return user.in_admin
3079
3080
3081+class ViewGitRepository(AuthorizationBase):
3082+ """Controls visibility of Git repositories.
3083+
3084+ A person can see the repository if the repository is public, they are
3085+ the owner of the repository, they are in the team that owns the
3086+ repository, they have an access grant to the repository, or they are a
3087+ Launchpad administrator.
3088+ """
3089+ permission = 'launchpad.View'
3090+ usedfor = IGitRepository
3091+
3092+ def checkAuthenticated(self, user):
3093+ return self.obj.visibleByUser(user.person)
3094+
3095+ def checkUnauthenticated(self):
3096+ return self.obj.visibleByUser(None)
3097+
3098+
3099+class EditGitRepository(AuthorizationBase):
3100+ """The owner or admins can edit Git repositories."""
3101+ permission = 'launchpad.Edit'
3102+ usedfor = IGitRepository
3103+
3104+ def checkAuthenticated(self, user):
3105+ # XXX cjwatson 2015-01-23: People who can upload source packages to
3106+ # a distribution should be able to push to the corresponding
3107+ # "official" repositories, once those are defined.
3108+ return (
3109+ user.inTeam(self.obj.owner) or
3110+ user_has_special_git_repository_access(user.person))
3111+
3112+
3113+class ModerateGitRepository(EditGitRepository):
3114+ """The owners, project owners, and admins can moderate Git repositories."""
3115+ permission = 'launchpad.Moderate'
3116+
3117+ def checkAuthenticated(self, user):
3118+ if super(ModerateGitRepository, self).checkAuthenticated(user):
3119+ return True
3120+ target = self.obj.target
3121+ if (target is not None and IProduct.providedBy(target) and
3122+ user.inTeam(target.owner)):
3123+ return True
3124+ return user.in_commercial_admin
3125+
3126+
3127+class AdminGitRepository(AdminByAdminsTeam):
3128+ """The admins can administer Git repositories."""
3129+ usedfor = IGitRepository
3130+
3131+
3132 class AdminDistroSeriesTranslations(AuthorizationBase):
3133 permission = 'launchpad.TranslationsAdmin'
3134 usedfor = IDistroSeries
3135@@ -2858,8 +2936,7 @@
3136 usedfor = IPublisherConfig
3137
3138
3139-class EditSourcePackage(AuthorizationBase):
3140- permission = 'launchpad.Edit'
3141+class EditSourcePackage(EditDistributionSourcePackage):
3142 usedfor = ISourcePackage
3143
3144 def checkAuthenticated(self, user):
3145@@ -2871,15 +2948,8 @@
3146 if user.inTeam(distribution.owner):
3147 return True
3148
3149- # We use verifyUpload() instead of checkUpload() because
3150- # we don't have a pocket.
3151- # It returns the reason the user can't upload
3152- # or None if they are allowed.
3153- reason = distribution.main_archive.verifyUpload(
3154- user.person, distroseries=self.obj.distroseries,
3155- sourcepackagename=self.obj.sourcepackagename,
3156- component=None, strict_component=False)
3157- return reason is None
3158+ return self._checkUpload(
3159+ user, distribution.main_archive, self.obj.distroseries)
3160
3161
3162 class ViewLiveFS(DelegatedAuthorization):
3163
3164=== modified file 'lib/lp/services/config/schema-lazr.conf'
3165--- lib/lp/services/config/schema-lazr.conf 2014-08-05 08:58:14 +0000
3166+++ lib/lp/services/config/schema-lazr.conf 2015-02-16 13:40:19 +0000
3167@@ -335,6 +335,27 @@
3168 # of shutting down and so should not receive any more connections.
3169 web_status_port = tcp:8022
3170
3171+# The URL of the internal Git hosting API endpoint.
3172+internal_git_endpoint: none
3173+
3174+# The URL prefix for links to the Git code browser. Links are formed by
3175+# appending the repository's path to the root URL.
3176+#
3177+# datatype: urlbase
3178+git_browse_root: none
3179+
3180+# The URL prefix for anonymous Git protocol fetches. Links are formed by
3181+# appending the repository's path to the root URL.
3182+#
3183+# datatype: urlbase
3184+git_anon_root: none
3185+
3186+# The URL prefix for Git-over-SSH. Links are formed by appending the
3187+# repository's path to the root URL.
3188+#
3189+# datatype: urlbase
3190+git_ssh_root: none
3191+
3192
3193 [codeimport]
3194 # Where the Bazaar imports are stored.