Merge lp:~cjwatson/launchpad/git-webservice into lp:launchpad
- git-webservice
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Merged at revision: | 17384 |
Proposed branch: | lp:~cjwatson/launchpad/git-webservice |
Merge into: | lp:launchpad |
Diff against target: |
795 lines (+388/-47) 10 files modified
lib/lp/app/browser/launchpad.py (+2/-0) lib/lp/code/browser/configure.zcml (+5/-0) lib/lp/code/configure.zcml (+2/-1) lib/lp/code/interfaces/gitrepository.py (+133/-37) lib/lp/code/interfaces/hasgitrepositories.py (+4/-0) lib/lp/code/interfaces/webservice.py (+8/-0) lib/lp/code/model/tests/test_gitrepository.py (+176/-8) lib/lp/registry/interfaces/sharingservice.py (+18/-1) lib/lp/registry/services/tests/test_sharingservice.py (+17/-0) lib/lp/services/webservice/wadl-to-refhtml.xsl (+23/-0) |
To merge this branch: | bzr merge lp:~cjwatson/launchpad/git-webservice |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+251978@code.launchpad.net |
Commit message
Export Git-related methods on the webservice.
Description of the change
Export Git-related methods on the webservice.
This is reasonably basic, but also fairly complete with respect to the operations that exist so far. The main thing that's not exported is IGitRepositoryS
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/app/browser/launchpad.py' |
2 | --- lib/lp/app/browser/launchpad.py 2014-11-27 11:01:16 +0000 |
3 | +++ lib/lp/app/browser/launchpad.py 2015-03-06 16:33:15 +0000 |
4 | @@ -102,6 +102,7 @@ |
5 | from lp.code.interfaces.branchlookup import IBranchLookup |
6 | from lp.code.interfaces.codehosting import IBazaarApplication |
7 | from lp.code.interfaces.codeimport import ICodeImportSet |
8 | +from lp.code.interfaces.gitrepository import IGitRepositorySet |
9 | from lp.hardwaredb.interfaces.hwdb import IHWDBApplication |
10 | from lp.layers import WebServiceLayer |
11 | from lp.registry.interfaces.announcement import IAnnouncementSet |
12 | @@ -783,6 +784,7 @@ |
13 | 'codeofconduct': ICodeOfConductSet, |
14 | '+countries': ICountrySet, |
15 | 'distros': IDistributionSet, |
16 | + '+git': IGitRepositorySet, |
17 | '+hwdb': IHWDBApplication, |
18 | 'karmaaction': IKarmaActionSet, |
19 | '+imports': ITranslationImportQueue, |
20 | |
21 | === modified file 'lib/lp/code/browser/configure.zcml' |
22 | --- lib/lp/code/browser/configure.zcml 2015-03-04 16:56:48 +0000 |
23 | +++ lib/lp/code/browser/configure.zcml 2015-03-06 16:33:15 +0000 |
24 | @@ -13,6 +13,11 @@ |
25 | path_expression="string:branches" |
26 | parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" |
27 | /> |
28 | + <browser:url |
29 | + for="lp.code.interfaces.gitrepository.IGitRepositorySet" |
30 | + path_expression="string:+git" |
31 | + parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" |
32 | + /> |
33 | <browser:feeds |
34 | module="lp.code.feed.branch" |
35 | classes="BranchFeed PersonBranchFeed ProductBranchFeed ProjectBranchFeed |
36 | |
37 | === modified file 'lib/lp/code/configure.zcml' |
38 | --- lib/lp/code/configure.zcml 2015-02-26 17:20:31 +0000 |
39 | +++ lib/lp/code/configure.zcml 2015-03-06 16:33:15 +0000 |
40 | @@ -818,7 +818,8 @@ |
41 | <require |
42 | permission="launchpad.Moderate" |
43 | interface="lp.code.interfaces.gitrepository.IGitRepositoryModerate" |
44 | - set_schema="lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" /> |
45 | + set_schema="lp.code.interfaces.gitrepository.IGitRepositoryModerateAttributes" |
46 | + set_attributes="date_last_modified" /> |
47 | <require |
48 | permission="launchpad.Edit" |
49 | interface="lp.code.interfaces.gitrepository.IGitRepositoryEdit" /> |
50 | |
51 | === modified file 'lib/lp/code/interfaces/gitrepository.py' |
52 | --- lib/lp/code/interfaces/gitrepository.py 2015-03-05 14:13:16 +0000 |
53 | +++ lib/lp/code/interfaces/gitrepository.py 2015-03-06 16:33:15 +0000 |
54 | @@ -17,7 +17,24 @@ |
55 | |
56 | import re |
57 | |
58 | +from lazr.restful.declarations import ( |
59 | + call_with, |
60 | + collection_default_content, |
61 | + export_as_webservice_collection, |
62 | + export_as_webservice_entry, |
63 | + export_destructor_operation, |
64 | + export_read_operation, |
65 | + export_write_operation, |
66 | + exported, |
67 | + mutator_for, |
68 | + operation_for_version, |
69 | + operation_parameters, |
70 | + operation_returns_collection_of, |
71 | + operation_returns_entry, |
72 | + REQUEST_USER, |
73 | + ) |
74 | from lazr.restful.fields import Reference |
75 | +from lazr.restful.interface import copy_field |
76 | from zope.component import getUtility |
77 | from zope.interface import ( |
78 | Attribute, |
79 | @@ -40,6 +57,7 @@ |
80 | from lp.registry.interfaces.distributionsourcepackage import ( |
81 | IDistributionSourcePackage, |
82 | ) |
83 | +from lp.registry.interfaces.person import IPerson |
84 | from lp.registry.interfaces.persondistributionsourcepackage import ( |
85 | IPersonDistributionSourcePackageFactory, |
86 | ) |
87 | @@ -97,68 +115,76 @@ |
88 | |
89 | id = Int(title=_("ID"), readonly=True, required=True) |
90 | |
91 | - date_created = Datetime( |
92 | - title=_("Date created"), required=True, readonly=True) |
93 | - |
94 | - date_last_modified = Datetime( |
95 | - title=_("Date last modified"), required=True, readonly=True) |
96 | - |
97 | - registrant = PublicPersonChoice( |
98 | + date_created = exported(Datetime( |
99 | + title=_("Date created"), required=True, readonly=True)) |
100 | + |
101 | + registrant = exported(PublicPersonChoice( |
102 | title=_("Registrant"), required=True, readonly=True, |
103 | vocabulary="ValidPersonOrTeam", |
104 | - description=_("The person who registered this Git repository.")) |
105 | + description=_("The person who registered this Git repository."))) |
106 | |
107 | - owner = PersonChoice( |
108 | - title=_("Owner"), required=True, readonly=False, |
109 | + owner = exported(PersonChoice( |
110 | + title=_("Owner"), required=True, readonly=True, |
111 | vocabulary="AllUserTeamsParticipationPlusSelf", |
112 | description=_( |
113 | "The owner of this Git repository. This controls who can modify " |
114 | - "the repository.")) |
115 | + "the repository."))) |
116 | |
117 | - target = Reference( |
118 | - title=_("Target"), required=True, readonly=True, |
119 | - schema=IHasGitRepositories, |
120 | - description=_("The target of the repository.")) |
121 | + target = exported( |
122 | + Reference( |
123 | + title=_("Target"), required=True, readonly=True, |
124 | + schema=IHasGitRepositories, |
125 | + description=_("The target of the repository.")), |
126 | + as_of="devel") |
127 | |
128 | namespace = Attribute( |
129 | "The namespace of this repository, as an `IGitNamespace`.") |
130 | |
131 | - information_type = Choice( |
132 | + # XXX cjwatson 2015-01-29: Add some advice about default repository |
133 | + # naming. |
134 | + name = exported(TextLine( |
135 | + title=_("Name"), required=True, readonly=True, |
136 | + constraint=git_repository_name_validator, |
137 | + description=_( |
138 | + "The repository name. Keep very short, unique, and descriptive, " |
139 | + "because it will be used in URLs."))) |
140 | + |
141 | + information_type = exported(Choice( |
142 | title=_("Information type"), vocabulary=InformationType, |
143 | required=True, readonly=True, default=InformationType.PUBLIC, |
144 | description=_( |
145 | - "The type of information contained in this repository.")) |
146 | + "The type of information contained in this repository."))) |
147 | |
148 | - owner_default = Bool( |
149 | + owner_default = exported(Bool( |
150 | title=_("Owner default"), required=True, readonly=True, |
151 | description=_( |
152 | "Whether this repository is the default for its owner and " |
153 | - "target.")) |
154 | + "target."))) |
155 | |
156 | - target_default = Bool( |
157 | + target_default = exported(Bool( |
158 | title=_("Target default"), required=True, readonly=True, |
159 | description=_( |
160 | - "Whether this repository is the default for its target.")) |
161 | + "Whether this repository is the default for its target."))) |
162 | |
163 | - unique_name = Text( |
164 | + unique_name = exported(Text( |
165 | title=_("Unique name"), readonly=True, |
166 | description=_( |
167 | "Unique name of the repository, including the owner and project " |
168 | - "names.")) |
169 | + "names."))) |
170 | |
171 | - display_name = Text( |
172 | + display_name = exported(Text( |
173 | title=_("Display name"), readonly=True, |
174 | - description=_("Display name of the repository.")) |
175 | + description=_("Display name of the repository."))) |
176 | |
177 | shortened_path = Attribute( |
178 | "The shortest reasonable version of the path to this repository.") |
179 | |
180 | - git_identity = Text( |
181 | + git_identity = exported(Text( |
182 | title=_("Git identity"), readonly=True, |
183 | description=_( |
184 | "If this is the default repository for some target, then this is " |
185 | "'lp:' plus a shortcut version of the path via that target. " |
186 | - "Otherwise it is simply 'lp:' plus the unique name.")) |
187 | + "Otherwise it is simply 'lp:' plus the unique name."))) |
188 | |
189 | def setOwnerDefault(value): |
190 | """Set whether this repository is the default for its owner-target. |
191 | @@ -242,19 +268,20 @@ |
192 | """IGitRepository attributes that can be edited by more than one community. |
193 | """ |
194 | |
195 | - # XXX cjwatson 2015-01-29: Add some advice about default repository |
196 | - # naming. |
197 | - name = TextLine( |
198 | - title=_("Name"), required=True, |
199 | - constraint=git_repository_name_validator, |
200 | - description=_( |
201 | - "The repository name. Keep very short, unique, and descriptive, " |
202 | - "because it will be used in URLs.")) |
203 | + date_last_modified = exported(Datetime( |
204 | + title=_("Date last modified"), required=True, readonly=True)) |
205 | |
206 | |
207 | class IGitRepositoryModerate(Interface): |
208 | """IGitRepository methods that can be called by more than one community.""" |
209 | |
210 | + @mutator_for(IGitRepositoryView["information_type"]) |
211 | + @operation_parameters( |
212 | + information_type=copy_field(IGitRepositoryView["information_type"]), |
213 | + ) |
214 | + @call_with(user=REQUEST_USER) |
215 | + @export_write_operation() |
216 | + @operation_for_version("devel") |
217 | def transitionToInformationType(information_type, user, |
218 | verify_policy=True): |
219 | """Set the information type for this repository. |
220 | @@ -269,12 +296,31 @@ |
221 | class IGitRepositoryEdit(Interface): |
222 | """IGitRepository methods that require launchpad.Edit permission.""" |
223 | |
224 | + @mutator_for(IGitRepositoryView["owner"]) |
225 | + @call_with(user=REQUEST_USER) |
226 | + @operation_parameters( |
227 | + new_owner=Reference( |
228 | + title=_("The new owner of the repository."), schema=IPerson)) |
229 | + @export_write_operation() |
230 | + @operation_for_version("devel") |
231 | def setOwner(new_owner, user): |
232 | """Set the owner of the repository to be `new_owner`.""" |
233 | |
234 | + @mutator_for(IGitRepositoryView["target"]) |
235 | + @call_with(user=REQUEST_USER) |
236 | + @operation_parameters( |
237 | + target=Reference( |
238 | + title=_( |
239 | + "The project, distribution source package, or person the " |
240 | + "repository belongs to."), |
241 | + schema=IHasGitRepositories, required=True)) |
242 | + @export_write_operation() |
243 | + @operation_for_version("devel") |
244 | def setTarget(target, user): |
245 | """Set the target of the repository.""" |
246 | |
247 | + @export_destructor_operation() |
248 | + @operation_for_version("devel") |
249 | def destroySelf(): |
250 | """Delete the specified repository.""" |
251 | |
252 | @@ -283,14 +329,22 @@ |
253 | IGitRepositoryModerate, IGitRepositoryEdit): |
254 | """A Git repository.""" |
255 | |
256 | - private = Bool( |
257 | + # Mark repositories as exported entries for the Launchpad API. |
258 | + # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL |
259 | + # generation working. Individual attributes must set their version to |
260 | + # "devel". |
261 | + export_as_webservice_entry(plural_name="git_repositories", as_of="beta") |
262 | + |
263 | + private = exported(Bool( |
264 | title=_("Private"), required=False, readonly=True, |
265 | - description=_("This repository is visible only to its subscribers.")) |
266 | + description=_("This repository is visible only to its subscribers."))) |
267 | |
268 | |
269 | class IGitRepositorySet(Interface): |
270 | """Interface representing the set of Git repositories.""" |
271 | |
272 | + export_as_webservice_collection(IGitRepository) |
273 | + |
274 | def new(registrant, owner, target, name, information_type=None, |
275 | date_created=None): |
276 | """Create a Git repository and return it. |
277 | @@ -306,6 +360,12 @@ |
278 | """ |
279 | |
280 | # Marker for references to Git URL layouts: ##GITNAMESPACE## |
281 | + @call_with(user=REQUEST_USER) |
282 | + @operation_parameters( |
283 | + path=TextLine(title=_("Repository path"), required=True)) |
284 | + @operation_returns_entry(IGitRepository) |
285 | + @export_read_operation() |
286 | + @operation_for_version("devel") |
287 | def getByPath(user, path): |
288 | """Find a repository by its path. |
289 | |
290 | @@ -324,6 +384,13 @@ |
291 | Return None if no match was found. |
292 | """ |
293 | |
294 | + @call_with(user=REQUEST_USER) |
295 | + @operation_parameters( |
296 | + target=Reference( |
297 | + title=_("Target"), required=True, schema=IHasGitRepositories)) |
298 | + @operation_returns_collection_of(IGitRepository) |
299 | + @export_read_operation() |
300 | + @operation_for_version("devel") |
301 | def getRepositories(user, target): |
302 | """Get all repositories for a target. |
303 | |
304 | @@ -334,6 +401,12 @@ |
305 | :return: A collection of `IGitRepository` objects. |
306 | """ |
307 | |
308 | + @operation_parameters( |
309 | + target=Reference( |
310 | + title=_("Target"), required=True, schema=IHasGitRepositories)) |
311 | + @operation_returns_entry(IGitRepository) |
312 | + @export_read_operation() |
313 | + @operation_for_version("devel") |
314 | def getDefaultRepository(target): |
315 | """Get the default repository for a target. |
316 | |
317 | @@ -343,6 +416,13 @@ |
318 | :return: An `IGitRepository`, or None. |
319 | """ |
320 | |
321 | + @operation_parameters( |
322 | + owner=Reference(title=_("Owner"), required=True, schema=IPerson), |
323 | + target=Reference( |
324 | + title=_("Target"), required=True, schema=IHasGitRepositories)) |
325 | + @operation_returns_entry(IGitRepository) |
326 | + @export_read_operation() |
327 | + @operation_for_version("devel") |
328 | def getDefaultRepositoryForOwner(owner, target): |
329 | """Get a person's default repository for a target. |
330 | |
331 | @@ -353,6 +433,13 @@ |
332 | :return: An `IGitRepository`, or None. |
333 | """ |
334 | |
335 | + @operation_parameters( |
336 | + target=Reference( |
337 | + title=_("Target"), required=True, schema=IHasGitRepositories), |
338 | + repository=Reference( |
339 | + title=_("Git repository"), required=False, schema=IGitRepository)) |
340 | + @export_write_operation() |
341 | + @operation_for_version("devel") |
342 | def setDefaultRepository(target, repository): |
343 | """Set the default repository for a target. |
344 | |
345 | @@ -363,6 +450,14 @@ |
346 | :raises GitTargetError: if `target` is an `IPerson`. |
347 | """ |
348 | |
349 | + @operation_parameters( |
350 | + owner=Reference(title=_("Owner"), required=True, schema=IPerson), |
351 | + target=Reference( |
352 | + title=_("Target"), required=True, schema=IHasGitRepositories), |
353 | + repository=Reference( |
354 | + title=_("Git repository"), required=False, schema=IGitRepository)) |
355 | + @export_write_operation() |
356 | + @operation_for_version("devel") |
357 | def setDefaultRepositoryForOwner(owner, target, repository): |
358 | """Set a person's default repository for a target. |
359 | |
360 | @@ -374,6 +469,7 @@ |
361 | :raises GitTargetError: if `target` is an `IPerson`. |
362 | """ |
363 | |
364 | + @collection_default_content() |
365 | def empty_list(): |
366 | """Return an empty collection of repositories. |
367 | |
368 | |
369 | === modified file 'lib/lp/code/interfaces/hasgitrepositories.py' |
370 | --- lib/lp/code/interfaces/hasgitrepositories.py 2015-03-04 19:05:47 +0000 |
371 | +++ lib/lp/code/interfaces/hasgitrepositories.py 2015-03-06 16:33:15 +0000 |
372 | @@ -9,6 +9,7 @@ |
373 | 'IHasGitRepositories', |
374 | ] |
375 | |
376 | +from lazr.restful.declarations import export_as_webservice_entry |
377 | from zope.interface import Interface |
378 | |
379 | |
380 | @@ -18,3 +19,6 @@ |
381 | A project contains Git repositories, a source package on a distribution |
382 | contains branches, and a person contains "personal" branches. |
383 | """ |
384 | + |
385 | + export_as_webservice_entry( |
386 | + singular_name="git_target", plural_name="git_targets", as_of="devel") |
387 | |
388 | === modified file 'lib/lp/code/interfaces/webservice.py' |
389 | --- lib/lp/code/interfaces/webservice.py 2015-01-30 18:24:07 +0000 |
390 | +++ lib/lp/code/interfaces/webservice.py 2015-03-06 16:33:15 +0000 |
391 | @@ -25,6 +25,9 @@ |
392 | 'ICodeReviewComment', |
393 | 'ICodeReviewVoteReference', |
394 | 'IDiff', |
395 | + 'IGitRepository', |
396 | + 'IGitRepositorySet', |
397 | + 'IHasGitRepositories', |
398 | 'IPreviewDiff', |
399 | 'ISourcePackageRecipe', |
400 | 'ISourcePackageRecipeBuild', |
401 | @@ -57,6 +60,11 @@ |
402 | IDiff, |
403 | IPreviewDiff, |
404 | ) |
405 | +from lp.code.interfaces.gitrepository import ( |
406 | + IGitRepository, |
407 | + IGitRepositorySet, |
408 | + ) |
409 | +from lp.code.interfaces.hasgitrepositories import IHasGitRepositories |
410 | from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe |
411 | from lp.code.interfaces.sourcepackagerecipebuild import ( |
412 | ISourcePackageRecipeBuild, |
413 | |
414 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' |
415 | --- lib/lp/code/model/tests/test_gitrepository.py 2015-03-05 14:13:16 +0000 |
416 | +++ lib/lp/code/model/tests/test_gitrepository.py 2015-03-06 16:33:15 +0000 |
417 | @@ -7,6 +7,7 @@ |
418 | |
419 | from datetime import datetime |
420 | from functools import partial |
421 | +import json |
422 | |
423 | from lazr.lifecycle.event import ObjectModifiedEvent |
424 | import pytz |
425 | @@ -54,8 +55,11 @@ |
426 | from lp.services.database.constants import UTC_NOW |
427 | from lp.services.features.testing import FeatureFixture |
428 | from lp.services.webapp.authorization import check_permission |
429 | +from lp.services.webapp.interfaces import OAuthPermission |
430 | from lp.testing import ( |
431 | admin_logged_in, |
432 | + ANONYMOUS, |
433 | + api_url, |
434 | celebrity_logged_in, |
435 | person_logged_in, |
436 | TestCaseWithFactory, |
437 | @@ -65,6 +69,7 @@ |
438 | DatabaseFunctionalLayer, |
439 | ZopelessDatabaseLayer, |
440 | ) |
441 | +from lp.testing.pages import webservice_for_person |
442 | |
443 | |
444 | class TestGitRepositoryFeatureFlag(TestCaseWithFactory): |
445 | @@ -476,14 +481,6 @@ |
446 | self.assertEqual( |
447 | InformationType.PRIVATESECURITY, repository.information_type) |
448 | |
449 | - def test_attribute_smoketest(self): |
450 | - # Users with launchpad.Moderate can set attributes. |
451 | - project = self.factory.makeProduct() |
452 | - repository = self.factory.makeGitRepository(target=project) |
453 | - with person_logged_in(project.owner): |
454 | - repository.name = u"not-secret" |
455 | - self.assertEqual(u"not-secret", repository.name) |
456 | - |
457 | |
458 | class TestGitRepositorySetOwner(TestCaseWithFactory): |
459 | """Test `IGitRepository.setOwner`.""" |
460 | @@ -858,3 +855,174 @@ |
461 | TestGitRepositorySetDefaultsOwnerMixin, |
462 | TestGitRepositorySetDefaultsPackage): |
463 | pass |
464 | + |
465 | + |
466 | +class TestGitRepositoryWebservice(TestCaseWithFactory): |
467 | + """Tests for the webservice.""" |
468 | + |
469 | + layer = DatabaseFunctionalLayer |
470 | + |
471 | + def setUp(self): |
472 | + super(TestGitRepositoryWebservice, self).setUp() |
473 | + self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"})) |
474 | + |
475 | + def test_getRepositories_project(self): |
476 | + project_db = self.factory.makeProduct() |
477 | + repository_db = self.factory.makeGitRepository(target=project_db) |
478 | + webservice = webservice_for_person( |
479 | + repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC) |
480 | + webservice.default_api_version = "devel" |
481 | + with person_logged_in(ANONYMOUS): |
482 | + repository_url = api_url(repository_db) |
483 | + owner_url = api_url(repository_db.owner) |
484 | + project_url = api_url(project_db) |
485 | + response = webservice.named_get( |
486 | + "/+git", "getRepositories", user=owner_url, target=project_url) |
487 | + self.assertEqual(200, response.status) |
488 | + self.assertEqual( |
489 | + [webservice.getAbsoluteUrl(repository_url)], |
490 | + [entry["self_link"] for entry in response.jsonBody()["entries"]]) |
491 | + |
492 | + def test_getRepositories_package(self): |
493 | + dsp_db = self.factory.makeDistributionSourcePackage() |
494 | + repository_db = self.factory.makeGitRepository(target=dsp_db) |
495 | + webservice = webservice_for_person( |
496 | + repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC) |
497 | + webservice.default_api_version = "devel" |
498 | + with person_logged_in(ANONYMOUS): |
499 | + repository_url = api_url(repository_db) |
500 | + owner_url = api_url(repository_db.owner) |
501 | + dsp_url = api_url(dsp_db) |
502 | + response = webservice.named_get( |
503 | + "/+git", "getRepositories", user=owner_url, target=dsp_url) |
504 | + self.assertEqual(200, response.status) |
505 | + self.assertEqual( |
506 | + [webservice.getAbsoluteUrl(repository_url)], |
507 | + [entry["self_link"] for entry in response.jsonBody()["entries"]]) |
508 | + |
509 | + def test_getRepositories_personal(self): |
510 | + owner_db = self.factory.makePerson() |
511 | + repository_db = self.factory.makeGitRepository( |
512 | + owner=owner_db, target=owner_db) |
513 | + webservice = webservice_for_person( |
514 | + owner_db, permission=OAuthPermission.WRITE_PUBLIC) |
515 | + webservice.default_api_version = "devel" |
516 | + with person_logged_in(ANONYMOUS): |
517 | + repository_url = api_url(repository_db) |
518 | + owner_url = api_url(owner_db) |
519 | + response = webservice.named_get( |
520 | + "/+git", "getRepositories", user=owner_url, target=owner_url) |
521 | + self.assertEqual(200, response.status) |
522 | + self.assertEqual( |
523 | + [webservice.getAbsoluteUrl(repository_url)], |
524 | + [entry["self_link"] for entry in response.jsonBody()["entries"]]) |
525 | + |
526 | + def test_set_information_type(self): |
527 | + # The repository owner can change the information type. |
528 | + repository_db = self.factory.makeGitRepository() |
529 | + webservice = webservice_for_person( |
530 | + repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC) |
531 | + webservice.default_api_version = "devel" |
532 | + with person_logged_in(ANONYMOUS): |
533 | + repository_url = api_url(repository_db) |
534 | + response = webservice.patch( |
535 | + repository_url, "application/json", |
536 | + json.dumps({"information_type": "Public Security"})) |
537 | + self.assertEqual(209, response.status) |
538 | + with person_logged_in(ANONYMOUS): |
539 | + self.assertEqual( |
540 | + InformationType.PUBLICSECURITY, repository_db.information_type) |
541 | + |
542 | + def test_set_information_type_other_person(self): |
543 | + # An unrelated user cannot change the information type. |
544 | + repository_db = self.factory.makeGitRepository() |
545 | + webservice = webservice_for_person( |
546 | + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) |
547 | + webservice.default_api_version = "devel" |
548 | + with person_logged_in(ANONYMOUS): |
549 | + repository_url = api_url(repository_db) |
550 | + response = webservice.patch( |
551 | + repository_url, "application/json", |
552 | + json.dumps({"information_type": "Public Security"})) |
553 | + self.assertEqual(401, response.status) |
554 | + with person_logged_in(ANONYMOUS): |
555 | + self.assertEqual( |
556 | + InformationType.PUBLIC, repository_db.information_type) |
557 | + |
558 | + def test_set_target(self): |
559 | + # The repository owner can move the repository to another target; |
560 | + # this redirects to the new location. |
561 | + repository_db = self.factory.makeGitRepository() |
562 | + new_project_db = self.factory.makeProduct() |
563 | + webservice = webservice_for_person( |
564 | + repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC) |
565 | + webservice.default_api_version = "devel" |
566 | + with person_logged_in(ANONYMOUS): |
567 | + repository_url = api_url(repository_db) |
568 | + new_project_url = api_url(new_project_db) |
569 | + response = webservice.patch( |
570 | + repository_url, "application/json", |
571 | + json.dumps({"target_link": new_project_url})) |
572 | + self.assertEqual(301, response.status) |
573 | + with person_logged_in(ANONYMOUS): |
574 | + self.assertEqual( |
575 | + webservice.getAbsoluteUrl(api_url(repository_db)), |
576 | + response.getHeader("Location")) |
577 | + self.assertEqual(new_project_db, repository_db.target) |
578 | + |
579 | + def test_set_target_other_person(self): |
580 | + # An unrelated person cannot change the target. |
581 | + project_db = self.factory.makeProduct() |
582 | + repository_db = self.factory.makeGitRepository(target=project_db) |
583 | + new_project_db = self.factory.makeProduct() |
584 | + webservice = webservice_for_person( |
585 | + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) |
586 | + webservice.default_api_version = "devel" |
587 | + with person_logged_in(ANONYMOUS): |
588 | + repository_url = api_url(repository_db) |
589 | + new_project_url = api_url(new_project_db) |
590 | + response = webservice.patch( |
591 | + repository_url, "application/json", |
592 | + json.dumps({"target_link": new_project_url})) |
593 | + self.assertEqual(401, response.status) |
594 | + with person_logged_in(ANONYMOUS): |
595 | + self.assertEqual(project_db, repository_db.target) |
596 | + |
597 | + def test_set_owner(self): |
598 | + # The repository owner can reassign the repository to a team they're |
599 | + # a member of; this redirects to the new location. |
600 | + repository_db = self.factory.makeGitRepository() |
601 | + new_owner_db = self.factory.makeTeam(members=[repository_db.owner]) |
602 | + webservice = webservice_for_person( |
603 | + repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC) |
604 | + webservice.default_api_version = "devel" |
605 | + with person_logged_in(ANONYMOUS): |
606 | + repository_url = api_url(repository_db) |
607 | + new_owner_url = api_url(new_owner_db) |
608 | + response = webservice.patch( |
609 | + repository_url, "application/json", |
610 | + json.dumps({"owner_link": new_owner_url})) |
611 | + self.assertEqual(301, response.status) |
612 | + with person_logged_in(ANONYMOUS): |
613 | + self.assertEqual( |
614 | + webservice.getAbsoluteUrl(api_url(repository_db)), |
615 | + response.getHeader("Location")) |
616 | + self.assertEqual(new_owner_db, repository_db.owner) |
617 | + |
618 | + def test_set_owner_other_person(self): |
619 | + # An unrelated person cannot change the owner. |
620 | + owner_db = self.factory.makePerson() |
621 | + repository_db = self.factory.makeGitRepository(owner=owner_db) |
622 | + new_owner_db = self.factory.makeTeam() |
623 | + webservice = webservice_for_person( |
624 | + new_owner_db.teamowner, permission=OAuthPermission.WRITE_PUBLIC) |
625 | + webservice.default_api_version = "devel" |
626 | + with person_logged_in(ANONYMOUS): |
627 | + repository_url = api_url(repository_db) |
628 | + new_owner_url = api_url(new_owner_db) |
629 | + response = webservice.patch( |
630 | + repository_url, "application/json", |
631 | + json.dumps({"owner_link": new_owner_url})) |
632 | + self.assertEqual(401, response.status) |
633 | + with person_logged_in(ANONYMOUS): |
634 | + self.assertEqual(owner_db, repository_db.owner) |
635 | |
636 | === modified file 'lib/lp/registry/interfaces/sharingservice.py' |
637 | --- lib/lp/registry/interfaces/sharingservice.py 2015-02-16 13:08:52 +0000 |
638 | +++ lib/lp/registry/interfaces/sharingservice.py 2015-03-06 16:33:15 +0000 |
639 | @@ -18,6 +18,7 @@ |
640 | operation_for_version, |
641 | operation_parameters, |
642 | operation_returns_collection_of, |
643 | + rename_parameters_as, |
644 | REQUEST_USER, |
645 | ) |
646 | from lazr.restful.fields import Reference |
647 | @@ -33,6 +34,7 @@ |
648 | from lp.blueprints.interfaces.specification import ISpecification |
649 | from lp.bugs.interfaces.bug import IBug |
650 | from lp.code.interfaces.branch import IBranch |
651 | +from lp.code.interfaces.gitrepository import IGitRepository |
652 | from lp.registry.enums import ( |
653 | BranchSharingPolicy, |
654 | BugSharingPolicy, |
655 | @@ -148,6 +150,13 @@ |
656 | :return: a collection of branches |
657 | """ |
658 | |
659 | + @export_read_operation() |
660 | + @call_with(user=REQUEST_USER) |
661 | + @operation_parameters( |
662 | + pillar=Reference(IPillar, title=_('Pillar'), required=True), |
663 | + person=Reference(IPerson, title=_('Person'), required=True)) |
664 | + @operation_returns_collection_of(IGitRepository) |
665 | + @operation_for_version('devel') |
666 | def getSharedGitRepositories(pillar, person, user): |
667 | """Return the Git repositories shared between the pillar and person. |
668 | |
669 | @@ -312,6 +321,7 @@ |
670 | |
671 | @export_write_operation() |
672 | @call_with(user=REQUEST_USER) |
673 | + @rename_parameters_as(gitrepositories='git_repositories') |
674 | @operation_parameters( |
675 | pillar=Reference(IPillar, title=_('Pillar'), required=True), |
676 | grantee=Reference(IPerson, title=_('Grantee'), required=True), |
677 | @@ -319,6 +329,9 @@ |
678 | Reference(schema=IBug), title=_('Bugs'), required=False), |
679 | branches=List( |
680 | Reference(schema=IBranch), title=_('Branches'), required=False), |
681 | + gitrepositories=List( |
682 | + Reference(schema=IGitRepository), |
683 | + title=_('Git repositories'), required=False), |
684 | specifications=List( |
685 | Reference(schema=ISpecification), title=_('Specifications'), |
686 | required=False)) |
687 | @@ -338,13 +351,17 @@ |
688 | |
689 | @export_write_operation() |
690 | @call_with(user=REQUEST_USER) |
691 | + @rename_parameters_as(gitrepositories='git_repositories') |
692 | @operation_parameters( |
693 | grantees=List( |
694 | Reference(IPerson, title=_('Grantee'), required=True)), |
695 | bugs=List( |
696 | Reference(schema=IBug), title=_('Bugs'), required=False), |
697 | branches=List( |
698 | - Reference(schema=IBranch), title=_('Branches'), required=False)) |
699 | + Reference(schema=IBranch), title=_('Branches'), required=False), |
700 | + gitrepositories=List( |
701 | + Reference(schema=IGitRepository), |
702 | + title=_('Git repositories'), required=False)) |
703 | @operation_for_version('devel') |
704 | def ensureAccessGrants(grantees, user, bugs=None, branches=None, |
705 | gitrepositories=None, specifications=None): |
706 | |
707 | === modified file 'lib/lp/registry/services/tests/test_sharingservice.py' |
708 | --- lib/lp/registry/services/tests/test_sharingservice.py 2015-03-04 18:22:06 +0000 |
709 | +++ lib/lp/registry/services/tests/test_sharingservice.py 2015-03-06 16:33:15 +0000 |
710 | @@ -1949,6 +1949,7 @@ |
711 | |
712 | def setUp(self): |
713 | super(ApiTestMixin, self).setUp() |
714 | + self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"})) |
715 | self.owner = self.factory.makePerson(name='thundercat') |
716 | self.pillar = self.factory.makeProduct( |
717 | owner=self.owner, specification_sharing_policy=( |
718 | @@ -1963,6 +1964,9 @@ |
719 | self.branch = self.factory.makeBranch( |
720 | owner=self.owner, product=self.pillar, |
721 | information_type=InformationType.PRIVATESECURITY) |
722 | + self.gitrepository = self.factory.makeGitRepository( |
723 | + owner=self.owner, target=self.pillar, |
724 | + information_type=InformationType.PRIVATESECURITY) |
725 | self.spec = self.factory.makeSpecification( |
726 | product=self.pillar, owner=self.owner, |
727 | information_type=InformationType.PROPRIETARY) |
728 | @@ -1971,6 +1975,9 @@ |
729 | self.branch.subscribe( |
730 | self.grantee, BranchSubscriptionNotificationLevel.NOEMAIL, |
731 | None, CodeReviewNotificationLevel.NOEMAIL, self.owner) |
732 | + # XXX cjwatson 2015-02-05: subscribe to Git repository when implemented |
733 | + getUtility(IService, 'sharing').ensureAccessGrants( |
734 | + [self.grantee], self.grantor, gitrepositories=[self.gitrepository]) |
735 | getUtility(IService, 'sharing').ensureAccessGrants( |
736 | [self.grantee], self.grantor, specifications=[self.spec]) |
737 | transaction.commit() |
738 | @@ -2094,6 +2101,16 @@ |
739 | self.assertEqual(1, len(branches)) |
740 | self.assertEqual(branches[0].unique_name, self.branch.unique_name) |
741 | |
742 | + def test_getSharedGitRepositories(self): |
743 | + # Test the exported getSharedGitRepositories() method. |
744 | + ws_pillar = ws_object(self.launchpad, self.pillar) |
745 | + ws_grantee = ws_object(self.launchpad, self.grantee) |
746 | + gitrepositories = self.service.getSharedGitRepositories( |
747 | + pillar=ws_pillar, person=ws_grantee) |
748 | + self.assertEqual(1, len(gitrepositories)) |
749 | + self.assertEqual( |
750 | + gitrepositories[0].unique_name, self.gitrepository.unique_name) |
751 | + |
752 | def test_getSharedSpecifications(self): |
753 | # Test the exported getSharedSpecifications() method. |
754 | ws_pillar = ws_object(self.launchpad, self.pillar) |
755 | |
756 | === modified file 'lib/lp/services/webservice/wadl-to-refhtml.xsl' |
757 | --- lib/lp/services/webservice/wadl-to-refhtml.xsl 2013-09-18 06:34:44 +0000 |
758 | +++ lib/lp/services/webservice/wadl-to-refhtml.xsl 2015-03-06 16:33:15 +0000 |
759 | @@ -168,6 +168,7 @@ |
760 | <xsl:when test=" |
761 | @id = 'bug_link_target' |
762 | or @id = 'bug_target' |
763 | + or @id = 'git_target' |
764 | or @id = 'has_bugs' |
765 | or @id = 'has_milestones' |
766 | or @id = 'object_with_translation_imports' |
767 | @@ -309,6 +310,28 @@ |
768 | <xsl:text>/+email/</xsl:text> |
769 | <var><email></var> |
770 | </xsl:when> |
771 | + <xsl:when test="@id = 'git_repository'"> |
772 | + <xsl:text>/~</xsl:text> |
773 | + <var><person.name></var> |
774 | + <xsl:text>/</xsl:text> |
775 | + <var><project.name></var> |
776 | + <xsl:text>/+git/</xsl:text> |
777 | + <var><repository.name></var> |
778 | + or |
779 | + <xsl:text>/~</xsl:text> |
780 | + <var><person.name></var> |
781 | + <xsl:text>/</xsl:text> |
782 | + <var><distribution.name></var> |
783 | + <xsl:text>/+source/</xsl:text> |
784 | + <var><source_package.name></var> |
785 | + <xsl:text>/+git/</xsl:text> |
786 | + <var><repository.name></var> |
787 | + or |
788 | + <xsl:text>/~</xsl:text> |
789 | + <var><person.name></var> |
790 | + <xsl:text>/+git/</xsl:text> |
791 | + <var><repository.name></var> |
792 | + </xsl:when> |
793 | <xsl:when test="@id = 'gpg_key'"> |
794 | <xsl:text>/</xsl:text> |
795 | <var><person.name></var> |