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

Proposed by Colin Watson on 2015-03-05
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
Reviewer Review Type Date Requested Status
William Grant code 2015-03-05 Approve on 2015-03-06
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 IGitRepositorySet.new, because currently we want you to do that by pushing a repository instead. We'll need to work on the workflows there, but we can easily export that method later.

To post a comment you must log in.
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>&lt;email&gt;</var>
770 </xsl:when>
771+ <xsl:when test="@id = 'git_repository'">
772+ <xsl:text>/~</xsl:text>
773+ <var>&lt;person.name&gt;</var>
774+ <xsl:text>/</xsl:text>
775+ <var>&lt;project.name&gt;</var>
776+ <xsl:text>/+git/</xsl:text>
777+ <var>&lt;repository.name&gt;</var>
778+ or
779+ <xsl:text>/~</xsl:text>
780+ <var>&lt;person.name&gt;</var>
781+ <xsl:text>/</xsl:text>
782+ <var>&lt;distribution.name&gt;</var>
783+ <xsl:text>/+source/</xsl:text>
784+ <var>&lt;source_package.name&gt;</var>
785+ <xsl:text>/+git/</xsl:text>
786+ <var>&lt;repository.name&gt;</var>
787+ or
788+ <xsl:text>/~</xsl:text>
789+ <var>&lt;person.name&gt;</var>
790+ <xsl:text>/+git/</xsl:text>
791+ <var>&lt;repository.name&gt;</var>
792+ </xsl:when>
793 <xsl:when test="@id = 'gpg_key'">
794 <xsl:text>/</xsl:text>
795 <var>&lt;person.name&gt;</var>