← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-webservice into lp:launchpad.

Commit message:
Export Git-related methods on the webservice.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1032731 in Launchpad itself: "Support for Launchpad-hosted Git repositories"
  https://bugs.launchpad.net/launchpad/+bug/1032731

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-webservice/+merge/251978

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.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-webservice into lp:launchpad.
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py	2014-11-27 11:01:16 +0000
+++ lib/lp/app/browser/launchpad.py	2015-03-05 16:39:54 +0000
@@ -102,6 +102,7 @@
 from lp.code.interfaces.branchlookup import IBranchLookup
 from lp.code.interfaces.codehosting import IBazaarApplication
 from lp.code.interfaces.codeimport import ICodeImportSet
+from lp.code.interfaces.gitrepository import IGitRepositorySet
 from lp.hardwaredb.interfaces.hwdb import IHWDBApplication
 from lp.layers import WebServiceLayer
 from lp.registry.interfaces.announcement import IAnnouncementSet
@@ -783,6 +784,7 @@
         'codeofconduct': ICodeOfConductSet,
         '+countries': ICountrySet,
         'distros': IDistributionSet,
+        '+git': IGitRepositorySet,
         '+hwdb': IHWDBApplication,
         'karmaaction': IKarmaActionSet,
         '+imports': ITranslationImportQueue,

=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml	2015-03-04 16:56:48 +0000
+++ lib/lp/code/browser/configure.zcml	2015-03-05 16:39:54 +0000
@@ -13,6 +13,11 @@
        path_expression="string:branches"
        parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"
       />
+    <browser:url
+       for="lp.code.interfaces.gitrepository.IGitRepositorySet"
+       path_expression="string:+git"
+       parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"
+      />
     <browser:feeds
         module="lp.code.feed.branch"
         classes="BranchFeed PersonBranchFeed ProductBranchFeed ProjectBranchFeed

=== modified file 'lib/lp/code/interfaces/gitrepository.py'
--- lib/lp/code/interfaces/gitrepository.py	2015-03-05 14:13:16 +0000
+++ lib/lp/code/interfaces/gitrepository.py	2015-03-05 16:39:54 +0000
@@ -17,7 +17,23 @@
 
 import re
 
+from lazr.restful.declarations import (
+    call_with,
+    collection_default_content,
+    export_as_webservice_collection,
+    export_as_webservice_entry,
+    export_destructor_operation,
+    export_read_operation,
+    export_write_operation,
+    exported,
+    operation_for_version,
+    operation_parameters,
+    operation_returns_collection_of,
+    operation_returns_entry,
+    REQUEST_USER,
+    )
 from lazr.restful.fields import Reference
+from lazr.restful.interface import copy_field
 from zope.component import getUtility
 from zope.interface import (
     Attribute,
@@ -40,6 +56,7 @@
 from lp.registry.interfaces.distributionsourcepackage import (
     IDistributionSourcePackage,
     )
+from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.persondistributionsourcepackage import (
     IPersonDistributionSourcePackageFactory,
     )
@@ -97,68 +114,70 @@
 
     id = Int(title=_("ID"), readonly=True, required=True)
 
-    date_created = Datetime(
-        title=_("Date created"), required=True, readonly=True)
-
-    date_last_modified = Datetime(
-        title=_("Date last modified"), required=True, readonly=True)
-
-    registrant = PublicPersonChoice(
+    date_created = exported(Datetime(
+        title=_("Date created"), required=True, readonly=True))
+
+    date_last_modified = exported(Datetime(
+        title=_("Date last modified"), required=True, readonly=True))
+
+    registrant = exported(PublicPersonChoice(
         title=_("Registrant"), required=True, readonly=True,
         vocabulary="ValidPersonOrTeam",
-        description=_("The person who registered this Git repository."))
+        description=_("The person who registered this Git repository.")))
 
-    owner = PersonChoice(
+    owner = exported(PersonChoice(
         title=_("Owner"), required=True, readonly=False,
         vocabulary="AllUserTeamsParticipationPlusSelf",
         description=_(
             "The owner of this Git repository. This controls who can modify "
-            "the repository."))
+            "the repository.")))
 
-    target = Reference(
-        title=_("Target"), required=True, readonly=True,
-        schema=IHasGitRepositories,
-        description=_("The target of the repository."))
+    target = exported(
+        Reference(
+            title=_("Target"), required=True, readonly=True,
+            schema=IHasGitRepositories,
+            description=_("The target of the repository.")),
+        as_of="devel")
 
     namespace = Attribute(
         "The namespace of this repository, as an `IGitNamespace`.")
 
-    information_type = Choice(
+    information_type = exported(Choice(
         title=_("Information type"), vocabulary=InformationType,
         required=True, readonly=True, default=InformationType.PUBLIC,
         description=_(
-            "The type of information contained in this repository."))
+            "The type of information contained in this repository.")))
 
-    owner_default = Bool(
+    owner_default = exported(Bool(
         title=_("Owner default"), required=True, readonly=True,
         description=_(
             "Whether this repository is the default for its owner and "
-            "target."))
+            "target.")))
 
-    target_default = Bool(
+    target_default = exported(Bool(
         title=_("Target default"), required=True, readonly=True,
         description=_(
-            "Whether this repository is the default for its target."))
+            "Whether this repository is the default for its target.")))
 
-    unique_name = Text(
+    unique_name = exported(Text(
         title=_("Unique name"), readonly=True,
         description=_(
             "Unique name of the repository, including the owner and project "
-            "names."))
+            "names.")))
 
-    display_name = Text(
+    display_name = exported(Text(
         title=_("Display name"), readonly=True,
-        description=_("Display name of the repository."))
+        description=_("Display name of the repository.")))
 
     shortened_path = Attribute(
         "The shortest reasonable version of the path to this repository.")
 
-    git_identity = Text(
+    git_identity = exported(Text(
         title=_("Git identity"), readonly=True,
         description=_(
             "If this is the default repository for some target, then this is "
             "'lp:' plus a shortcut version of the path via that target.  "
-            "Otherwise it is simply 'lp:' plus the unique name."))
+            "Otherwise it is simply 'lp:' plus the unique name.")))
 
     def setOwnerDefault(value):
         """Set whether this repository is the default for its owner-target.
@@ -244,17 +263,23 @@
 
     # XXX cjwatson 2015-01-29: Add some advice about default repository
     # naming.
-    name = TextLine(
+    name = exported(TextLine(
         title=_("Name"), required=True,
         constraint=git_repository_name_validator,
         description=_(
             "The repository name. Keep very short, unique, and descriptive, "
-            "because it will be used in URLs."))
+            "because it will be used in URLs.")))
 
 
 class IGitRepositoryModerate(Interface):
     """IGitRepository methods that can be called by more than one community."""
 
+    @operation_parameters(
+        information_type=copy_field(IGitRepositoryView["information_type"]),
+        )
+    @call_with(user=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version("devel")
     def transitionToInformationType(information_type, user,
                                     verify_policy=True):
         """Set the information type for this repository.
@@ -269,12 +294,29 @@
 class IGitRepositoryEdit(Interface):
     """IGitRepository methods that require launchpad.Edit permission."""
 
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(
+        new_owner=Reference(
+            title=_("The new owner of the repository."), schema=IPerson))
+    @export_write_operation()
+    @operation_for_version("devel")
     def setOwner(new_owner, user):
         """Set the owner of the repository to be `new_owner`."""
 
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(
+        target=Reference(
+            title=_(
+                "The project, distribution source package, or person the "
+                "repository belongs to."),
+            schema=IHasGitRepositories, required=True))
+    @export_write_operation()
+    @operation_for_version("devel")
     def setTarget(target, user):
         """Set the target of the repository."""
 
+    @export_destructor_operation()
+    @operation_for_version("devel")
     def destroySelf():
         """Delete the specified repository."""
 
@@ -283,14 +325,22 @@
                      IGitRepositoryModerate, IGitRepositoryEdit):
     """A Git repository."""
 
-    private = Bool(
+    # Mark repositories as exported entries for the Launchpad API.
+    # XXX cjwatson 2015-01-19 bug=760849: "beta" is a lie to get WADL
+    # generation working.  Individual attributes must set their version to
+    # "devel".
+    export_as_webservice_entry(plural_name="git_repositories", as_of="beta")
+
+    private = exported(Bool(
         title=_("Private"), required=False, readonly=True,
-        description=_("This repository is visible only to its subscribers."))
+        description=_("This repository is visible only to its subscribers.")))
 
 
 class IGitRepositorySet(Interface):
     """Interface representing the set of Git repositories."""
 
+    export_as_webservice_collection(IGitRepository)
+
     def new(registrant, owner, target, name, information_type=None,
             date_created=None):
         """Create a Git repository and return it.
@@ -306,6 +356,12 @@
         """
 
     # Marker for references to Git URL layouts: ##GITNAMESPACE##
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(
+        path=TextLine(title=_("Repository path"), required=True))
+    @operation_returns_entry(IGitRepository)
+    @export_read_operation()
+    @operation_for_version("devel")
     def getByPath(user, path):
         """Find a repository by its path.
 
@@ -324,6 +380,13 @@
         Return None if no match was found.
         """
 
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(
+        target=Reference(
+            title=_("Target"), required=True, schema=IHasGitRepositories))
+    @operation_returns_collection_of(IGitRepository)
+    @export_read_operation()
+    @operation_for_version("devel")
     def getRepositories(user, target):
         """Get all repositories for a target.
 
@@ -334,6 +397,12 @@
         :return: A collection of `IGitRepository` objects.
         """
 
+    @operation_parameters(
+        target=Reference(
+            title=_("Target"), required=True, schema=IHasGitRepositories))
+    @operation_returns_entry(IGitRepository)
+    @export_read_operation()
+    @operation_for_version("devel")
     def getDefaultRepository(target):
         """Get the default repository for a target.
 
@@ -343,6 +412,13 @@
         :return: An `IGitRepository`, or None.
         """
 
+    @operation_parameters(
+        owner=Reference(title=_("Owner"), required=True, schema=IPerson),
+        target=Reference(
+            title=_("Target"), required=True, schema=IHasGitRepositories))
+    @operation_returns_entry(IGitRepository)
+    @export_read_operation()
+    @operation_for_version("devel")
     def getDefaultRepositoryForOwner(owner, target):
         """Get a person's default repository for a target.
 
@@ -353,6 +429,13 @@
         :return: An `IGitRepository`, or None.
         """
 
+    @operation_parameters(
+        target=Reference(
+            title=_("Target"), required=True, schema=IHasGitRepositories),
+        repository=Reference(
+            title=_("Git repository"), required=False, schema=IGitRepository))
+    @export_write_operation()
+    @operation_for_version("devel")
     def setDefaultRepository(target, repository):
         """Set the default repository for a target.
 
@@ -363,6 +446,14 @@
         :raises GitTargetError: if `target` is an `IPerson`.
         """
 
+    @operation_parameters(
+        owner=Reference(title=_("Owner"), required=True, schema=IPerson),
+        target=Reference(
+            title=_("Target"), required=True, schema=IHasGitRepositories),
+        repository=Reference(
+            title=_("Git repository"), required=False, schema=IGitRepository))
+    @export_write_operation()
+    @operation_for_version("devel")
     def setDefaultRepositoryForOwner(owner, target, repository):
         """Set a person's default repository for a target.
 
@@ -374,6 +465,7 @@
         :raises GitTargetError: if `target` is an `IPerson`.
         """
 
+    @collection_default_content()
     def empty_list():
         """Return an empty collection of repositories.
 

=== modified file 'lib/lp/code/interfaces/hasgitrepositories.py'
--- lib/lp/code/interfaces/hasgitrepositories.py	2015-03-04 19:05:47 +0000
+++ lib/lp/code/interfaces/hasgitrepositories.py	2015-03-05 16:39:54 +0000
@@ -9,6 +9,7 @@
     'IHasGitRepositories',
     ]
 
+from lazr.restful.declarations import export_as_webservice_entry
 from zope.interface import Interface
 
 
@@ -18,3 +19,6 @@
     A project contains Git repositories, a source package on a distribution
     contains branches, and a person contains "personal" branches.
     """
+
+    export_as_webservice_entry(
+        singular_name="git_target", plural_name="git_targets", as_of="devel")

=== modified file 'lib/lp/code/interfaces/webservice.py'
--- lib/lp/code/interfaces/webservice.py	2015-01-30 18:24:07 +0000
+++ lib/lp/code/interfaces/webservice.py	2015-03-05 16:39:54 +0000
@@ -25,6 +25,9 @@
     'ICodeReviewComment',
     'ICodeReviewVoteReference',
     'IDiff',
+    'IGitRepository',
+    'IGitRepositorySet',
+    'IHasGitRepositories',
     'IPreviewDiff',
     'ISourcePackageRecipe',
     'ISourcePackageRecipeBuild',
@@ -57,6 +60,11 @@
     IDiff,
     IPreviewDiff,
     )
+from lp.code.interfaces.gitrepository import (
+    IGitRepository,
+    IGitRepositorySet,
+    )
+from lp.code.interfaces.hasgitrepositories import IHasGitRepositories
 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipe
 from lp.code.interfaces.sourcepackagerecipebuild import (
     ISourcePackageRecipeBuild,

=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py	2015-03-05 14:13:16 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py	2015-03-05 16:39:54 +0000
@@ -54,8 +54,11 @@
 from lp.services.database.constants import UTC_NOW
 from lp.services.features.testing import FeatureFixture
 from lp.services.webapp.authorization import check_permission
+from lp.services.webapp.interfaces import OAuthPermission
 from lp.testing import (
     admin_logged_in,
+    ANONYMOUS,
+    api_url,
     celebrity_logged_in,
     person_logged_in,
     TestCaseWithFactory,
@@ -65,6 +68,7 @@
     DatabaseFunctionalLayer,
     ZopelessDatabaseLayer,
     )
+from lp.testing.pages import webservice_for_person
 
 
 class TestGitRepositoryFeatureFlag(TestCaseWithFactory):
@@ -858,3 +862,64 @@
     TestGitRepositorySetDefaultsOwnerMixin,
     TestGitRepositorySetDefaultsPackage):
     pass
+
+
+class TestWebservice(TestCaseWithFactory):
+    """Tests for the webservice."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestWebservice, self).setUp()
+        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
+
+    def test_getRepositories_project(self):
+        project_db = self.factory.makeProduct()
+        repository_db = self.factory.makeGitRepository(target=project_db)
+        webservice = webservice_for_person(
+            repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC)
+        webservice.default_api_version = "devel"
+        with person_logged_in(ANONYMOUS):
+            repository_url = api_url(repository_db)
+            owner_url = api_url(repository_db.owner)
+            project_url = api_url(project_db)
+        response = webservice.named_get(
+            "/+git", "getRepositories", user=owner_url, target=project_url)
+        self.assertEqual(200, response.status)
+        self.assertEqual(
+            [webservice.getAbsoluteUrl(repository_url)],
+            [entry["self_link"] for entry in response.jsonBody()["entries"]])
+
+    def test_getRepositories_package(self):
+        dsp_db = self.factory.makeDistributionSourcePackage()
+        repository_db = self.factory.makeGitRepository(target=dsp_db)
+        webservice = webservice_for_person(
+            repository_db.owner, permission=OAuthPermission.WRITE_PUBLIC)
+        webservice.default_api_version = "devel"
+        with person_logged_in(ANONYMOUS):
+            repository_url = api_url(repository_db)
+            owner_url = api_url(repository_db.owner)
+            dsp_url = api_url(dsp_db)
+        response = webservice.named_get(
+            "/+git", "getRepositories", user=owner_url, target=dsp_url)
+        self.assertEqual(200, response.status)
+        self.assertEqual(
+            [webservice.getAbsoluteUrl(repository_url)],
+            [entry["self_link"] for entry in response.jsonBody()["entries"]])
+
+    def test_getRepositories_personal(self):
+        owner_db = self.factory.makePerson()
+        repository_db = self.factory.makeGitRepository(
+            owner=owner_db, target=owner_db)
+        webservice = webservice_for_person(
+            owner_db, permission=OAuthPermission.WRITE_PUBLIC)
+        webservice.default_api_version = "devel"
+        with person_logged_in(ANONYMOUS):
+            repository_url = api_url(repository_db)
+            owner_url = api_url(owner_db)
+        response = webservice.named_get(
+            "/+git", "getRepositories", user=owner_url, target=owner_url)
+        self.assertEqual(200, response.status)
+        self.assertEqual(
+            [webservice.getAbsoluteUrl(repository_url)],
+            [entry["self_link"] for entry in response.jsonBody()["entries"]])

=== modified file 'lib/lp/registry/interfaces/sharingservice.py'
--- lib/lp/registry/interfaces/sharingservice.py	2015-02-16 13:08:52 +0000
+++ lib/lp/registry/interfaces/sharingservice.py	2015-03-05 16:39:54 +0000
@@ -18,6 +18,7 @@
     operation_for_version,
     operation_parameters,
     operation_returns_collection_of,
+    rename_parameters_as,
     REQUEST_USER,
     )
 from lazr.restful.fields import Reference
@@ -33,6 +34,7 @@
 from lp.blueprints.interfaces.specification import ISpecification
 from lp.bugs.interfaces.bug import IBug
 from lp.code.interfaces.branch import IBranch
+from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.enums import (
     BranchSharingPolicy,
     BugSharingPolicy,
@@ -148,6 +150,13 @@
         :return: a collection of branches
         """
 
+    @export_read_operation()
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(
+        pillar=Reference(IPillar, title=_('Pillar'), required=True),
+        person=Reference(IPerson, title=_('Person'), required=True))
+    @operation_returns_collection_of(IGitRepository)
+    @operation_for_version('devel')
     def getSharedGitRepositories(pillar, person, user):
         """Return the Git repositories shared between the pillar and person.
 
@@ -312,6 +321,7 @@
 
     @export_write_operation()
     @call_with(user=REQUEST_USER)
+    @rename_parameters_as(gitrepositories='git_repositories')
     @operation_parameters(
         pillar=Reference(IPillar, title=_('Pillar'), required=True),
         grantee=Reference(IPerson, title=_('Grantee'), required=True),
@@ -319,6 +329,9 @@
             Reference(schema=IBug), title=_('Bugs'), required=False),
         branches=List(
             Reference(schema=IBranch), title=_('Branches'), required=False),
+        gitrepositories=List(
+            Reference(schema=IGitRepository),
+            title=_('Git repositories'), required=False),
         specifications=List(
             Reference(schema=ISpecification), title=_('Specifications'),
             required=False))
@@ -338,13 +351,17 @@
 
     @export_write_operation()
     @call_with(user=REQUEST_USER)
+    @rename_parameters_as(gitrepositories='git_repositories')
     @operation_parameters(
         grantees=List(
             Reference(IPerson, title=_('Grantee'), required=True)),
         bugs=List(
             Reference(schema=IBug), title=_('Bugs'), required=False),
         branches=List(
-            Reference(schema=IBranch), title=_('Branches'), required=False))
+            Reference(schema=IBranch), title=_('Branches'), required=False),
+        gitrepositories=List(
+            Reference(schema=IGitRepository),
+            title=_('Git repositories'), required=False))
     @operation_for_version('devel')
     def ensureAccessGrants(grantees, user, bugs=None, branches=None,
                            gitrepositories=None, specifications=None):

=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
--- lib/lp/registry/services/tests/test_sharingservice.py	2015-03-04 18:22:06 +0000
+++ lib/lp/registry/services/tests/test_sharingservice.py	2015-03-05 16:39:54 +0000
@@ -1949,6 +1949,7 @@
 
     def setUp(self):
         super(ApiTestMixin, self).setUp()
+        self.useFixture(FeatureFixture({GIT_FEATURE_FLAG: u"on"}))
         self.owner = self.factory.makePerson(name='thundercat')
         self.pillar = self.factory.makeProduct(
             owner=self.owner, specification_sharing_policy=(
@@ -1963,6 +1964,9 @@
         self.branch = self.factory.makeBranch(
             owner=self.owner, product=self.pillar,
             information_type=InformationType.PRIVATESECURITY)
+        self.gitrepository = self.factory.makeGitRepository(
+            owner=self.owner, target=self.pillar,
+            information_type=InformationType.PRIVATESECURITY)
         self.spec = self.factory.makeSpecification(
             product=self.pillar, owner=self.owner,
             information_type=InformationType.PROPRIETARY)
@@ -1971,6 +1975,9 @@
         self.branch.subscribe(
             self.grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
             None, CodeReviewNotificationLevel.NOEMAIL, self.owner)
+        # XXX cjwatson 2015-02-05: subscribe to Git repository when implemented
+        getUtility(IService, 'sharing').ensureAccessGrants(
+            [self.grantee], self.grantor, gitrepositories=[self.gitrepository])
         getUtility(IService, 'sharing').ensureAccessGrants(
             [self.grantee], self.grantor, specifications=[self.spec])
         transaction.commit()
@@ -2094,6 +2101,16 @@
         self.assertEqual(1, len(branches))
         self.assertEqual(branches[0].unique_name, self.branch.unique_name)
 
+    def test_getSharedGitRepositories(self):
+        # Test the exported getSharedGitRepositories() method.
+        ws_pillar = ws_object(self.launchpad, self.pillar)
+        ws_grantee = ws_object(self.launchpad, self.grantee)
+        gitrepositories = self.service.getSharedGitRepositories(
+            pillar=ws_pillar, person=ws_grantee)
+        self.assertEqual(1, len(gitrepositories))
+        self.assertEqual(
+            gitrepositories[0].unique_name, self.gitrepository.unique_name)
+
     def test_getSharedSpecifications(self):
         # Test the exported getSharedSpecifications() method.
         ws_pillar = ws_object(self.launchpad, self.pillar)

=== modified file 'lib/lp/services/webservice/wadl-to-refhtml.xsl'
--- lib/lp/services/webservice/wadl-to-refhtml.xsl	2013-09-18 06:34:44 +0000
+++ lib/lp/services/webservice/wadl-to-refhtml.xsl	2015-03-05 16:39:54 +0000
@@ -168,6 +168,7 @@
                     <xsl:when test="
                         @id = 'bug_link_target'
                         or @id = 'bug_target'
+                        or @id = 'git_target'
                         or @id = 'has_bugs'
                         or @id = 'has_milestones'
                         or @id = 'object_with_translation_imports'
@@ -309,6 +310,28 @@
                 <xsl:text>/+email/</xsl:text>
                 <var>&lt;email&gt;</var>
             </xsl:when>
+            <xsl:when test="@id = 'git_repository'">
+                <xsl:text>/~</xsl:text>
+                <var>&lt;person.name&gt;</var>
+                <xsl:text>/</xsl:text>
+                <var>&lt;project.name&gt;</var>
+                <xsl:text>/+git/</xsl:text>
+                <var>&lt;repository.name&gt;</var>
+                or
+                <xsl:text>/~</xsl:text>
+                <var>&lt;person.name&gt;</var>
+                <xsl:text>/</xsl:text>
+                <var>&lt;distribution.name&gt;</var>
+                <xsl:text>/+source/</xsl:text>
+                <var>&lt;source_package.name&gt;</var>
+                <xsl:text>/+git/</xsl:text>
+                <var>&lt;repository.name&gt;</var>
+                or
+                <xsl:text>/~</xsl:text>
+                <var>&lt;person.name&gt;</var>
+                <xsl:text>/+git/</xsl:text>
+                <var>&lt;repository.name&gt;</var>
+            </xsl:when>
             <xsl:when test="@id = 'gpg_key'">
                 <xsl:text>/</xsl:text>
                 <var>&lt;person.name&gt;</var>


Follow ups