Merge lp:~cjwatson/launchpad/git-basic-model into lp:launchpad

Proposed by Colin Watson on 2015-02-06
Status: Merged
Approved by: Colin Watson on 2015-02-19
Approved revision: no longer in the source branch.
Merged at revision: 17350
Proposed branch: lp:~cjwatson/launchpad/git-basic-model
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-personmerge-whitelist
Diff against target: 1363 lines (+1062/-29)
19 files modified
configs/development/launchpad-lazr.conf (+4/-0)
lib/lp/code/configure.zcml (+31/-0)
lib/lp/code/errors.py (+31/-0)
lib/lp/code/interfaces/gitrepository.py (+372/-0)
lib/lp/code/interfaces/hasgitrepositories.py (+40/-0)
lib/lp/code/model/branch.py (+1/-2)
lib/lp/code/model/gitrepository.py (+390/-0)
lib/lp/code/model/hasgitrepositories.py (+28/-0)
lib/lp/code/model/tests/test_hasgitrepositories.py (+34/-0)
lib/lp/registry/configure.zcml (+5/-0)
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/model/distributionsourcepackage.py (+2/-1)
lib/lp/registry/model/person.py (+2/-1)
lib/lp/registry/model/product.py (+2/-1)
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-basic-model
Reviewer Review Type Date Requested Status
William Grant code 2015-02-06 Approve on 2015-02-19
Review via email: mp+248976@code.launchpad.net

This proposal supersedes a proposal from 2015-02-06.

Commit message

Very basic preliminary model for GitRepository. Make Product, DistributionSourcePackage, and Person implement IHasGitRepositories.

Description of the change

Very basic preliminary model for GitRepository. Make Product, DistributionSourcePackage, and Person implement IHasGitRepositories.

This doesn't really do anything useful by itself. In particular, in the cause of getting this chunk of changes down to a reasonable size, I left out the GitNamespace stuff that makes it possible to actually create GitRepository objects (the general architecture is fairly similar to Branch*), which also means that most of the tests are deferred to a later branch when we have enough infrastructure to support creating test objects.

This branch implements one of the candidate proposals for Git URL layouts in Launchpad. I'm not assuming that this will be the final layout; it's relatively easy to change later.

To post a comment you must log in.
William Grant (wgrant) :
review: Needs Fixing (code)
William Grant (wgrant) :
William Grant (wgrant) :
Colin Watson (cjwatson) :
William Grant (wgrant) :
review: Needs Fixing (code)
Colin Watson (cjwatson) :
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '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-19 18:43:50 +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_api_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/code/configure.zcml'
17--- lib/lp/code/configure.zcml 2015-02-09 11:38:30 +0000
18+++ lib/lp/code/configure.zcml 2015-02-19 18:43:50 +0000
19@@ -807,6 +807,37 @@
20 <adapter factory="lp.code.model.linkedbranch.PackageLinkedBranch" />
21 <adapter factory="lp.code.model.linkedbranch.DistributionPackageLinkedBranch" />
22
23+ <!-- GitRepository -->
24+
25+ <class class="lp.code.model.gitrepository.GitRepository">
26+ <require
27+ permission="launchpad.View"
28+ interface="lp.app.interfaces.launchpad.IPrivacy
29+ lp.code.interfaces.gitrepository.IGitRepositoryView
30+ lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
31+ <require
32+ permission="launchpad.Moderate"
33+ interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate"
34+ set_schema="lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" />
35+ <require
36+ permission="launchpad.Edit"
37+ interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" />
38+ </class>
39+ <subscriber
40+ for="lp.code.interfaces.gitrepository.IGitRepository zope.lifecycleevent.interfaces.IObjectModifiedEvent"
41+ handler="lp.code.model.gitrepository.git_repository_modified"/>
42+
43+ <!-- GitRepositorySet -->
44+
45+ <class class="lp.code.model.gitrepository.GitRepositorySet">
46+ <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
47+ </class>
48+ <securedutility
49+ class="lp.code.model.gitrepository.GitRepositorySet"
50+ provides="lp.code.interfaces.gitrepository.IGitRepositorySet">
51+ <allow interface="lp.code.interfaces.gitrepository.IGitRepositorySet" />
52+ </securedutility>
53+
54 <lp:help-folder folder="help" name="+help-code" />
55
56 <!-- Diffs -->
57
58=== modified file 'lib/lp/code/errors.py'
59--- lib/lp/code/errors.py 2013-12-20 05:38:18 +0000
60+++ lib/lp/code/errors.py 2015-02-19 18:43:50 +0000
61@@ -28,6 +28,8 @@
62 'CodeImportNotInReviewedState',
63 'ClaimReviewFailed',
64 'DiffNotFound',
65+ 'GitDefaultConflict',
66+ 'GitTargetError',
67 'InvalidBranchMergeProposal',
68 'InvalidMergeQueueConfig',
69 'InvalidNamespace',
70@@ -312,6 +314,35 @@
71 """Raised when the user specifies an unrecognized branch type."""
72
73
74+class GitTargetError(Exception):
75+ """Raised when there is an error determining a Git repository target."""
76+
77+
78+@error_status(httplib.CONFLICT)
79+class GitDefaultConflict(Exception):
80+ """Raised when trying to set a Git repository as the default for
81+ something that already has a default."""
82+
83+ def __init__(self, existing_repository, target, owner=None):
84+ params = {
85+ "unique_name": existing_repository.unique_name,
86+ "target": target.displayname,
87+ "owner": owner.displayname,
88+ }
89+ if owner is None:
90+ message = (
91+ "The default repository for '%(target)s' is already set to "
92+ "%(unique_name)s." % params)
93+ else:
94+ message = (
95+ "%(owner)'s default repository for '%(target)s' is already "
96+ "set to %(unique_name)s." % params)
97+ self.existing_repository = existing_repository
98+ self.target = target
99+ self.owner = owner
100+ Exception.__init__(self, message)
101+
102+
103 @error_status(httplib.BAD_REQUEST)
104 class CodeImportNotInReviewedState(Exception):
105 """Raised when the user requests an import of a non-automatic import."""
106
107=== added file 'lib/lp/code/interfaces/gitrepository.py'
108--- lib/lp/code/interfaces/gitrepository.py 1970-01-01 00:00:00 +0000
109+++ lib/lp/code/interfaces/gitrepository.py 2015-02-19 18:43:50 +0000
110@@ -0,0 +1,372 @@
111+# Copyright 2015 Canonical Ltd. This software is licensed under the
112+# GNU Affero General Public License version 3 (see the file LICENSE).
113+
114+"""Git repository interfaces."""
115+
116+__metaclass__ = type
117+
118+__all__ = [
119+ 'GitIdentityMixin',
120+ 'git_repository_name_validator',
121+ 'IGitRepository',
122+ 'IGitRepositorySet',
123+ 'user_has_special_git_repository_access',
124+ ]
125+
126+import re
127+
128+from lazr.restful.fields import Reference
129+from zope.interface import (
130+ Attribute,
131+ Interface,
132+ )
133+from zope.schema import (
134+ Bool,
135+ Choice,
136+ Datetime,
137+ Int,
138+ Text,
139+ TextLine,
140+ )
141+
142+from lp import _
143+from lp.app.enums import InformationType
144+from lp.app.validators import LaunchpadValidationError
145+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
146+from lp.registry.interfaces.role import IPersonRoles
147+from lp.services.fields import (
148+ PersonChoice,
149+ PublicPersonChoice,
150+ )
151+
152+
153+GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE = _(
154+ "Git repository names must start with a number or letter. The characters "
155+ "+, -, _, . and @ are also allowed after the first character. Repository "
156+ "names must not end with \".git\".")
157+
158+
159+# This is a copy of the pattern in database/schema/patch-2209-61-0.sql.
160+# Don't change it without changing that.
161+valid_git_repository_name_pattern = re.compile(
162+ r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z")
163+
164+
165+def valid_git_repository_name(name):
166+ """Return True iff the name is valid as a Git repository name.
167+
168+ The rules for what is a valid Git repository name are described in
169+ GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE.
170+ """
171+ if (not name.endswith(".git") and
172+ valid_git_repository_name_pattern.match(name)):
173+ return True
174+ return False
175+
176+
177+def git_repository_name_validator(name):
178+ """Return True if the name is valid, or raise a LaunchpadValidationError.
179+ """
180+ if not valid_git_repository_name(name):
181+ raise LaunchpadValidationError(
182+ _("Invalid Git repository name '${name}'. ${message}",
183+ mapping={
184+ "name": name,
185+ "message": GIT_REPOSITORY_NAME_VALIDATION_ERROR_MESSAGE,
186+ }))
187+ return True
188+
189+
190+class IGitRepositoryView(Interface):
191+ """IGitRepository attributes that require launchpad.View permission."""
192+
193+ id = Int(title=_("ID"), readonly=True, required=True)
194+
195+ date_created = Datetime(
196+ title=_("Date created"), required=True, readonly=True)
197+
198+ date_last_modified = Datetime(
199+ title=_("Date last modified"), required=True, readonly=True)
200+
201+ registrant = PublicPersonChoice(
202+ title=_("Registrant"), required=True, readonly=True,
203+ vocabulary="ValidPersonOrTeam",
204+ description=_("The person who registered this Git repository."))
205+
206+ owner = PersonChoice(
207+ title=_("Owner"), required=True, readonly=False,
208+ vocabulary="AllUserTeamsParticipationPlusSelf",
209+ description=_(
210+ "The owner of this Git repository. This controls who can modify "
211+ "the repository."))
212+
213+ target = Reference(
214+ title=_("Target"), required=True, readonly=True,
215+ schema=IHasGitRepositories,
216+ description=_("The target of the repository."))
217+
218+ information_type = Choice(
219+ title=_("Information type"), vocabulary=InformationType,
220+ required=True, readonly=True, default=InformationType.PUBLIC,
221+ description=_(
222+ "The type of information contained in this repository."))
223+
224+ owner_default = Bool(
225+ title=_("Owner default"), required=True, readonly=True,
226+ description=_(
227+ "Whether this repository is the default for its owner and "
228+ "target."))
229+
230+ target_default = Bool(
231+ title=_("Target default"), required=True, readonly=True,
232+ description=_(
233+ "Whether this repository is the default for its target."))
234+
235+ unique_name = Text(
236+ title=_("Unique name"), readonly=True,
237+ description=_(
238+ "Unique name of the repository, including the owner and project "
239+ "names."))
240+
241+ display_name = Text(
242+ title=_("Display name"), readonly=True,
243+ description=_("Display name of the repository."))
244+
245+ shortened_path = Attribute(
246+ "The shortest reasonable version of the path to this repository.")
247+
248+ git_identity = Text(
249+ title=_("Git identity"), readonly=True,
250+ description=_(
251+ "If this is the default repository for some target, then this is "
252+ "'lp:' plus a shortcut version of the path via that target. "
253+ "Otherwise it is simply 'lp:' plus the unique name."))
254+
255+ def setOwnerDefault(value):
256+ """Set whether this repository is the default for its owner-target.
257+
258+ This is for internal use; the caller should ensure permission to
259+ edit the owner, should arrange to remove any existing owner-target
260+ default, and should check that this repository is attached to the
261+ desired target.
262+
263+ :param value: True if this repository should be the owner-target
264+ default, otherwise False.
265+ """
266+
267+ def setTargetDefault(value):
268+ """Set whether this repository is the default for its target.
269+
270+ This is for internal use; the caller should ensure permission to
271+ edit the target, should arrange to remove any existing target
272+ default, and should check that this repository is attached to the
273+ desired target.
274+
275+ :param value: True if this repository should be the target default,
276+ otherwise False.
277+ """
278+
279+ def getCodebrowseUrl():
280+ """Construct a browsing URL for this Git repository."""
281+
282+ def visibleByUser(user):
283+ """Can the specified user see this repository?"""
284+
285+ def getAllowedInformationTypes(user):
286+ """Get a list of acceptable `InformationType`s for this repository.
287+
288+ If the user is a Launchpad admin, any type is acceptable.
289+ """
290+
291+ def getInternalPath():
292+ """Get the internal path to this repository.
293+
294+ This is used on the storage backend.
295+ """
296+
297+ def getRepositoryDefaults():
298+ """Return a sorted list of `ICanHasDefaultGitRepository` objects.
299+
300+ There is one result for each related object for which this
301+ repository is the default. For example, in the case where a
302+ repository is the default for a project and is also its owner's
303+ default repository for that project, the objects for both the
304+ project and the person-project are returned.
305+
306+ More important related objects are sorted first.
307+ """
308+
309+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
310+ def getRepositoryIdentities():
311+ """A list of aliases for a repository.
312+
313+ Returns a list of tuples of path and context object. There is at
314+ least one alias for any repository, and that is the repository
315+ itself. For default repositories, the context object is the
316+ appropriate default object.
317+
318+ Where a repository is the default for a product or a distribution
319+ source package, the repository is available through a number of
320+ different URLs. These URLs are the aliases for the repository.
321+
322+ For example, a repository which is the default for the 'fooix'
323+ project and which is also its owner's default repository for that
324+ project is accessible using:
325+ fooix - the context object is the project fooix
326+ ~fooix-owner/fooix - the context object is the person-project
327+ ~fooix-owner and fooix
328+ ~fooix-owner/fooix/+git/fooix - the unique name of the repository
329+ where the context object is the repository itself.
330+ """
331+
332+
333+class IGitRepositoryModerateAttributes(Interface):
334+ """IGitRepository attributes that can be edited by more than one community.
335+ """
336+
337+ # XXX cjwatson 2015-01-29: Add some advice about default repository
338+ # naming.
339+ name = TextLine(
340+ title=_("Name"), required=True,
341+ constraint=git_repository_name_validator,
342+ description=_(
343+ "The repository name. Keep very short, unique, and descriptive, "
344+ "because it will be used in URLs."))
345+
346+
347+class IGitRepositoryModerate(Interface):
348+ """IGitRepository methods that can be called by more than one community."""
349+
350+ def transitionToInformationType(information_type, user,
351+ verify_policy=True):
352+ """Set the information type for this repository.
353+
354+ :param information_type: The `InformationType` to transition to.
355+ :param user: The `IPerson` who is making the change.
356+ :param verify_policy: Check if the new information type complies
357+ with the `IGitNamespacePolicy`.
358+ """
359+
360+
361+class IGitRepositoryEdit(Interface):
362+ """IGitRepository methods that require launchpad.Edit permission."""
363+
364+ def setOwner(new_owner, user):
365+ """Set the owner of the repository to be `new_owner`."""
366+
367+ def setTarget(target, user):
368+ """Set the target of the repository."""
369+
370+ def destroySelf():
371+ """Delete the specified repository."""
372+
373+
374+class IGitRepository(IGitRepositoryView, IGitRepositoryModerateAttributes,
375+ IGitRepositoryModerate, IGitRepositoryEdit):
376+ """A Git repository."""
377+
378+ private = Bool(
379+ title=_("Private"), required=False, readonly=True,
380+ description=_("This repository is visible only to its subscribers."))
381+
382+
383+class IGitRepositorySet(Interface):
384+ """Interface representing the set of Git repositories."""
385+
386+ def new(registrant, owner, target, name, information_type=None,
387+ date_created=None):
388+ """Create a Git repository and return it.
389+
390+ :param registrant: The `IPerson` who registered the new repository.
391+ :param owner: The `IPerson` who owns the new repository.
392+ :param target: The `IProduct`, `IDistributionSourcePackage`, or
393+ `IPerson` that the new repository is associated with.
394+ :param name: The repository name.
395+ :param information_type: Set the repository's information type to
396+ one different from the target's default. The type must conform
397+ to the target's code sharing policy. (optional)
398+ """
399+
400+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
401+ def getByPath(user, path):
402+ """Find a repository by its path.
403+
404+ Any of these forms may be used, with or without a leading slash:
405+ Unique names:
406+ ~OWNER/PROJECT/+git/NAME
407+ ~OWNER/DISTRO/+source/SOURCE/+git/NAME
408+ ~OWNER/+git/NAME
409+ Owner-target default aliases:
410+ ~OWNER/PROJECT
411+ ~OWNER/DISTRO/+source/SOURCE
412+ Official aliases:
413+ PROJECT
414+ DISTRO/+source/SOURCE
415+
416+ Return None if no match was found.
417+ """
418+
419+ def getDefaultRepository(target, owner=None):
420+ """Get the default repository for a target or owner-target.
421+
422+ :param target: An `IHasGitRepositories`.
423+ :param owner: An `IPerson`, in which case search for that person's
424+ default repository for this target; or None, in which case
425+ search for the overall default repository for this target.
426+
427+ :raises GitTargetError: if `target` is an `IPerson`.
428+ :return: An `IGitRepository`, or None.
429+ """
430+
431+ def getRepositories():
432+ """Return an empty collection of repositories.
433+
434+ This only exists to keep lazr.restful happy.
435+ """
436+
437+
438+class GitIdentityMixin:
439+ """This mixin class determines Git repository paths.
440+
441+ Used by both the model GitRepository class and the browser repository
442+ listing item. This allows the browser code to cache the associated
443+ context objects which reduces query counts.
444+ """
445+
446+ @property
447+ def shortened_path(self):
448+ """See `IGitRepository`."""
449+ path, context = self.getRepositoryIdentities()[0]
450+ return path
451+
452+ @property
453+ def git_identity(self):
454+ """See `IGitRepository`."""
455+ return "lp:" + self.shortened_path
456+
457+ def getRepositoryDefaults(self):
458+ """See `IGitRepository`."""
459+ # XXX cjwatson 2015-02-06: This will return shortcut defaults once
460+ # they're implemented.
461+ return []
462+
463+ def getRepositoryIdentities(self):
464+ """See `IGitRepository`."""
465+ identities = [
466+ (default.path, default.context)
467+ for default in self.getRepositoryDefaults()]
468+ identities.append((self.unique_name, self))
469+ return identities
470+
471+
472+def user_has_special_git_repository_access(user):
473+ """Admins have special access.
474+
475+ :param user: An `IPerson` or None.
476+ """
477+ if user is None:
478+ return False
479+ roles = IPersonRoles(user)
480+ if roles.in_admin:
481+ return True
482+ return False
483
484=== added file 'lib/lp/code/interfaces/hasgitrepositories.py'
485--- lib/lp/code/interfaces/hasgitrepositories.py 1970-01-01 00:00:00 +0000
486+++ lib/lp/code/interfaces/hasgitrepositories.py 2015-02-19 18:43:50 +0000
487@@ -0,0 +1,40 @@
488+# Copyright 2015 Canonical Ltd. This software is licensed under the
489+# GNU Affero General Public License version 3 (see the file LICENSE).
490+
491+"""Interfaces relating to targets of Git repositories."""
492+
493+__metaclass__ = type
494+
495+__all__ = [
496+ 'IHasGitRepositories',
497+ ]
498+
499+from zope.interface import Interface
500+
501+
502+class IHasGitRepositories(Interface):
503+ """An object that has related Git repositories.
504+
505+ A project contains Git repositories, a source package on a distribution
506+ contains branches, and a person contains "personal" branches.
507+ """
508+
509+ def getGitRepositories(visible_by_user=None, eager_load=False):
510+ """Returns all Git repositories related to this object.
511+
512+ :param visible_by_user: Normally the user who is asking.
513+ :param eager_load: If True, load related objects for the whole
514+ collection.
515+ :returns: A list of `IGitRepository` objects.
516+ """
517+
518+ def createGitRepository(registrant, owner, name, information_type=None):
519+ """Create a Git repository for this target and return it.
520+
521+ :param registrant: The `IPerson` who registered the new repository.
522+ :param owner: The `IPerson` who owns the new repository.
523+ :param name: The repository name.
524+ :param information_type: Set the repository's information type to
525+ one different from the target's default. The type must conform
526+ to the target's code sharing policy. (optional)
527+ """
528
529=== modified file 'lib/lp/code/model/branch.py'
530--- lib/lp/code/model/branch.py 2014-01-15 00:59:48 +0000
531+++ lib/lp/code/model/branch.py 2015-02-19 18:43:50 +0000
532@@ -208,7 +208,6 @@
533 mirror_status_message = StringCol(default=None)
534 information_type = EnumCol(
535 enum=InformationType, default=InformationType.PUBLIC)
536- access_policy = IntCol()
537
538 @property
539 def private(self):
540@@ -1661,7 +1660,7 @@
541
542 policy_grant_query = Coalesce(
543 ArrayIntersects(
544- Array(branch_class.access_policy),
545+ Array(SQL('%s.access_policy' % branch_class.__storm_table__)),
546 Select(
547 ArrayAgg(AccessPolicyGrant.policy_id),
548 tables=(AccessPolicyGrant,
549
550=== added file 'lib/lp/code/model/gitrepository.py'
551--- lib/lp/code/model/gitrepository.py 1970-01-01 00:00:00 +0000
552+++ lib/lp/code/model/gitrepository.py 2015-02-19 18:43:50 +0000
553@@ -0,0 +1,390 @@
554+# Copyright 2015 Canonical Ltd. This software is licensed under the
555+# GNU Affero General Public License version 3 (see the file LICENSE).
556+
557+__metaclass__ = type
558+__all__ = [
559+ 'get_git_repository_privacy_filter',
560+ 'GitRepository',
561+ 'GitRepositorySet',
562+ ]
563+
564+from bzrlib import urlutils
565+import pytz
566+from storm.expr import (
567+ Coalesce,
568+ Join,
569+ Or,
570+ Select,
571+ SQL,
572+ )
573+from storm.locals import (
574+ Bool,
575+ DateTime,
576+ Int,
577+ Reference,
578+ Unicode,
579+ )
580+from zope.component import getUtility
581+from zope.interface import implements
582+
583+from lp.app.enums import (
584+ InformationType,
585+ PRIVATE_INFORMATION_TYPES,
586+ PUBLIC_INFORMATION_TYPES,
587+ )
588+from lp.app.interfaces.informationtype import IInformationType
589+from lp.app.interfaces.launchpad import IPrivacy
590+from lp.app.interfaces.services import IService
591+from lp.code.errors import (
592+ GitDefaultConflict,
593+ GitTargetError,
594+ )
595+from lp.code.interfaces.gitrepository import (
596+ GitIdentityMixin,
597+ IGitRepository,
598+ IGitRepositorySet,
599+ user_has_special_git_repository_access,
600+ )
601+from lp.registry.errors import CannotChangeInformationType
602+from lp.registry.interfaces.accesspolicy import (
603+ IAccessArtifactSource,
604+ IAccessPolicySource,
605+ )
606+from lp.registry.interfaces.distributionsourcepackage import (
607+ IDistributionSourcePackage,
608+ )
609+from lp.registry.interfaces.product import IProduct
610+from lp.registry.interfaces.role import IHasOwner
611+from lp.registry.interfaces.sharingjob import (
612+ IRemoveArtifactSubscriptionsJobSource,
613+ )
614+from lp.registry.model.accesspolicy import (
615+ AccessPolicyGrant,
616+ reconcile_access_for_artifact,
617+ )
618+from lp.registry.model.teammembership import TeamParticipation
619+from lp.services.config import config
620+from lp.services.database.constants import (
621+ DEFAULT,
622+ UTC_NOW,
623+ )
624+from lp.services.database.enumcol import EnumCol
625+from lp.services.database.interfaces import IStore
626+from lp.services.database.stormbase import StormBase
627+from lp.services.database.stormexpr import (
628+ Array,
629+ ArrayAgg,
630+ ArrayIntersects,
631+ )
632+from lp.services.propertycache import cachedproperty
633+
634+
635+def git_repository_modified(repository, event):
636+ """Update the date_last_modified property when a GitRepository is modified.
637+
638+ This method is registered as a subscriber to `IObjectModifiedEvent`
639+ events on Git repositories.
640+ """
641+ repository.date_last_modified = UTC_NOW
642+
643+
644+class GitRepository(StormBase, GitIdentityMixin):
645+ """See `IGitRepository`."""
646+
647+ __storm_table__ = 'GitRepository'
648+
649+ implements(IGitRepository, IHasOwner, IPrivacy, IInformationType)
650+
651+ id = Int(primary=True)
652+
653+ date_created = DateTime(
654+ name='date_created', tzinfo=pytz.UTC, allow_none=False)
655+ date_last_modified = DateTime(
656+ name='date_last_modified', tzinfo=pytz.UTC, allow_none=False)
657+
658+ registrant_id = Int(name='registrant', allow_none=False)
659+ registrant = Reference(registrant_id, 'Person.id')
660+
661+ owner_id = Int(name='owner', allow_none=False)
662+ owner = Reference(owner_id, 'Person.id')
663+
664+ project_id = Int(name='project', allow_none=True)
665+ project = Reference(project_id, 'Product.id')
666+
667+ distribution_id = Int(name='distribution', allow_none=True)
668+ distribution = Reference(distribution_id, 'Distribution.id')
669+
670+ sourcepackagename_id = Int(name='sourcepackagename', allow_none=True)
671+ sourcepackagename = Reference(sourcepackagename_id, 'SourcePackageName.id')
672+
673+ name = Unicode(name='name', allow_none=False)
674+
675+ information_type = EnumCol(enum=InformationType, notNull=True)
676+ owner_default = Bool(name='owner_default', allow_none=False)
677+ target_default = Bool(name='target_default', allow_none=False)
678+
679+ def __init__(self, registrant, owner, target, name, information_type,
680+ date_created):
681+ super(GitRepository, self).__init__()
682+ self.registrant = registrant
683+ self.owner = owner
684+ self.name = name
685+ self.information_type = information_type
686+ self.date_created = date_created
687+ self.date_last_modified = date_created
688+ self.project = None
689+ self.distribution = None
690+ self.sourcepackagename = None
691+ if IProduct.providedBy(target):
692+ self.project = target
693+ elif IDistributionSourcePackage.providedBy(target):
694+ self.distribution = target.distribution
695+ self.sourcepackagename = target.sourcepackagename
696+ self.owner_default = False
697+ self.target_default = False
698+
699+ # Marker for references to Git URL layouts: ##GITNAMESPACE##
700+ @property
701+ def unique_name(self):
702+ names = {"owner": self.owner.name, "repository": self.name}
703+ if self.project is not None:
704+ fmt = "~%(owner)s/%(project)s"
705+ names["project"] = self.project.name
706+ elif self.distribution is not None:
707+ fmt = "~%(owner)s/%(distribution)s/+source/%(source)s"
708+ names["distribution"] = self.distribution.name
709+ names["source"] = self.sourcepackagename.name
710+ else:
711+ fmt = "~%(owner)s"
712+ fmt += "/+git/%(repository)s"
713+ return fmt % names
714+
715+ def __repr__(self):
716+ return "<GitRepository %r (%d)>" % (self.unique_name, self.id)
717+
718+ @cachedproperty
719+ def target(self):
720+ """See `IGitRepository`."""
721+ if self.project is not None:
722+ return self.project
723+ elif self.distribution is not None:
724+ return self.distribution.getSourcePackage(self.sourcepackagename)
725+ else:
726+ return self.owner
727+
728+ def setTarget(self, target, user):
729+ """See `IGitRepository`."""
730+ # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
731+ # place.
732+ raise NotImplementedError
733+
734+ def setOwnerDefault(self, value):
735+ """See `IGitRepository`."""
736+ if value:
737+ # Check for an existing owner-target default.
738+ existing = getUtility(IGitRepositorySet).getDefaultRepository(
739+ self.target, owner=self.owner)
740+ if existing is not None:
741+ raise GitDefaultConflict(
742+ existing, self.target, owner=self.owner)
743+ self.owner_default = value
744+
745+ def setTargetDefault(self, value):
746+ """See `IGitRepository`."""
747+ if value:
748+ # Check for an existing target default.
749+ existing = getUtility(IGitRepositorySet).getDefaultRepository(
750+ self.target)
751+ if existing is not None:
752+ raise GitDefaultConflict(existing, self.target)
753+ self.target_default = value
754+
755+ @property
756+ def display_name(self):
757+ return self.git_identity
758+
759+ def getInternalPath(self):
760+ """See `IGitRepository`."""
761+ # This may need to change later to improve support for sharding.
762+ return str(self.id)
763+
764+ def getCodebrowseUrl(self):
765+ """See `IGitRepository`."""
766+ return urlutils.join(
767+ config.codehosting.git_browse_root, self.unique_name)
768+
769+ @property
770+ def private(self):
771+ return self.information_type in PRIVATE_INFORMATION_TYPES
772+
773+ def _reconcileAccess(self):
774+ """Reconcile the repository's sharing information.
775+
776+ Takes the information_type and target and makes the related
777+ AccessArtifact and AccessPolicyArtifacts match.
778+ """
779+ wanted_links = None
780+ pillars = []
781+ # For private personal repositories, we calculate the wanted grants.
782+ if (not self.project and not self.distribution and
783+ not self.information_type in PUBLIC_INFORMATION_TYPES):
784+ aasource = getUtility(IAccessArtifactSource)
785+ [abstract_artifact] = aasource.ensure([self])
786+ wanted_links = set(
787+ (abstract_artifact, policy) for policy in
788+ getUtility(IAccessPolicySource).findByTeam([self.owner]))
789+ else:
790+ # We haven't yet quite worked out how distribution privacy
791+ # works, so only work for projects for now.
792+ if self.project is not None:
793+ pillars = [self.project]
794+ reconcile_access_for_artifact(
795+ self, self.information_type, pillars, wanted_links)
796+
797+ @cachedproperty
798+ def _known_viewers(self):
799+ """A set of known persons able to view this repository.
800+
801+ This method must return an empty set or repository searches will
802+ trigger late evaluation. Any 'should be set on load' properties
803+ must be done by the repository search.
804+
805+ If you are tempted to change this method, don't. Instead see
806+ visibleByUser which defines the just-in-time policy for repository
807+ visibility, and IGitCollection which honours visibility rules.
808+ """
809+ return set()
810+
811+ def visibleByUser(self, user):
812+ """See `IGitRepository`."""
813+ if self.information_type in PUBLIC_INFORMATION_TYPES:
814+ return True
815+ elif user is None:
816+ return False
817+ elif user.id in self._known_viewers:
818+ return True
819+ else:
820+ # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is
821+ # in place.
822+ return False
823+
824+ def getAllowedInformationTypes(self, user):
825+ """See `IGitRepository`."""
826+ if user_has_special_git_repository_access(user):
827+ # Admins can set any type.
828+ types = set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES)
829+ else:
830+ # Otherwise the permitted types are defined by the namespace.
831+ # XXX cjwatson 2015-01-19: Define permitted types properly. For
832+ # now, non-admins only get public repository access.
833+ types = set(PUBLIC_INFORMATION_TYPES)
834+ return types
835+
836+ def transitionToInformationType(self, information_type, user,
837+ verify_policy=True):
838+ """See `IGitRepository`."""
839+ if self.information_type == information_type:
840+ return
841+ if (verify_policy and
842+ information_type not in self.getAllowedInformationTypes(user)):
843+ raise CannotChangeInformationType("Forbidden by project policy.")
844+ self.information_type = information_type
845+ self._reconcileAccess()
846+ # XXX cjwatson 2015-02-05: Once we have repository subscribers, we
847+ # need to grant them access if necessary. For now, treat the owner
848+ # as always subscribed, which is just about enough to make the
849+ # GitCollection tests pass.
850+ if information_type in PRIVATE_INFORMATION_TYPES:
851+ # Grant the subscriber access if they can't see the repository.
852+ service = getUtility(IService, "sharing")
853+ blind_subscribers = service.getPeopleWithoutAccess(
854+ self, [self.owner])
855+ if len(blind_subscribers):
856+ service.ensureAccessGrants(
857+ blind_subscribers, user, gitrepositories=[self],
858+ ignore_permissions=True)
859+ # As a result of the transition, some subscribers may no longer have
860+ # access to the repository. We need to run a job to remove any such
861+ # subscriptions.
862+ getUtility(IRemoveArtifactSubscriptionsJobSource).create(user, [self])
863+
864+ def setOwner(self, new_owner, user):
865+ """See `IGitRepository`."""
866+ # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
867+ # place.
868+ raise NotImplementedError
869+
870+ def destroySelf(self):
871+ raise NotImplementedError
872+
873+
874+class GitRepositorySet:
875+ """See `IGitRepositorySet`."""
876+
877+ implements(IGitRepositorySet)
878+
879+ def new(self, registrant, owner, target, name, information_type=None,
880+ date_created=DEFAULT):
881+ """See `IGitRepositorySet`."""
882+ # XXX cjwatson 2015-02-06: Fill this in once IGitNamespace is in
883+ # place.
884+ raise NotImplementedError
885+
886+ def getByPath(self, user, path):
887+ """See `IGitRepositorySet`."""
888+ # XXX cjwatson 2015-02-06: Fill this in once IGitLookup is in place.
889+ raise NotImplementedError
890+
891+ def getDefaultRepository(self, target, owner=None):
892+ """See `IGitRepositorySet`."""
893+ clauses = []
894+ if IProduct.providedBy(target):
895+ clauses.append(GitRepository.project == target)
896+ elif IDistributionSourcePackage.providedBy(target):
897+ clauses.append(GitRepository.distribution == target.distribution)
898+ clauses.append(
899+ GitRepository.sourcepackagename == target.sourcepackagename)
900+ else:
901+ raise GitTargetError(
902+ "Personal repositories cannot be defaults for any target.")
903+ if owner is not None:
904+ clauses.append(GitRepository.owner == owner)
905+ clauses.append(GitRepository.owner_default == True)
906+ else:
907+ clauses.append(GitRepository.target_default == True)
908+ return IStore(GitRepository).find(GitRepository, *clauses).one()
909+
910+ def getRepositories(self):
911+ """See `IGitRepositorySet`."""
912+ return []
913+
914+
915+def get_git_repository_privacy_filter(user):
916+ public_filter = GitRepository.information_type.is_in(
917+ PUBLIC_INFORMATION_TYPES)
918+
919+ if user is None:
920+ return [public_filter]
921+
922+ artifact_grant_query = Coalesce(
923+ ArrayIntersects(
924+ SQL("GitRepository.access_grants"),
925+ Select(
926+ ArrayAgg(TeamParticipation.teamID),
927+ tables=TeamParticipation,
928+ where=(TeamParticipation.person == user)
929+ )), False)
930+
931+ policy_grant_query = Coalesce(
932+ ArrayIntersects(
933+ Array(SQL("GitRepository.access_policy")),
934+ Select(
935+ ArrayAgg(AccessPolicyGrant.policy_id),
936+ tables=(AccessPolicyGrant,
937+ Join(TeamParticipation,
938+ TeamParticipation.teamID ==
939+ AccessPolicyGrant.grantee_id)),
940+ where=(TeamParticipation.person == user)
941+ )), False)
942+
943+ return [Or(public_filter, artifact_grant_query, policy_grant_query)]
944
945=== added file 'lib/lp/code/model/hasgitrepositories.py'
946--- lib/lp/code/model/hasgitrepositories.py 1970-01-01 00:00:00 +0000
947+++ lib/lp/code/model/hasgitrepositories.py 2015-02-19 18:43:50 +0000
948@@ -0,0 +1,28 @@
949+# Copyright 2015 Canonical Ltd. This software is licensed under the
950+# GNU Affero General Public License version 3 (see the file LICENSE).
951+
952+__metaclass__ = type
953+__all__ = [
954+ 'HasGitRepositoriesMixin',
955+ ]
956+
957+from zope.component import getUtility
958+
959+from lp.code.interfaces.gitrepository import IGitRepositorySet
960+
961+
962+class HasGitRepositoriesMixin:
963+ """A mixin implementation for `IHasGitRepositories`."""
964+
965+ def createGitRepository(self, registrant, owner, name,
966+ information_type=None):
967+ """See `IHasGitRepositories`."""
968+ return getUtility(IGitRepositorySet).new(
969+ registrant, owner, self, name,
970+ information_type=information_type)
971+
972+ def getGitRepositories(self, visible_by_user=None, eager_load=False):
973+ """See `IHasGitRepositories`."""
974+ # XXX cjwatson 2015-02-06: Fill this in once IGitCollection is in
975+ # place.
976+ raise NotImplementedError
977
978=== added file 'lib/lp/code/model/tests/test_hasgitrepositories.py'
979--- lib/lp/code/model/tests/test_hasgitrepositories.py 1970-01-01 00:00:00 +0000
980+++ lib/lp/code/model/tests/test_hasgitrepositories.py 2015-02-19 18:43:50 +0000
981@@ -0,0 +1,34 @@
982+# Copyright 2015 Canonical Ltd. This software is licensed under the
983+# GNU Affero General Public License version 3 (see the file LICENSE).
984+
985+"""Tests for classes that implement IHasGitRepositories."""
986+
987+__metaclass__ = type
988+
989+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
990+from lp.testing import (
991+ TestCaseWithFactory,
992+ verifyObject,
993+ )
994+from lp.testing.layers import DatabaseFunctionalLayer
995+
996+
997+class TestIHasGitRepositories(TestCaseWithFactory):
998+ """Test that the correct objects implement the interface."""
999+
1000+ layer = DatabaseFunctionalLayer
1001+
1002+ def test_project_implements_hasgitrepositories(self):
1003+ # Projects should implement IHasGitRepositories.
1004+ project = self.factory.makeProduct()
1005+ verifyObject(IHasGitRepositories, project)
1006+
1007+ def test_dsp_implements_hasgitrepositories(self):
1008+ # DistributionSourcePackages should implement IHasGitRepositories.
1009+ dsp = self.factory.makeDistributionSourcePackage()
1010+ verifyObject(IHasGitRepositories, dsp)
1011+
1012+ def test_person_implements_hasgitrepositories(self):
1013+ # People should implement IHasGitRepositories.
1014+ person = self.factory.makePerson()
1015+ verifyObject(IHasGitRepositories, person)
1016
1017=== modified file 'lib/lp/registry/configure.zcml'
1018--- lib/lp/registry/configure.zcml 2015-02-09 17:42:48 +0000
1019+++ lib/lp/registry/configure.zcml 2015-02-19 18:43:50 +0000
1020@@ -556,6 +556,11 @@
1021 bug_reporting_guidelines
1022 enable_bugfiling_duplicate_search
1023 "/>
1024+
1025+ <!-- IHasGitRepositories -->
1026+
1027+ <allow
1028+ interface="lp.code.interfaces.hasgitrepositories.IHasGitRepositories" />
1029 </class>
1030 <adapter
1031 provides="lp.registry.interfaces.distribution.IDistribution"
1032
1033=== modified file 'lib/lp/registry/interfaces/distributionsourcepackage.py'
1034--- lib/lp/registry/interfaces/distributionsourcepackage.py 2014-11-28 22:28:40 +0000
1035+++ lib/lp/registry/interfaces/distributionsourcepackage.py 2015-02-19 18:43:50 +0000
1036@@ -34,6 +34,7 @@
1037 IHasBranches,
1038 IHasMergeProposals,
1039 )
1040+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
1041 from lp.registry.interfaces.distribution import IDistribution
1042 from lp.registry.interfaces.role import IHasDrivers
1043 from lp.soyuz.enums import ArchivePurpose
1044@@ -42,7 +43,8 @@
1045 class IDistributionSourcePackage(IHeadingContext, IBugTarget, IHasBranches,
1046 IHasMergeProposals, IHasOfficialBugTags,
1047 IStructuralSubscriptionTarget,
1048- IQuestionTarget, IHasDrivers):
1049+ IQuestionTarget, IHasDrivers,
1050+ IHasGitRepositories):
1051 """Represents a source package in a distribution.
1052
1053 Create IDistributionSourcePackages by invoking
1054
1055=== modified file 'lib/lp/registry/interfaces/person.py'
1056--- lib/lp/registry/interfaces/person.py 2015-01-30 18:24:07 +0000
1057+++ lib/lp/registry/interfaces/person.py 2015-02-19 18:43:50 +0000
1058@@ -111,6 +111,7 @@
1059 IHasMergeProposals,
1060 IHasRequestedReviews,
1061 )
1062+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
1063 from lp.code.interfaces.hasrecipes import IHasRecipes
1064 from lp.registry.enums import (
1065 EXCLUSIVE_TEAM_POLICY,
1066@@ -688,7 +689,7 @@
1067 IHasMergeProposals, IHasMugshot,
1068 IHasLocation, IHasRequestedReviews, IObjectWithLocation,
1069 IHasBugs, IHasRecipes, IHasTranslationImports,
1070- IPersonSettings, IQuestionsPerson):
1071+ IPersonSettings, IQuestionsPerson, IHasGitRepositories):
1072 """IPerson attributes that require launchpad.View permission."""
1073 account = Object(schema=IAccount)
1074 accountID = Int(title=_('Account ID'), required=True, readonly=True)
1075
1076=== modified file 'lib/lp/registry/interfaces/product.py'
1077--- lib/lp/registry/interfaces/product.py 2015-01-30 18:24:07 +0000
1078+++ lib/lp/registry/interfaces/product.py 2015-02-19 18:43:50 +0000
1079@@ -102,6 +102,7 @@
1080 IHasCodeImports,
1081 IHasMergeProposals,
1082 )
1083+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
1084 from lp.code.interfaces.hasrecipes import IHasRecipes
1085 from lp.registry.enums import (
1086 BranchSharingPolicy,
1087@@ -475,7 +476,7 @@
1088 IHasMugshot, IHasSprints, IHasTranslationImports,
1089 ITranslationPolicy, IKarmaContext, IMakesAnnouncements,
1090 IOfficialBugTagTargetPublic, IHasOOPSReferences,
1091- IHasRecipes, IHasCodeImports, IServiceUsage):
1092+ IHasRecipes, IHasCodeImports, IServiceUsage, IHasGitRepositories):
1093 """Public IProduct properties."""
1094
1095 registrant = exported(
1096
1097=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
1098--- lib/lp/registry/model/distributionsourcepackage.py 2014-11-27 20:52:37 +0000
1099+++ lib/lp/registry/model/distributionsourcepackage.py 2015-02-19 18:43:50 +0000
1100@@ -43,6 +43,7 @@
1101 HasBranchesMixin,
1102 HasMergeProposalsMixin,
1103 )
1104+from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
1105 from lp.registry.interfaces.distributionsourcepackage import (
1106 IDistributionSourcePackage,
1107 )
1108@@ -119,7 +120,7 @@
1109 HasBranchesMixin,
1110 HasCustomLanguageCodesMixin,
1111 HasMergeProposalsMixin,
1112- HasDriversMixin):
1113+ HasDriversMixin, HasGitRepositoriesMixin):
1114 """This is a "Magic Distribution Source Package". It is not an
1115 SQLObject, but instead it represents a source package with a particular
1116 name in a particular distribution. You can then ask it all sorts of
1117
1118=== modified file 'lib/lp/registry/model/person.py'
1119--- lib/lp/registry/model/person.py 2015-01-28 16:10:51 +0000
1120+++ lib/lp/registry/model/person.py 2015-02-19 18:43:50 +0000
1121@@ -146,6 +146,7 @@
1122 HasMergeProposalsMixin,
1123 HasRequestedReviewsMixin,
1124 )
1125+from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
1126 from lp.registry.enums import (
1127 EXCLUSIVE_TEAM_POLICY,
1128 INCLUSIVE_TEAM_POLICY,
1129@@ -476,7 +477,7 @@
1130 class Person(
1131 SQLBase, HasBugsBase, HasSpecificationsMixin, HasTranslationImportsMixin,
1132 HasBranchesMixin, HasMergeProposalsMixin, HasRequestedReviewsMixin,
1133- QuestionsPersonMixin):
1134+ QuestionsPersonMixin, HasGitRepositoriesMixin):
1135 """A Person."""
1136
1137 implements(IPerson, IHasIcon, IHasLogo, IHasMugshot)
1138
1139=== modified file 'lib/lp/registry/model/product.py'
1140--- lib/lp/registry/model/product.py 2015-01-29 16:28:30 +0000
1141+++ lib/lp/registry/model/product.py 2015-02-19 18:43:50 +0000
1142@@ -124,6 +124,7 @@
1143 HasCodeImportsMixin,
1144 HasMergeProposalsMixin,
1145 )
1146+from lp.code.model.hasgitrepositories import HasGitRepositoriesMixin
1147 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
1148 from lp.code.model.sourcepackagerecipedata import SourcePackageRecipeData
1149 from lp.registry.enums import (
1150@@ -361,7 +362,7 @@
1151 OfficialBugTagTargetMixin, HasBranchesMixin,
1152 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
1153 HasCodeImportsMixin, InformationTypeMixin,
1154- TranslationPolicyMixin):
1155+ TranslationPolicyMixin, HasGitRepositoriesMixin):
1156 """A Product."""
1157
1158 implements(
1159
1160=== modified file 'lib/lp/registry/tests/test_product.py'
1161--- lib/lp/registry/tests/test_product.py 2015-01-29 16:28:30 +0000
1162+++ lib/lp/registry/tests/test_product.py 2015-02-19 18:43:50 +0000
1163@@ -858,10 +858,10 @@
1164 'getCustomLanguageCode', 'getDefaultBugInformationType',
1165 'getDefaultSpecificationInformationType',
1166 'getEffectiveTranslationPermission', 'getExternalBugTracker',
1167- 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
1168- 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
1169- 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
1170- 'getSeries', 'getSubscription',
1171+ 'getFAQ', 'getFirstEntryToImport', 'getGitRepositories',
1172+ 'getLinkedBugWatches', 'getMergeProposals', 'getMilestone',
1173+ 'getMilestonesAndReleases', 'getQuestion', 'getQuestionLanguages',
1174+ 'getPackage', 'getRelease', 'getSeries', 'getSubscription',
1175 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
1176 'getTopContributors', 'getTopContributorsGroupedByCategory',
1177 'getTranslationGroups', 'getTranslationImportQueueEntries',
1178@@ -902,7 +902,8 @@
1179 'launchpad.Edit': set((
1180 'addOfficialBugTag', 'removeOfficialBugTag',
1181 'setBranchSharingPolicy', 'setBugSharingPolicy',
1182- 'setSpecificationSharingPolicy', 'checkInformationType')),
1183+ 'setSpecificationSharingPolicy', 'checkInformationType',
1184+ 'createGitRepository')),
1185 'launchpad.Moderate': set((
1186 'is_permitted', 'license_approved', 'project_reviewed',
1187 'reviewer_whiteboard', 'setAliases')),
1188
1189=== modified file 'lib/lp/security.py'
1190--- lib/lp/security.py 2015-01-06 04:52:44 +0000
1191+++ lib/lp/security.py 2015-02-19 18:43:50 +0000
1192@@ -83,6 +83,10 @@
1193 )
1194 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
1195 from lp.code.interfaces.diff import IPreviewDiff
1196+from lp.code.interfaces.gitrepository import (
1197+ IGitRepository,
1198+ user_has_special_git_repository_access,
1199+ )
1200 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
1201 from lp.code.interfaces.sourcepackagerecipebuild import (
1202 ISourcePackageRecipeBuild,
1203@@ -1151,14 +1155,37 @@
1204
1205
1206 class EditDistributionSourcePackage(AuthorizationBase):
1207- """DistributionSourcePackage is not editable.
1208-
1209- But EditStructuralSubscription needs launchpad.Edit defined on all
1210- targets.
1211- """
1212 permission = 'launchpad.Edit'
1213 usedfor = IDistributionSourcePackage
1214
1215+ def _checkUpload(self, user, archive, distroseries):
1216+ # We use verifyUpload() instead of checkUpload() because we don't
1217+ # have a pocket. It returns the reason the user can't upload or
1218+ # None if they are allowed.
1219+ if distroseries is None:
1220+ return False
1221+ reason = archive.verifyUpload(
1222+ user.person, sourcepackagename=self.obj.sourcepackagename,
1223+ component=None, distroseries=distroseries, strict_component=False)
1224+ return reason is None
1225+
1226+ def checkAuthenticated(self, user):
1227+ """Anyone who can upload a package can edit it.
1228+
1229+ Checking upload permission requires a distroseries; a reasonable
1230+ approximation is to check whether the user can upload the package to
1231+ the current series.
1232+ """
1233+ if user.in_admin:
1234+ return True
1235+
1236+ distribution = self.obj.distribution
1237+ if user.inTeam(distribution.owner):
1238+ return True
1239+
1240+ return self._checkUpload(
1241+ user, distribution.main_archive, distribution.currentseries)
1242+
1243
1244 class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
1245 """Product's owner and bug supervisor can set official bug tags."""
1246@@ -2176,6 +2203,57 @@
1247 return user.in_admin
1248
1249
1250+class ViewGitRepository(AuthorizationBase):
1251+ """Controls visibility of Git repositories.
1252+
1253+ A person can see the repository if the repository is public, they are
1254+ the owner of the repository, they are in the team that owns the
1255+ repository, they have an access grant to the repository, or they are a
1256+ Launchpad administrator.
1257+ """
1258+ permission = 'launchpad.View'
1259+ usedfor = IGitRepository
1260+
1261+ def checkAuthenticated(self, user):
1262+ return self.obj.visibleByUser(user.person)
1263+
1264+ def checkUnauthenticated(self):
1265+ return self.obj.visibleByUser(None)
1266+
1267+
1268+class EditGitRepository(AuthorizationBase):
1269+ """The owner or admins can edit Git repositories."""
1270+ permission = 'launchpad.Edit'
1271+ usedfor = IGitRepository
1272+
1273+ def checkAuthenticated(self, user):
1274+ # XXX cjwatson 2015-01-23: People who can upload source packages to
1275+ # a distribution should be able to push to the corresponding
1276+ # "official" repositories, once those are defined.
1277+ return (
1278+ user.inTeam(self.obj.owner) or
1279+ user_has_special_git_repository_access(user.person))
1280+
1281+
1282+class ModerateGitRepository(EditGitRepository):
1283+ """The owners, project owners, and admins can moderate Git repositories."""
1284+ permission = 'launchpad.Moderate'
1285+
1286+ def checkAuthenticated(self, user):
1287+ if super(ModerateGitRepository, self).checkAuthenticated(user):
1288+ return True
1289+ target = self.obj.target
1290+ if (target is not None and IProduct.providedBy(target) and
1291+ user.inTeam(target.owner)):
1292+ return True
1293+ return user.in_commercial_admin
1294+
1295+
1296+class AdminGitRepository(AdminByAdminsTeam):
1297+ """The admins can administer Git repositories."""
1298+ usedfor = IGitRepository
1299+
1300+
1301 class AdminDistroSeriesTranslations(AuthorizationBase):
1302 permission = 'launchpad.TranslationsAdmin'
1303 usedfor = IDistroSeries
1304@@ -2858,8 +2936,7 @@
1305 usedfor = IPublisherConfig
1306
1307
1308-class EditSourcePackage(AuthorizationBase):
1309- permission = 'launchpad.Edit'
1310+class EditSourcePackage(EditDistributionSourcePackage):
1311 usedfor = ISourcePackage
1312
1313 def checkAuthenticated(self, user):
1314@@ -2871,15 +2948,8 @@
1315 if user.inTeam(distribution.owner):
1316 return True
1317
1318- # We use verifyUpload() instead of checkUpload() because
1319- # we don't have a pocket.
1320- # It returns the reason the user can't upload
1321- # or None if they are allowed.
1322- reason = distribution.main_archive.verifyUpload(
1323- user.person, distroseries=self.obj.distroseries,
1324- sourcepackagename=self.obj.sourcepackagename,
1325- component=None, strict_component=False)
1326- return reason is None
1327+ return self._checkUpload(
1328+ user, distribution.main_archive, self.obj.distroseries)
1329
1330
1331 class ViewLiveFS(DelegatedAuthorization):
1332
1333=== modified file 'lib/lp/services/config/schema-lazr.conf'
1334--- lib/lp/services/config/schema-lazr.conf 2014-08-05 08:58:14 +0000
1335+++ lib/lp/services/config/schema-lazr.conf 2015-02-19 18:43:50 +0000
1336@@ -335,6 +335,27 @@
1337 # of shutting down and so should not receive any more connections.
1338 web_status_port = tcp:8022
1339
1340+# The URL of the internal Git hosting API endpoint.
1341+internal_git_api_endpoint: none
1342+
1343+# The URL prefix for links to the Git code browser. Links are formed by
1344+# appending the repository's path to the root URL.
1345+#
1346+# datatype: urlbase
1347+git_browse_root: none
1348+
1349+# The URL prefix for anonymous Git protocol fetches. Links are formed by
1350+# appending the repository's path to the root URL.
1351+#
1352+# datatype: urlbase
1353+git_anon_root: none
1354+
1355+# The URL prefix for Git-over-SSH. Links are formed by appending the
1356+# repository's path to the root URL.
1357+#
1358+# datatype: urlbase
1359+git_ssh_root: none
1360+
1361
1362 [codeimport]
1363 # Where the Bazaar imports are stored.