launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20649
[Merge] lp:~thomir/launchpad/devel-fix-git-links into lp:launchpad
Thomi Richards has proposed merging lp:~thomir/launchpad/devel-fix-git-links into lp:launchpad.
Commit message:
Links to git repositories are now linked correctly.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~thomir/launchpad/devel-fix-git-links/+merge/297963
Make links to git repositories work in launchpad.
There are several changes here:
* Links to bzr branches or git repos in the form lp:<path> now generate expanded links in the form: /+code/<path> instead of /+branch/<path>. The +branch form still exists, as it's used by bzr clients, but it only deals with bazaar branches.
* The link checker API method now deals with +code links, instead of +branch links.
* +code links now traverse correctly to git repositories or bzr branches.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thomir/launchpad/devel-fix-git-links into lp:launchpad.
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2016-05-10 10:53:32 +0000
+++ lib/lp/app/browser/launchpad.py 2016-06-20 21:23:40 +0000
@@ -78,6 +78,7 @@
)
from lp.app.errors import (
GoneError,
+ NameLookupFailed,
NotFoundError,
POSTToNonCanonicalURL,
)
@@ -104,8 +105,10 @@
from lp.code.interfaces.codehosting import IBazaarApplication
from lp.code.interfaces.codeimport import ICodeImportSet
from lp.code.interfaces.gitrepository import IGitRepositorySet
+from lp.code.interfaces.gitlookup import IGitLookup
from lp.hardwaredb.interfaces.hwdb import IHWDBApplication
from lp.layers import WebServiceLayer
+from lp.registry.enums import VCSType
from lp.registry.interfaces.announcement import IAnnouncementSet
from lp.registry.interfaces.codeofconduct import ICodeOfConductSet
from lp.registry.interfaces.distribution import IDistributionSet
@@ -767,6 +770,70 @@
return self.redirectSubTree(target_url)
+ @stepto('+code')
+ def redirect_branch_or_repo(self):
+ """Redirect /+code/<foo> to the branch or repository named 'foo'.
+
+ 'foo' can be the unique name of the branch/repo, or any of the aliases
+ for the branch/repo.
+
+ If 'foo' is invalid, or exists but the user does not have permission to
+ view 'foo', a NotFoundError is raised, resulting in a 404 error.
+
+ Unlike +branch, this works for both git and bzr repositories/branches.
+ """
+ target_url = ''
+ path = '/'.join(self.request.stepstogo)
+
+ # Try a Git repository lookup first, since the schema is simpler and
+ # so it's quicker.
+ try:
+ repository, trailing = getUtility(IGitLookup).getByPath(path)
+ if repository is not None:
+ target_url = canonical_url(repository)
+ if trailing:
+ target_url = urlappend(target_url, trailing)
+ except (InvalidNamespace, InvalidProductName, NameLookupFailed,
+ Unauthorized):
+ # Either the git repository wasn't found, or it was found but we
+ # lack authority to access it. In either case, attempt a bzr lookup
+ # so don't set target_url
+ pass
+
+ # Git lookup failed. If '+git' is in the path, there's no point
+ # attempting a bzr lookup as well.
+ if '+git' not in path:
+ try:
+ branch, trailing = getUtility(IBranchLookup).getByLPPath(path)
+ bzr_url = canonical_url(branch)
+ if trailing != '':
+ bzr_url = urlappend(bzr_url, trailing)
+
+ if target_url:
+ # Project has both a bzr branch and a git repo. There's no
+ # right thing we can do here, so pretend we didn't see
+ # anything at all.
+ if branch.product.vcs is None:
+ target_url = ''
+ # if it's set to BZR, then set this branch as the target
+ if branch.product.vcs == VCSType.BZR:
+ target_url = bzr_url
+ else:
+ target_url = bzr_url
+ except (NoLinkedBranch, CannotHaveLinkedBranch, InvalidNamespace,
+ InvalidProductName, NotFoundError):
+ # No bzr branch found either.
+ pass
+
+ # Either neither bzr nor git returned matches, or they did but we're
+ # not authorised to view them, or they both did and the project has not
+ # set it's 'vcs' property to indicate which one to prefer. In all cases
+ # raise a 404:
+ if not target_url:
+ raise NotFoundError
+
+ return self.redirectSubTree(target_url)
+
@stepto('+builds')
def redirect_buildfarm(self):
"""Redirect old /+builds requests to new URL, /builders."""
=== modified file 'lib/lp/app/browser/linkchecker.py'
--- lib/lp/app/browser/linkchecker.py 2013-01-07 02:40:55 +0000
+++ lib/lp/app/browser/linkchecker.py 2016-06-20 21:23:40 +0000
@@ -19,6 +19,7 @@
NoSuchBranch,
)
from lp.code.interfaces.branchlookup import IBranchLookup
+from lp.code.interfaces.gitlookup import IGitLookup
from lp.registry.interfaces.product import InvalidProductName
from lp.services.searchbuilder import any
from lp.services.webapp import LaunchpadView
@@ -34,7 +35,7 @@
something appropriate.
This initial implementation supports processing links like the following:
- /+branch/foo/bar
+ /+code/foo/bar
The implementation can easily be extended to handle other forms by
providing a method to handle the link type extracted from the json
@@ -68,17 +69,19 @@
return simplejson.dumps(result)
def check_branch_links(self, links):
- """Check links of the form /+branch/foo/bar"""
+ """Check links of the form /+code/foo/bar"""
invalid_links = {}
- branch_lookup = getUtility(IBranchLookup)
+ bzr_branch_lookup = getUtility(IBranchLookup)
+ git_branch_lookup = getUtility(IGitLookup)
for link in links:
- path = link.strip('/')[len('+branch/'):]
- try:
- branch_lookup.getByLPPath(path)
- except (CannotHaveLinkedBranch, InvalidNamespace,
- InvalidProductName, NoLinkedBranch, NoSuchBranch,
- NotFoundError) as e:
- invalid_links[link] = self._error_message(e)
+ path = link.strip('/')[len('+code/'):]
+ if git_branch_lookup.getByPath(path)[0] is None:
+ try:
+ bzr_branch_lookup.getByLPPath(path)
+ except (CannotHaveLinkedBranch, InvalidNamespace,
+ InvalidProductName, NoLinkedBranch, NoSuchBranch,
+ NotFoundError) as e:
+ invalid_links[link] = self._error_message(e)
return {'invalid': invalid_links}
def check_bug_links(self, links):
=== modified file 'lib/lp/app/browser/stringformatter.py'
--- lib/lp/app/browser/stringformatter.py 2015-10-26 14:54:43 +0000
+++ lib/lp/app/browser/stringformatter.py 2016-06-20 21:23:40 +0000
@@ -455,7 +455,7 @@
if path.isdigit():
return FormattersAPI._linkify_bug_number(
lp_url, path, trailers)
- url = '/+branch/%s' % path
+ url = '/+code/%s' % path
# Mark the links with a 'branch-short-link' class so they can be
# harvested and validated when the page is rendered.
return structured(
=== modified file 'lib/lp/app/browser/tests/test_launchpad.py'
--- lib/lp/app/browser/tests/test_launchpad.py 2016-04-11 06:38:48 +0000
+++ lib/lp/app/browser/tests/test_launchpad.py 2016-06-20 21:23:40 +0000
@@ -22,9 +22,11 @@
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.services import IService
from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
+from lp.code.interfaces.gitrepository import IGitRepositorySet
from lp.registry.enums import (
PersonVisibility,
SharingPermission,
+ VCSType,
)
from lp.registry.interfaces.person import IPersonSet
from lp.services.identity.interfaces.account import AccountStatus
@@ -38,6 +40,7 @@
from lp.services.webapp.url import urlappend
from lp.testing import (
ANONYMOUS,
+ admin_logged_in,
celebrity_logged_in,
login,
login_person,
@@ -332,6 +335,253 @@
self.assertNotFound("_foo", use_default_referer=False)
+class TestCodeTraversal(TestCaseWithFactory, TraversalMixin):
+
+ layer = DatabaseFunctionalLayer
+
+ def traverse(self, path, **kwargs):
+ return super(TestCodeTraversal, self).traverse(
+ path, '+code', **kwargs)
+
+ def test_project_bzr_branch(self):
+ branch = self.factory.makeAnyBranch()
+ self.assertRedirects(branch.unique_name, canonical_url(branch))
+
+ def test_project_git_branch(self):
+ git_repo = self.factory.makeGitRepository()
+ self.assertRedirects(git_repo.unique_name, canonical_url(git_repo))
+
+ def test_no_such_bzr_unique_name(self):
+ branch = self.factory.makeAnyBranch()
+ bad_name = branch.unique_name + 'wibble'
+ self.assertNotFound(bad_name)
+
+ def test_no_such_git_unique_name(self):
+ repo = self.factory.makeGitRepository()
+ bad_name = repo.unique_name + 'wibble'
+ self.assertNotFound(bad_name)
+
+ def test_private_bzr_branch(self):
+ branch = self.factory.makeProductBranch(
+ information_type=InformationType.USERDATA)
+ branch_unique_name = removeSecurityProxy(branch).unique_name
+ login(ANONYMOUS)
+ self.assertNotFound(branch_unique_name)
+
+ def test_private_git_branch(self):
+ git_repo = self.factory.makeGitRepository(
+ information_type=InformationType.USERDATA)
+ repo_unique_name = removeSecurityProxy(git_repo).unique_name
+ login(ANONYMOUS)
+ self.assertNotFound(repo_unique_name)
+
+ def test_product_alias_bzr(self):
+ branch = self.factory.makeProductBranch()
+ naked_product = removeSecurityProxy(branch.product)
+ ICanHasLinkedBranch(naked_product).setBranch(branch)
+ self.assertRedirects(naked_product.name, canonical_url(branch))
+
+ def test_product_alias_git(self):
+ project = self.factory.makeProduct()
+ repo = self.factory.makeGitRepository(target=project)
+ naked_project = removeSecurityProxy(project)
+ with person_logged_in(repo.target.owner):
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ repo.target, repo)
+ self.assertRedirects(naked_project.name, canonical_url(repo))
+
+ def test_private_bzr_branch_for_product(self):
+ branch = self.factory.makeProductBranch()
+ naked_product = removeSecurityProxy(branch.product)
+ ICanHasLinkedBranch(naked_product).setBranch(branch)
+ removeSecurityProxy(branch).information_type = (
+ InformationType.USERDATA)
+ login(ANONYMOUS)
+ self.assertNotFound(naked_product.name)
+
+ def test_private_git_branch_for_product(self):
+ project = self.factory.makeProduct()
+ repo = self.factory.makeGitRepository(target=project)
+ with person_logged_in(repo.target.owner):
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ repo.target, repo)
+
+ removeSecurityProxy(repo).information_type = (
+ InformationType.USERDATA)
+ login(ANONYMOUS)
+
+ naked_project = removeSecurityProxy(project)
+ self.assertNotFound(naked_project.name)
+
+ def test_nonexistent_product(self):
+ non_existent = 'non-existent'
+ self.assertNotFound(non_existent)
+
+ def test_product_without_dev_focus(self):
+ product = self.factory.makeProduct()
+ self.assertNotFound(product.name)
+
+ def test_distro_package_alias_bzr(self):
+ sourcepackage = self.factory.makeSourcePackage()
+ branch = self.factory.makePackageBranch(sourcepackage=sourcepackage)
+ distro_package = sourcepackage.distribution_sourcepackage
+ registrant = distro_package.distribution.owner
+ target = ICanHasLinkedBranch(distro_package)
+ with person_logged_in(registrant):
+ target.setBranch(branch, registrant)
+ self.assertRedirects("%s" % target.bzr_path, canonical_url(branch))
+
+ def test_distro_package_alias_git(self):
+ sourcepackage = self.factory.makeSourcePackage()
+ distro_package = sourcepackage.distribution_sourcepackage
+ repo = self.factory.makeGitRepository(target=distro_package)
+
+ with admin_logged_in():
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ distro_package, repo)
+
+ self.assertRedirects("%s" % repo.shortened_path, canonical_url(repo))
+
+ def test_private_branch_for_distro_package_bzr(self):
+ sourcepackage = self.factory.makeSourcePackage()
+ branch = self.factory.makePackageBranch(
+ sourcepackage=sourcepackage,
+ information_type=InformationType.USERDATA)
+ distro_package = sourcepackage.distribution_sourcepackage
+ registrant = distro_package.distribution.owner
+ with person_logged_in(registrant):
+ ICanHasLinkedBranch(distro_package).setBranch(branch, registrant)
+ login(ANONYMOUS)
+ path = ICanHasLinkedBranch(distro_package).bzr_path
+ self.assertNotFound(path)
+
+ def test_private_branch_for_distro_package_git(self):
+ sourcepackage = self.factory.makeSourcePackage()
+ distro_package = sourcepackage.distribution_sourcepackage
+ repo = self.factory.makeGitRepository(
+ target=distro_package,
+ information_type=InformationType.USERDATA)
+ with admin_logged_in():
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ distro_package, repo)
+ login(ANONYMOUS)
+ path = removeSecurityProxy(repo).shortened_path
+ self.assertNotFound(path)
+
+ def test_trailing_path_redirect_bzr(self):
+ branch = self.factory.makeAnyBranch()
+ path = urlappend(branch.unique_name, '+edit')
+ self.assertRedirects(path, canonical_url(branch, view_name='+edit'))
+
+ def test_trailing_path_redirect_git(self):
+ repo = self.factory.makeGitRepository()
+ path = urlappend(repo.unique_name, '+edit')
+ self.assertRedirects(path, canonical_url(repo, view_name='+edit'))
+
+ def test_alias_trailing_path_redirect_bzr(self):
+ branch = self.factory.makeProductBranch()
+ with person_logged_in(branch.product.owner):
+ branch.product.development_focus.branch = branch
+ path = '%s/+edit' % branch.product.name
+ self.assertRedirects(path, canonical_url(branch, view_name='+edit'))
+
+ def test_alias_trailing_path_redirect_git(self):
+ project = self.factory.makeProduct()
+ repo = self.factory.makeGitRepository(target=project)
+ with admin_logged_in():
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ project, repo)
+ path = '%s/+edit' % project.name
+ self.assertRedirects(path, canonical_url(repo, view_name='+edit'))
+
+ def test_product_series_redirect_bzr(self):
+ branch = self.factory.makeBranch()
+ series = self.factory.makeProductSeries(branch=branch)
+ self.assertRedirects(
+ ICanHasLinkedBranch(series).bzr_path, canonical_url(branch))
+
+ def test_no_branch_for_series(self):
+ # If there's no branch for a product series, display a
+ # message telling the user there is no linked branch.
+ series = self.factory.makeProductSeries()
+ path = ICanHasLinkedBranch(series).bzr_path
+ self.assertNotFound(path)
+
+ def test_private_branch_for_series(self):
+ # If the development focus of a product series is private, display a
+ # message telling the user there is no linked branch.
+ branch = self.factory.makeBranch(
+ information_type=InformationType.USERDATA)
+ series = self.factory.makeProductSeries(branch=branch)
+ login(ANONYMOUS)
+ path = ICanHasLinkedBranch(series).bzr_path
+ self.assertNotFound(path)
+
+ def test_too_short_branch_name(self):
+ owner = self.factory.makePerson()
+ self.assertNotFound('~%s' % owner.name)
+
+ def test_invalid_product_name(self):
+ self.assertNotFound('_foo')
+
+ def test_invalid_product_name_without_referer(self):
+ self.assertNotFound("_foo", use_default_referer=False)
+
+ def test_ambigous_project_default_repo_bzr(self):
+ project = self.factory.makeProduct()
+ bzr_branch = self.factory.makeBranch(target=project)
+ self.factory.makeGitRepository(target=project)
+ with person_logged_in(project.owner):
+ ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
+
+ self.assertRedirects(project.name, canonical_url(bzr_branch))
+
+ def test_ambigous_project_without_vcs_set(self):
+ project = self.factory.makeProduct()
+ bzr_branch = self.factory.makeBranch(target=project)
+ repo = self.factory.makeGitRepository(target=project)
+ with person_logged_in(project.owner):
+ ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ project, repo)
+
+ self.assertNotFound(project.name)
+
+ def test_ambigous_project_with_vcs_set_to_git(self):
+ project = self.factory.makeProduct()
+ bzr_branch = self.factory.makeBranch(target=project)
+ repo = self.factory.makeGitRepository(target=project)
+ with person_logged_in(project.owner):
+ ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ project, repo)
+ project.vcs = VCSType.GIT
+
+ self.assertRedirects(project.name, canonical_url(repo))
+
+ def test_ambigous_project_with_vcs_set_to_bzr(self):
+ project = self.factory.makeProduct()
+ bzr_branch = self.factory.makeBranch(target=project)
+ repo = self.factory.makeGitRepository(target=project)
+ with person_logged_in(project.owner):
+ ICanHasLinkedBranch(project).setBranch(bzr_branch, project.owner)
+ getUtility(IGitRepositorySet).setDefaultRepository(
+ project, repo)
+ project.vcs = VCSType.BZR
+
+ self.assertRedirects(project.name, canonical_url(bzr_branch))
+
+ def test_personal_branch_bzr(self):
+ person = self.factory.makePerson()
+ branch = self.factory.makePersonalBranch(owner=person)
+ self.assertRedirects(branch.unique_name, canonical_url(branch))
+
+ def test_personal_branch_git(self):
+ person = self.factory.makePerson()
+ repo = self.factory.makeGitRepository(owner=person, target=person)
+ self.assertRedirects(repo.unique_name, canonical_url(repo))
+
+
class TestPersonTraversal(TestCaseWithFactory, TraversalMixin):
layer = DatabaseFunctionalLayer
=== modified file 'lib/lp/app/browser/tests/test_linkchecker.py'
--- lib/lp/app/browser/tests/test_linkchecker.py 2012-09-17 15:19:10 +0000
+++ lib/lp/app/browser/tests/test_linkchecker.py 2016-06-20 21:23:40 +0000
@@ -43,9 +43,22 @@
removeSecurityProxy(product).development_focus.branch = product_branch
valid_product_url = self.BRANCH_URL_TEMPLATE % product.name
+ # git branches are a thing now as well!
+ project_git_repo = removeSecurityProxy(
+ self.factory.makeGitRepository())
+ dsp = self.factory.makeDistributionSourcePackage()
+ dsp_git_repo = removeSecurityProxy(
+ self.factory.makeGitRepository(target=dsp))
+ person = self.factory.makePerson()
+ person_git_repo = removeSecurityProxy(
+ self.factory.makeGitRepository(target=person))
+
return [
valid_branch_url,
valid_product_url,
+ self.BRANCH_URL_TEMPLATE % project_git_repo.unique_name,
+ self.BRANCH_URL_TEMPLATE % dsp_git_repo.unique_name,
+ self.BRANCH_URL_TEMPLATE % person_git_repo.unique_name,
]
def make_invalid_branch_links(self):
Follow ups