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