Merge lp:~cjwatson/launchpad/git-ref-url-encode into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18755
Proposed branch: lp:~cjwatson/launchpad/git-ref-url-encode
Merge into: lp:launchpad
Diff against target: 211 lines (+70/-8)
8 files modified
lib/lp/code/browser/configure.zcml (+1/-1)
lib/lp/code/browser/tests/test_gitlisting.py (+8/-2)
lib/lp/code/browser/tests/test_gitref.py (+35/-2)
lib/lp/code/browser/tests/test_gitrepository.py (+5/-0)
lib/lp/code/interfaces/gitref.py (+2/-0)
lib/lp/code/model/gitref.py (+5/-0)
lib/lp/testing/publication.py (+3/-2)
lib/lp/testing/tests/test_publication.py (+11/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-ref-url-encode
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+353464@code.launchpad.net

Commit message

Percent-encode reference names in GitRef URLs.

Description of the change

Conceivably we could do this more generally in lp.services.webapp.metazcml.InterfaceInstanceDispatcher.__getitem__ instead, but that's a rather more sweeping change, and most URL segments we generate are based on IDs or LP-validated names and thus have a restricted enough character set that we don't need to worry about encoding. Git reference names are unusual in that they allow characters that might need URL-encoding; see git-check-ref-format(1).

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/browser/configure.zcml'
--- lib/lp/code/browser/configure.zcml 2018-05-16 19:09:04 +0000
+++ lib/lp/code/browser/configure.zcml 2018-08-21 00:01:12 +0000
@@ -913,7 +913,7 @@
913 name="+index"/>913 name="+index"/>
914 <browser:url914 <browser:url
915 for="lp.code.interfaces.gitref.IGitRef"915 for="lp.code.interfaces.gitref.IGitRef"
916 path_expression="string:+ref/${name}"916 path_expression="string:+ref/${url_quoted_name}"
917 attribute_to_parent="repository"917 attribute_to_parent="repository"
918 rootsite="code"/>918 rootsite="code"/>
919 <browser:pages919 <browser:pages
920920
=== modified file 'lib/lp/code/browser/tests/test_gitlisting.py'
--- lib/lp/code/browser/tests/test_gitlisting.py 2018-02-01 18:38:24 +0000
+++ lib/lp/code/browser/tests/test_gitlisting.py 2018-08-21 00:01:12 +0000
@@ -36,7 +36,10 @@
36 owner=self.owner, target=self.target, name="foo")36 owner=self.owner, target=self.target, name="foo")
37 self.factory.makeGitRefs(37 self.factory.makeGitRefs(
38 main_repo,38 main_repo,
39 paths=["refs/heads/master", "refs/heads/1.0", "refs/tags/1.1"])39 paths=[
40 "refs/heads/master", "refs/heads/1.0", "refs/heads/with#hash",
41 "refs/tags/1.1",
42 ])
4043
41 other_repo = self.factory.makeGitRepository(44 other_repo = self.factory.makeGitRepository(
42 owner=self.factory.makePerson(name="contributor"),45 owner=self.factory.makePerson(name="contributor"),
@@ -71,11 +74,14 @@
71 table = soup.find(74 table = soup.find(
72 'div', id='default-repository-branches').find('table')75 'div', id='default-repository-branches').find('table')
73 self.assertContentEqual(76 self.assertContentEqual(
74 ['1.0', 'master'],77 ['1.0', 'master', 'with#hash'],
75 [link.find(text=True) for link in table.findAll('a')])78 [link.find(text=True) for link in table.findAll('a')])
76 self.assertEndsWith(79 self.assertEndsWith(
77 table.find(text="1.0").parent['href'],80 table.find(text="1.0").parent['href'],
78 "/~foowner/%s/+git/foo/+ref/1.0" % self.target_path)81 "/~foowner/%s/+git/foo/+ref/1.0" % self.target_path)
82 self.assertEndsWith(
83 table.find(text="with#hash").parent['href'],
84 "/~foowner/%s/+git/foo/+ref/with%%23hash" % self.target_path)
7985
80 # Other repos are listed.86 # Other repos are listed.
81 table = soup.find(87 table = soup.find(
8288
=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
--- lib/lp/code/browser/tests/test_gitref.py 2017-10-21 18:14:14 +0000
+++ lib/lp/code/browser/tests/test_gitref.py 2018-08-21 00:01:12 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2017 Canonical Ltd. This software is licensed under the1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for GitRefView."""4"""Unit tests for GitRefView."""
@@ -28,9 +28,13 @@
28 admin_logged_in,28 admin_logged_in,
29 BrowserTestCase,29 BrowserTestCase,
30 StormStatementRecorder,30 StormStatementRecorder,
31 TestCaseWithFactory,
31 )32 )
32from lp.testing.dbuser import dbuser33from lp.testing.dbuser import dbuser
33from lp.testing.layers import LaunchpadFunctionalLayer34from lp.testing.layers import (
35 DatabaseFunctionalLayer,
36 LaunchpadFunctionalLayer,
37 )
34from lp.testing.matchers import HasQueryCount38from lp.testing.matchers import HasQueryCount
35from lp.testing.pages import (39from lp.testing.pages import (
36 extract_text,40 extract_text,
@@ -42,6 +46,35 @@
42 )46 )
4347
4448
49class TestGitRefNavigation(TestCaseWithFactory):
50
51 layer = DatabaseFunctionalLayer
52
53 def test_canonical_url_branch(self):
54 [ref] = self.factory.makeGitRefs(paths=["refs/heads/master"])
55 self.assertEqual(
56 "%s/+ref/master" % canonical_url(ref.repository),
57 canonical_url(ref))
58
59 def test_canonical_url_with_slash(self):
60 [ref] = self.factory.makeGitRefs(paths=["refs/heads/with/slash"])
61 self.assertEqual(
62 "%s/+ref/with/slash" % canonical_url(ref.repository),
63 canonical_url(ref))
64
65 def test_canonical_url_percent_encoded(self):
66 [ref] = self.factory.makeGitRefs(paths=["refs/heads/with#hash"])
67 self.assertEqual(
68 "%s/+ref/with%%23hash" % canonical_url(ref.repository),
69 canonical_url(ref))
70
71 def test_canonical_url_tag(self):
72 [ref] = self.factory.makeGitRefs(paths=["refs/tags/1.0"])
73 self.assertEqual(
74 "%s/+ref/refs/tags/1.0" % canonical_url(ref.repository),
75 canonical_url(ref))
76
77
45class TestGitRefView(BrowserTestCase):78class TestGitRefView(BrowserTestCase):
4679
47 layer = LaunchpadFunctionalLayer80 layer = LaunchpadFunctionalLayer
4881
=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
--- lib/lp/code/browser/tests/test_gitrepository.py 2018-06-22 10:01:34 +0000
+++ lib/lp/code/browser/tests/test_gitrepository.py 2018-08-21 00:01:12 +0000
@@ -76,6 +76,11 @@
76 url = "%s/+ref/refs/heads/master" % canonical_url(repository)76 url = "%s/+ref/refs/heads/master" % canonical_url(repository)
77 self.assertRaises(NotFound, test_traverse, url)77 self.assertRaises(NotFound, test_traverse, url)
7878
79 def test_traverse_quoted_ref(self):
80 [ref] = self.factory.makeGitRefs(paths=["refs/heads/with#hash"])
81 url = "%s/+ref/with%%23hash" % canonical_url(ref.repository)
82 self.assertEqual(ref, test_traverse(url)[0])
83
7984
80class TestGitRepositoryView(BrowserTestCase):85class TestGitRepositoryView(BrowserTestCase):
8186
8287
=== modified file 'lib/lp/code/interfaces/gitref.py'
--- lib/lp/code/interfaces/gitref.py 2018-06-15 16:58:14 +0000
+++ lib/lp/code/interfaces/gitref.py 2018-08-21 00:01:12 +0000
@@ -80,6 +80,8 @@
80 "A shortened version of the full path to this reference, with any "80 "A shortened version of the full path to this reference, with any "
81 "leading refs/heads/ removed.")81 "leading refs/heads/ removed.")
8282
83 url_quoted_name = Attribute("The reference name, quoted for use in URLs.")
84
83 commit_sha1 = exported(TextLine(85 commit_sha1 = exported(TextLine(
84 title=_("Commit SHA-1"), required=True, readonly=True,86 title=_("Commit SHA-1"), required=True, readonly=True,
85 description=_(87 description=_(
8688
=== modified file 'lib/lp/code/model/gitref.py'
--- lib/lp/code/model/gitref.py 2018-07-12 17:46:32 +0000
+++ lib/lp/code/model/gitref.py 2018-08-21 00:01:12 +0000
@@ -108,6 +108,11 @@
108 return self.path108 return self.path
109109
110 @property110 @property
111 def url_quoted_name(self):
112 """See `IGitRef`."""
113 return quote(self.name)
114
115 @property
111 def identity(self):116 def identity(self):
112 """See `IGitRef`."""117 """See `IGitRef`."""
113 return "%s:%s" % (self.repository.shortened_path, self.name)118 return "%s:%s" % (self.repository.shortened_path, self.name)
114119
=== modified file 'lib/lp/testing/publication.py'
--- lib/lp/testing/publication.py 2015-10-26 14:54:43 +0000
+++ lib/lp/testing/publication.py 2018-08-21 00:01:12 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Helpers for testing out publication related code."""4"""Helpers for testing out publication related code."""
@@ -12,6 +12,7 @@
1212
13from cStringIO import StringIO13from cStringIO import StringIO
1414
15from six.moves.urllib_parse import unquote
15from zope.app.publication.requestpublicationregistry import factoryRegistry16from zope.app.publication.requestpublicationregistry import factoryRegistry
16from zope.authentication.interfaces import IUnauthenticatedPrincipal17from zope.authentication.interfaces import IUnauthenticatedPrincipal
17from zope.component import (18from zope.component import (
@@ -98,7 +99,7 @@
98 request, publication = get_request_and_publication(99 request, publication = get_request_and_publication(
99 host=url_parts[1], extra_environment={100 host=url_parts[1], extra_environment={
100 'SERVER_URL': server_url,101 'SERVER_URL': server_url,
101 'PATH_INFO': path_info})102 'PATH_INFO': unquote(path_info)})
102103
103 request.setPublication(publication)104 request.setPublication(publication)
104 # We avoid calling publication.beforePublication because this starts a new105 # We avoid calling publication.beforePublication because this starts a new
105106
=== modified file 'lib/lp/testing/tests/test_publication.py'
--- lib/lp/testing/tests/test_publication.py 2013-04-10 09:36:25 +0000
+++ lib/lp/testing/tests/test_publication.py 2018-08-21 00:01:12 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2011 Canonical Ltd. This software is licensed under the1# Copyright 2010-2018 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the helpers in `lp.testing.publication`."""4"""Tests for the helpers in `lp.testing.publication`."""
@@ -7,6 +7,7 @@
77
8from lazr.restful import EntryResource8from lazr.restful import EntryResource
9from lazr.restful.utils import get_current_browser_request9from lazr.restful.utils import get_current_browser_request
10from six.moves.urllib_parse import quote
10from zope.browserpage.simpleviewclass import simple11from zope.browserpage.simpleviewclass import simple
11from zope.component import (12from zope.component import (
12 getSiteManager,13 getSiteManager,
@@ -82,6 +83,15 @@
82 'https://launchpad.dev/' + product.name)83 'https://launchpad.dev/' + product.name)
83 self.assertEqual(product, context)84 self.assertEqual(product, context)
8485
86 def test_traverse_quoted(self):
87 # test_traverse decodes percent-encoded segments in URLs when
88 # constructing PATH_INFO so that traversal works.
89 login(ANONYMOUS)
90 product = self.factory.makeProduct(name='foo+bar')
91 context, view, request = test_traverse(
92 'https://launchpad.dev/' + quote(product.name))
93 self.assertEqual(product, context)
94
85 def test_request_is_current_during_traversal(self):95 def test_request_is_current_during_traversal(self):
86 # The request that test_traverse creates is current during96 # The request that test_traverse creates is current during
87 # traversal in the sense of get_current_browser_request.97 # traversal in the sense of get_current_browser_request.