launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18034
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