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