← Back to team overview

launchpad-reviewers team mailing list archive

Re: [Merge] lp:~cjwatson/launchpad/git-basic-browser into lp:launchpad

 

Review: Approve code



Diff comments:

> === modified file 'lib/lp/code/browser/configure.zcml'
> --- lib/lp/code/browser/configure.zcml	2014-12-06 10:52:39 +0000
> +++ lib/lp/code/browser/configure.zcml	2015-03-04 16:58:30 +0000
> @@ -1,4 +1,4 @@
> -<!-- Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
> +<!-- Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>       GNU Affero General Public License version 3 (see the file LICENSE).
>  -->
>  
> @@ -760,6 +760,36 @@
>          name="+count-summary"
>          template="../templates/branch-count-summary.pt"/>
>  
> +    <browser:defaultView
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        name="+index"/>
> +    <browser:url
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        urldata="lp.code.browser.gitrepository.GitRepositoryURL"/>
> +    <browser:menus
> +        module="lp.code.browser.gitrepository"
> +        classes="
> +            GitRepositoryContextMenu"/>
> +    <browser:pages
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        class="lp.code.browser.gitrepository.GitRepositoryView"
> +        permission="launchpad.View">
> +        <browser:page
> +            name="+index"
> +            template="../templates/gitrepository-index.pt"/>
> +        <browser:page
> +            name="++repository-information"
> +            template="../templates/gitrepository-information.pt"/>
> +        <browser:page
> +            name="++repository-management"
> +            template="../templates/gitrepository-management.pt"/>
> +    </browser:pages>
> +    <adapter
> +        provides="lp.services.webapp.interfaces.IBreadcrumb"
> +        for="lp.code.interfaces.gitrepository.IGitRepository"
> +        factory="lp.code.browser.gitrepository.GitRepositoryBreadcrumb"
> +        permission="zope.Public"/>
> +
>      <browser:menus
>          classes="ProductBranchesMenu"
>          module="lp.code.browser.branchlisting"/>
> 
> === added file 'lib/lp/code/browser/gitrepository.py'
> --- lib/lp/code/browser/gitrepository.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/browser/gitrepository.py	2015-03-04 16:58:30 +0000
> @@ -0,0 +1,108 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Git repository views."""
> +
> +__metaclass__ = type
> +
> +__all__ = [
> +    'GitRepositoryBreadcrumb',
> +    'GitRepositoryContextMenu',
> +    'GitRepositoryURL',
> +    'GitRepositoryView',
> +    ]
> +
> +from bzrlib import urlutils
> +from zope.interface import implements
> +
> +from lp.app.browser.informationtype import InformationTypePortletMixin
> +from lp.code.interfaces.gitrepository import IGitRepository
> +from lp.services.config import config
> +from lp.services.webapp import (
> +    ContextMenu,
> +    LaunchpadView,
> +    Link,
> +    )
> +from lp.services.webapp.authorization import (
> +    check_permission,
> +    precache_permission_for_objects,
> +    )
> +from lp.services.webapp.breadcrumb import NameBreadcrumb
> +from lp.services.webapp.interfaces import ICanonicalUrlData
> +
> +
> +class GitRepositoryURL:
> +    """Git repository URL creation rules."""
> +
> +    implements(ICanonicalUrlData)
> +
> +    rootsite = "code"
> +    inside = None
> +
> +    def __init__(self, repository):
> +        self.repository = repository
> +
> +    @property
> +    def path(self):
> +        return self.repository.unique_name
> +
> +
> +class GitRepositoryBreadcrumb(NameBreadcrumb):
> +
> +    @property
> +    def inside(self):
> +        return self.context.unique_name.split("/")[-1]
> +
> +
> +class GitRepositoryContextMenu(ContextMenu):
> +    """Context menu for `IGitRepository`."""
> +
> +    usedfor = IGitRepository
> +    facet = "branches"
> +    links = ["source"]
> +
> +    def source(self):
> +        """Return a link to the branch's browsing interface."""
> +        text = "Browse the code"
> +        url = self.context.getCodebrowseUrl()
> +        return Link(url, text, icon="info")
> +
> +
> +class GitRepositoryView(InformationTypePortletMixin, LaunchpadView):
> +
> +    @property
> +    def page_title(self):
> +        return self.context.display_name
> +
> +    label = page_title
> +
> +    def initialize(self):
> +        super(GitRepositoryView, self).initialize()
> +        # Cache permission so that the private team owner can be rendered.  The
> +        # security adapter will do the job also but we don't want or need the
> +        # expense of running several complex SQL queries.
> +        authorised_people = [self.context.owner]
> +        if self.user is not None:
> +            precache_permission_for_objects(
> +                self.request, "launchpad.LimitedView", authorised_people)
> +
> +    @property
> +    def anon_url(self):
> +        if self.context.visibleByUser(None):
> +            return urlutils.join(
> +                config.codehosting.git_anon_root, self.context.shortened_path)

In future this should probably be HTTPS, but that's annoying for testing.

> +        else:
> +            return None
> +
> +    @property
> +    def ssh_url(self):
> +        if self.user is not None:
> +            return urlutils.join(
> +                config.codehosting.git_ssh_root, self.context.shortened_path)
> +        else:
> +            return None
> +
> +    @property
> +    def user_can_push(self):
> +        """Whether the user can push to this branch."""
> +        return check_permission("launchpad.Edit", self.context)
> 
> === added file 'lib/lp/code/browser/tests/test_gitrepository.py'
> --- lib/lp/code/browser/tests/test_gitrepository.py	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/browser/tests/test_gitrepository.py	2015-03-04 16:58:30 +0000
> @@ -0,0 +1,169 @@
> +# Copyright 2015 Canonical Ltd.  This software is licensed under the
> +# GNU Affero General Public License version 3 (see the file LICENSE).
> +
> +"""Unit tests for GitRepositoryView."""
> +
> +__metaclass__ = type
> +
> +from BeautifulSoup import BeautifulSoup
> +from bzrlib import urlutils
> +from fixtures import FakeLogger
> +from zope.component import getUtility
> +from zope.publisher.interfaces import NotFound
> +
> +from lp.app.enums import InformationType
> +from lp.app.interfaces.services import IService
> +from lp.registry.interfaces.person import PersonVisibility
> +from lp.services.config import config
> +from lp.services.webapp.publisher import canonical_url
> +from lp.testing import (
> +    admin_logged_in,
> +    BrowserTestCase,
> +    login_person,
> +    logout,
> +    person_logged_in,
> +    )
> +from lp.testing.layers import DatabaseFunctionalLayer
> +from lp.testing.pages import (
> +    setupBrowser,
> +    setupBrowserForUser,
> +    )
> +from lp.testing.views import create_initialized_view
> +
> +
> +class TestGitRepositoryView(BrowserTestCase):
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def test_anon_url_for_public(self):
> +        # Public repositories have an anonymous URL, visible to anyone.
> +        repository = self.factory.makeGitRepository()
> +        view = create_initialized_view(repository, "+index")
> +        expected_url = urlutils.join(
> +            config.codehosting.git_anon_root, repository.shortened_path)
> +        self.assertEqual(expected_url, view.anon_url)
> +
> +    def test_anon_url_not_for_private(self):
> +        # Private repositories do not have an anonymous URL.
> +        owner = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(
> +            owner=owner, information_type=InformationType.USERDATA)
> +        with person_logged_in(owner):
> +            view = create_initialized_view(repository, "+index")
> +            self.assertIsNone(view.anon_url)
> +
> +    def test_ssh_url_for_public_logged_in(self):
> +        # Public repositories have an SSH URL, visible if logged in.
> +        repository = self.factory.makeGitRepository()
> +        with person_logged_in(repository.owner):
> +            view = create_initialized_view(repository, "+index")
> +            expected_url = urlutils.join(
> +                config.codehosting.git_ssh_root, repository.shortened_path)
> +            self.assertEqual(expected_url, view.ssh_url)
> +
> +    def test_ssh_url_for_public_not_anonymous(self):
> +        # Public repositories do not have an SSH URL if not logged in.
> +        repository = self.factory.makeGitRepository()
> +        view = create_initialized_view(repository, "+index")
> +        self.assertIsNone(view.ssh_url)
> +
> +    def test_ssh_url_for_private(self):
> +        # Private repositories have an SSH URL.
> +        owner = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(
> +            owner=owner, information_type=InformationType.USERDATA)
> +        with person_logged_in(owner):
> +            view = create_initialized_view(repository, "+index")
> +            expected_url = urlutils.join(
> +                config.codehosting.git_ssh_root, repository.shortened_path)
> +            self.assertEqual(expected_url, view.ssh_url)
> +
> +    def test_user_can_push(self):
> +        # A user can push if they have edit permissions.
> +        repository = self.factory.makeGitRepository()
> +        with person_logged_in(repository.owner):
> +            view = create_initialized_view(repository, "+index")
> +            self.assertTrue(view.user_can_push)
> +
> +    def test_user_can_push_admins_can(self):
> +        # Admins can push to any repository.
> +        repository = self.factory.makeGitRepository()
> +        with admin_logged_in():
> +            view = create_initialized_view(repository, "+index")
> +            self.assertTrue(view.user_can_push)
> +
> +    def test_user_can_push_non_owner(self):
> +        # Someone not associated with the repository cannot upload.
> +        repository = self.factory.makeGitRepository()
> +        with person_logged_in(self.factory.makePerson()):
> +            view = create_initialized_view(repository, "+index")
> +            self.assertFalse(view.user_can_push)
> +
> +    def test_view_for_user_with_artifact_grant(self):
> +        # Users with an artifact grant for a repository related to a private
> +        # project can view the main repository page.
> +        owner = self.factory.makePerson()
> +        user = self.factory.makePerson()
> +        project = self.factory.makeProduct(
> +            owner=owner, information_type=InformationType.PROPRIETARY)
> +        with person_logged_in(owner):
> +            project_name = project.name
> +            repository = self.factory.makeGitRepository(
> +                owner=owner, target=project,
> +                information_type=InformationType.PROPRIETARY)
> +            getUtility(IService, "sharing").ensureAccessGrants(
> +                [user], owner, gitrepositories=[repository])
> +        with person_logged_in(user):
> +            url = canonical_url(repository)
> +        # The main check: No Unauthorized error should be raised.
> +        browser = self.getUserBrowser(url, user=user)
> +        self.assertIn(project_name, browser.contents)
> +
> +
> +class TestGitRepositoryViewPrivateArtifacts(BrowserTestCase):
> +    """ Tests that Git repositories with private team artifacts can be viewed.
> +
> +    A repository may be associated with a private team as follows:
> +    - the owner is a private team
> +
> +    A logged in user who is not authorised to see the private team(s) still
> +    needs to be able to view the repository.  The private team will be
> +    rendered in the normal way, displaying the team name and Launchpad URL.
> +    """
> +
> +    layer = DatabaseFunctionalLayer
> +
> +    def _getBrowser(self, user=None):
> +        if user is None:
> +            browser = setupBrowser()
> +            logout()
> +            return browser
> +        else:
> +            login_person(user)
> +            return setupBrowserForUser(user=user)
> +
> +    def test_view_repository_with_private_owner(self):
> +        # A repository with a private owner is rendered.
> +        private_owner = self.factory.makeTeam(
> +            displayname="PrivateTeam", visibility=PersonVisibility.PRIVATE)
> +        with person_logged_in(private_owner):
> +            repository = self.factory.makeGitRepository(owner=private_owner)
> +        # Ensure the repository owner is rendered.
> +        url = canonical_url(repository, rootsite="code")
> +        user = self.factory.makePerson()
> +        browser = self._getBrowser(user)
> +        browser.open(url)
> +        soup = BeautifulSoup(browser.contents)
> +        self.assertIsNotNone(soup.find('a', text="PrivateTeam"))
> +
> +    def test_anonymous_view_repository_with_private_owner(self):
> +        # A repository with a private owner is not rendered for anon users.
> +        self.useFixture(FakeLogger())
> +        private_owner = self.factory.makeTeam(
> +            visibility=PersonVisibility.PRIVATE)
> +        with person_logged_in(private_owner):
> +            repository = self.factory.makeGitRepository(owner=private_owner)
> +        # Viewing the branch results in an error.
> +        url = canonical_url(repository, rootsite="code")
> +        browser = self._getBrowser()
> +        self.assertRaises(NotFound, browser.open, url)
> 
> === modified file 'lib/lp/code/model/tests/test_gitcollection.py'
> --- lib/lp/code/model/tests/test_gitcollection.py	2015-02-26 14:46:35 +0000
> +++ lib/lp/code/model/tests/test_gitcollection.py	2015-03-04 16:58:30 +0000
> @@ -27,6 +27,7 @@
>      )
>  from lp.registry.model.personproduct import PersonProduct
>  from lp.services.database.interfaces import IStore
> +from lp.services.webapp.publisher import canonical_url
>  from lp.testing import (
>      person_logged_in,
>      StormStatementRecorder,
> @@ -423,8 +424,8 @@
>              [self.public_repository], list(repositories.getRepositories()))
>  
>      def test_owner_sees_own_repositories(self):
> -        # Users can always see the repositories that they own, as well as public
> -        # repositories.
> +        # Users can always see the repositories that they own, as well as
> +        # public repositories.
>          owner = removeSecurityProxy(self.private_repository).owner
>          repositories = self.all_repositories.visibleByUser(owner)
>          self.assertEqual(
> @@ -520,6 +521,11 @@
>          search_results = self.collection.search(lp_name)
>          self.assertEqual([repository], list(search_results))
>  
> +    def test_exact_match_full_url(self):
> +        repository = self.factory.makeGitRepository()
> +        url = canonical_url(repository)
> +        self.assertEqual([repository], list(self.collection.search(url)))
> +
>      def test_exact_match_bad_url(self):
>          search_results = self.collection.search('http:hahafail')
>          self.assertEqual([], list(search_results))
> @@ -653,7 +659,8 @@
>          team1 = self.factory.makeTeam(owner=person)
>          repository = self.factory.makeGitRepository(owner=team1)
>          # Make another team that person is in that owns a repository in a
> -        # different namespace to the namespace of the repository owned by team1.
> +        # different namespace to the namespace of the repository owned by
> +        # team1.
>          team2 = self.factory.makeTeam(owner=person)
>          self.factory.makeGitRepository(owner=team2)
>          collection = self.all_repositories.inProject(repository.target)
> 
> === added file 'lib/lp/code/templates/gitrepository-index.pt'
> --- lib/lp/code/templates/gitrepository-index.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitrepository-index.pt	2015-03-04 16:58:30 +0000
> @@ -0,0 +1,53 @@
> +<html
> +  xmlns="http://www.w3.org/1999/xhtml";
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  metal:use-macro="view/macro:page/main_side"
> +  i18n:domain="launchpad"
> +>
> +
> +<metal:block fill-slot="head_epilogue">
> +  <style type="text/css">
> +    #clone-url dt {
> +      font-weight: strong;
> +    }
> +  </style>
> +</metal:block>
> +
> +<body>
> +
> +<metal:side fill-slot="side">
> +  <div tal:replace="structure context/@@+global-actions" />
> +</metal:side>
> +
> +<tal:registering metal:fill-slot="registering">
> +  Created by
> +    <tal:registrant replace="structure context/registrant/fmt:link" />
> +  on
> +    <tal:created-on replace="structure context/date_created/fmt:date" />
> +  and last modified on
> +    <tal:last-modified replace="structure context/date_last_modified/fmt:date" />
> +</tal:registering>
> +
> +<div metal:fill-slot="main">
> +
> +  <div class="yui-g first">
> +    <div id="repository-management" class="portlet">
> +      <tal:repository-management
> +          replace="structure context/@@++repository-management" />
> +    </div>
> +  </div>
> +
> +  <div class="yui-g">
> +    <div id="repository-info" class="portlet">
> +      <h2>Repository information</h2>
> +      <tal:repository-info
> +          replace="structure context/@@++repository-information" />
> +    </div>
> +  </div>
> +
> +</div>
> +
> +</body>
> +</html>
> 
> === added file 'lib/lp/code/templates/gitrepository-information.pt'
> --- lib/lp/code/templates/gitrepository-information.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitrepository-information.pt	2015-03-04 16:58:30 +0000
> @@ -0,0 +1,18 @@
> +<div
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";>
> +
> +  <div class="two-column-list">
> +    <dl id="owner">
> +      <dt>Owner:</dt>
> +      <dd tal:content="structure context/owner/fmt:link" />
> +    </dl>
> +
> +    <dl id="partof" tal:condition="context/target">
> +      <dt>Target:</dt>
> +      <dd tal:content="structure context/target/fmt:link" />
> +    </dl>
> +  </div>
> +
> +</div>
> 
> === added file 'lib/lp/code/templates/gitrepository-management.pt'
> --- lib/lp/code/templates/gitrepository-management.pt	1970-01-01 00:00:00 +0000
> +++ lib/lp/code/templates/gitrepository-management.pt	2015-03-04 16:58:30 +0000
> @@ -0,0 +1,82 @@
> +<div
> +  xmlns:tal="http://xml.zope.org/namespaces/tal";
> +  xmlns:metal="http://xml.zope.org/namespaces/metal";
> +  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
> +  tal:define="context_menu view/context/menu:context">
> +
> +  <dl id="clone-url">
> +    <dt>Get this repository:</dt>
> +    <dd>
> +      <tal:anonymous condition="view/anon_url">
> +        <tt class="command">
> +          git clone <span class="anon-url" tal:content="view/anon_url" />
> +        </tt>
> +        <br />
> +      </tal:anonymous>
> +      <tal:ssh condition="view/ssh_url">
> +        <tt class="command">
> +          git clone <span class="ssh-url" tal:content="view/ssh_url" />
> +        </tt>
> +      </tal:ssh>
> +    </dd>
> +  </dl>
> +
> +  <div id="upload-directions">
> +    <tal:not-logged-in condition="not:view/user">
> +      <tal:individual condition="not:context/owner/is_team">
> +          Only
> +          <a tal:attributes="href context/owner/fmt:url"
> +             tal:content="context/owner/displayname">Person</a>
> +          can upload to this repository. If you are
> +          <tal:branch-owner replace="context/owner/displayname"/>
> +          please <a href="+login">log in</a> for upload directions.
> +      </tal:individual>
> +      <tal:team tal:condition="context/owner/is_team">
> +          Members of
> +          <a tal:attributes="href context/owner/fmt:url"
> +             tal:content="context/owner/displayname">Team</a>
> +          can upload to this repository. <a href="+login">Log in</a> for
> +          directions.
> +      </tal:team>
> +    </tal:not-logged-in>
> +
> +    <tal:logged-in condition="view/user">
> +      <tal:can-push tal:condition="view/user_can_push">
> +        <dl id="push-url">
> +          <dt>Update this repository:</dt>
> +          <dd>
> +            <tt class="command">
> +            git push
> +            </tt>
> +          </dd>
> +        </dl>
> +        <p tal:condition="not:view/user/sshkeys" id="ssh-key-directions">
> +          To authenticate with the Launchpad Git hosting service, you need to
> +          <a tal:attributes="href string:${view/user/fmt:url}/+editsshkeys">
> +            register a SSH key</a>.
> +        </p>
> +      </tal:can-push>
> +
> +      <tal:cannot-push condition="not:view/user_can_push">
> +        <div id="push-directions" tal:condition="not:context/owner/is_team">
> +          You cannot push to this repository. Only
> +          <a tal:attributes="href context/owner/fmt:url"
> +             tal:content="context/owner/displayname">Person</a>
> +          can push to this repository.
> +        </div>
> +        <div id="push-directions" tal:condition="context/owner/is_team">
> +          You cannot push to this repository. Members of
> +          <a tal:attributes="href context/owner/fmt:url"
> +             tal:content="context/owner/displayname">Team</a>
> +          can push to this repository.
> +        </div>
> +      </tal:cannot-push>
> +    </tal:logged-in>
> +
> +  </div>
> +
> +  <div style="margin-top: 1.5em" tal:define="link context_menu/source">
> +    <a tal:replace="structure link/fmt:link" />
> +  </div>
> +
> +</div>
> 
> === modified file 'lib/lp/registry/browser/person.py'
> --- lib/lp/registry/browser/person.py	2015-02-27 01:11:06 +0000
> +++ lib/lp/registry/browser/person.py	2015-03-04 16:58:30 +0000
> @@ -143,6 +143,7 @@
>  from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
>  from lp.code.errors import InvalidNamespace
>  from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
> +from lp.code.interfaces.gitlookup import IGitTraverser
>  from lp.registry.browser import BaseRdfView
>  from lp.registry.browser.branding import BrandingChangeView
>  from lp.registry.browser.menu import (
> @@ -155,6 +156,9 @@
>  from lp.registry.errors import VoucherAlreadyRedeemed
>  from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
>  from lp.registry.interfaces.distribution import IDistribution
> +from lp.registry.interfaces.distributionsourcepackage import (
> +    IDistributionSourcePackage,
> +    )
>  from lp.registry.interfaces.gpg import IGPGKeySet
>  from lp.registry.interfaces.irc import IIrcIDSet
>  from lp.registry.interfaces.jabber import (
> @@ -361,6 +365,40 @@
>          raise NotFoundError
>  
>      def traverse(self, pillar_name):
> +        try:
> +            # Look for a Git repository.  We must be careful not to consume
> +            # the traversal stack immediately, as if we fail to find a Git
> +            # repository we will need to look for a Bazaar branch instead.
> +            segments = (
> +                ["~%s" % self.context.name, pillar_name] +
> +                list(reversed(self.request.getTraversalStack())))
> +            num_segments = len(segments)
> +            iter_segments = iter(segments)
> +            traverser = getUtility(IGitTraverser)
> +            _, target, repository = traverser.traverse(iter_segments)
> +            if repository is None:
> +                raise NotFoundError
> +            for i in range(num_segments - len(list(iter_segments))):
> +                self.request.stepstogo.consume()

Ew, but OK.

> +
> +            if IProduct.providedBy(target):
> +                if target.name != pillar_name:
> +                    # This repository was accessed through one of its
> +                    # project's aliases, so we must redirect to its
> +                    # canonical URL.
> +                    return self.redirectSubTree(canonical_url(repository))
> +
> +            if IDistributionSourcePackage.providedBy(target):
> +                if target.distribution.name != pillar_name:
> +                    # This branch or repository was accessed through one of its
> +                    # distribution's aliases, so we must redirect to its
> +                    # canonical URL.
> +                    return self.redirectSubTree(canonical_url(repository))
> +
> +            return repository
> +        except (NotFoundError, InvalidNamespace):
> +            pass
> +
>          # If the pillar is a product, then return the PersonProduct; if it
>          # is a distribution and further segments provide a source package,
>          # then return the PersonDistributionSourcePackage.
> @@ -409,13 +447,13 @@
>  
>          if branch.product is not None:
>              if branch.product.name != pillar_name:
> -                # This branch was accessed through one of its product's
> +                # This branch was accessed through one of its project's
>                  # aliases, so we must redirect to its canonical URL.
>                  return self.redirectSubTree(canonical_url(branch))
>  
>          if branch.distribution is not None:
>              if branch.distribution.name != pillar_name:
> -                # This branch was accessed through one of its product's
> +                # This branch was accessed through one of its distribution's
>                  # aliases, so we must redirect to its canonical URL.
>                  return self.redirectSubTree(canonical_url(branch))
>  
> 
> === modified file 'lib/lp/registry/browser/tests/test_person.py'
> --- lib/lp/registry/browser/tests/test_person.py	2015-02-26 21:09:49 +0000
> +++ lib/lp/registry/browser/tests/test_person.py	2015-03-04 16:58:30 +0000
> @@ -1,4 +1,4 @@
> -# Copyright 2009-2014 Canonical Ltd.  This software is licensed under the
> +# Copyright 2009-2015 Canonical Ltd.  This software is licensed under the
>  # GNU Affero General Public License version 3 (see the file LICENSE).
>  
>  __metaclass__ = type
> @@ -144,6 +144,28 @@
>          self.assertRedirect('/api/devel' + in_suf, '/api/devel' + out_suf)
>          self.assertRedirect('/api/1.0' + in_suf, '/api/1.0' + out_suf)
>  
> +    def test_traverse_git_repository_project(self):
> +        project = self.factory.makeProduct()
> +        repository = self.factory.makeGitRepository(target=project)
> +        url = "/~%s/%s/+git/%s" % (
> +            repository.owner.name, project.name, repository.name)
> +        self.assertEqual(repository, test_traverse(url)[0])
> +
> +    def test_traverse_git_repository_package(self):
> +        dsp = self.factory.makeDistributionSourcePackage()
> +        repository = self.factory.makeGitRepository(target=dsp)
> +        url = "/~%s/%s/+source/%s/+git/%s" % (
> +            repository.owner.name, dsp.distribution.name,
> +            dsp.sourcepackagename.name, repository.name)
> +        self.assertEqual(repository, test_traverse(url)[0])
> +
> +    def test_traverse_git_repository_personal(self):
> +        person = self.factory.makePerson()
> +        repository = self.factory.makeGitRepository(
> +            owner=person, target=person)
> +        url = "/~%s/+git/%s" % (person.name, repository.name)
> +        self.assertEqual(repository, test_traverse(url)[0])
> +
>  
>  class PersonViewOpenidIdentityUrlTestCase(TestCaseWithFactory):
>      """Tests for the public OpenID identifier shown on the profile page."""
> @@ -274,7 +296,7 @@
>              implementation_status=SpecificationImplementationStatus.STARTED,
>              information_type=InformationType.PUBLIC)
>          private_name = 'super-private'
> -        private_spec = self.factory.makeSpecification(
> +        self.factory.makeSpecification(
>              name=private_name, assignee=person,
>              implementation_status=SpecificationImplementationStatus.STARTED,
>              information_type=InformationType.PROPRIETARY)
> 


-- 
https://code.launchpad.net/~cjwatson/launchpad/git-basic-browser/+merge/251779
Your team Launchpad code reviewers is subscribed to branch lp:launchpad.


References