launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18024
[Merge] lp:~cjwatson/launchpad/git-basic-browser into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/git-basic-browser into lp:launchpad.
Commit message:
Add basic navigation and a bare minimum of browser rendering for Git repositories.
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-basic-browser/+merge/251779
Add basic navigation and a bare minimum of browser rendering for Git repositories.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-basic-browser into lp:launchpad.
=== 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)
+ 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()
+
+ 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)
Follow ups