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
1=== modified file 'lib/lp/code/browser/configure.zcml'
2--- lib/lp/code/browser/configure.zcml 2018-05-16 19:09:04 +0000
3+++ lib/lp/code/browser/configure.zcml 2018-08-21 00:01:12 +0000
4@@ -913,7 +913,7 @@
5 name="+index"/>
6 <browser:url
7 for="lp.code.interfaces.gitref.IGitRef"
8- path_expression="string:+ref/${name}"
9+ path_expression="string:+ref/${url_quoted_name}"
10 attribute_to_parent="repository"
11 rootsite="code"/>
12 <browser:pages
13
14=== modified file 'lib/lp/code/browser/tests/test_gitlisting.py'
15--- lib/lp/code/browser/tests/test_gitlisting.py 2018-02-01 18:38:24 +0000
16+++ lib/lp/code/browser/tests/test_gitlisting.py 2018-08-21 00:01:12 +0000
17@@ -36,7 +36,10 @@
18 owner=self.owner, target=self.target, name="foo")
19 self.factory.makeGitRefs(
20 main_repo,
21- paths=["refs/heads/master", "refs/heads/1.0", "refs/tags/1.1"])
22+ paths=[
23+ "refs/heads/master", "refs/heads/1.0", "refs/heads/with#hash",
24+ "refs/tags/1.1",
25+ ])
26
27 other_repo = self.factory.makeGitRepository(
28 owner=self.factory.makePerson(name="contributor"),
29@@ -71,11 +74,14 @@
30 table = soup.find(
31 'div', id='default-repository-branches').find('table')
32 self.assertContentEqual(
33- ['1.0', 'master'],
34+ ['1.0', 'master', 'with#hash'],
35 [link.find(text=True) for link in table.findAll('a')])
36 self.assertEndsWith(
37 table.find(text="1.0").parent['href'],
38 "/~foowner/%s/+git/foo/+ref/1.0" % self.target_path)
39+ self.assertEndsWith(
40+ table.find(text="with#hash").parent['href'],
41+ "/~foowner/%s/+git/foo/+ref/with%%23hash" % self.target_path)
42
43 # Other repos are listed.
44 table = soup.find(
45
46=== modified file 'lib/lp/code/browser/tests/test_gitref.py'
47--- lib/lp/code/browser/tests/test_gitref.py 2017-10-21 18:14:14 +0000
48+++ lib/lp/code/browser/tests/test_gitref.py 2018-08-21 00:01:12 +0000
49@@ -1,4 +1,4 @@
50-# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
51+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
52 # GNU Affero General Public License version 3 (see the file LICENSE).
53
54 """Unit tests for GitRefView."""
55@@ -28,9 +28,13 @@
56 admin_logged_in,
57 BrowserTestCase,
58 StormStatementRecorder,
59+ TestCaseWithFactory,
60 )
61 from lp.testing.dbuser import dbuser
62-from lp.testing.layers import LaunchpadFunctionalLayer
63+from lp.testing.layers import (
64+ DatabaseFunctionalLayer,
65+ LaunchpadFunctionalLayer,
66+ )
67 from lp.testing.matchers import HasQueryCount
68 from lp.testing.pages import (
69 extract_text,
70@@ -42,6 +46,35 @@
71 )
72
73
74+class TestGitRefNavigation(TestCaseWithFactory):
75+
76+ layer = DatabaseFunctionalLayer
77+
78+ def test_canonical_url_branch(self):
79+ [ref] = self.factory.makeGitRefs(paths=["refs/heads/master"])
80+ self.assertEqual(
81+ "%s/+ref/master" % canonical_url(ref.repository),
82+ canonical_url(ref))
83+
84+ def test_canonical_url_with_slash(self):
85+ [ref] = self.factory.makeGitRefs(paths=["refs/heads/with/slash"])
86+ self.assertEqual(
87+ "%s/+ref/with/slash" % canonical_url(ref.repository),
88+ canonical_url(ref))
89+
90+ def test_canonical_url_percent_encoded(self):
91+ [ref] = self.factory.makeGitRefs(paths=["refs/heads/with#hash"])
92+ self.assertEqual(
93+ "%s/+ref/with%%23hash" % canonical_url(ref.repository),
94+ canonical_url(ref))
95+
96+ def test_canonical_url_tag(self):
97+ [ref] = self.factory.makeGitRefs(paths=["refs/tags/1.0"])
98+ self.assertEqual(
99+ "%s/+ref/refs/tags/1.0" % canonical_url(ref.repository),
100+ canonical_url(ref))
101+
102+
103 class TestGitRefView(BrowserTestCase):
104
105 layer = LaunchpadFunctionalLayer
106
107=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
108--- lib/lp/code/browser/tests/test_gitrepository.py 2018-06-22 10:01:34 +0000
109+++ lib/lp/code/browser/tests/test_gitrepository.py 2018-08-21 00:01:12 +0000
110@@ -76,6 +76,11 @@
111 url = "%s/+ref/refs/heads/master" % canonical_url(repository)
112 self.assertRaises(NotFound, test_traverse, url)
113
114+ def test_traverse_quoted_ref(self):
115+ [ref] = self.factory.makeGitRefs(paths=["refs/heads/with#hash"])
116+ url = "%s/+ref/with%%23hash" % canonical_url(ref.repository)
117+ self.assertEqual(ref, test_traverse(url)[0])
118+
119
120 class TestGitRepositoryView(BrowserTestCase):
121
122
123=== modified file 'lib/lp/code/interfaces/gitref.py'
124--- lib/lp/code/interfaces/gitref.py 2018-06-15 16:58:14 +0000
125+++ lib/lp/code/interfaces/gitref.py 2018-08-21 00:01:12 +0000
126@@ -80,6 +80,8 @@
127 "A shortened version of the full path to this reference, with any "
128 "leading refs/heads/ removed.")
129
130+ url_quoted_name = Attribute("The reference name, quoted for use in URLs.")
131+
132 commit_sha1 = exported(TextLine(
133 title=_("Commit SHA-1"), required=True, readonly=True,
134 description=_(
135
136=== modified file 'lib/lp/code/model/gitref.py'
137--- lib/lp/code/model/gitref.py 2018-07-12 17:46:32 +0000
138+++ lib/lp/code/model/gitref.py 2018-08-21 00:01:12 +0000
139@@ -108,6 +108,11 @@
140 return self.path
141
142 @property
143+ def url_quoted_name(self):
144+ """See `IGitRef`."""
145+ return quote(self.name)
146+
147+ @property
148 def identity(self):
149 """See `IGitRef`."""
150 return "%s:%s" % (self.repository.shortened_path, self.name)
151
152=== modified file 'lib/lp/testing/publication.py'
153--- lib/lp/testing/publication.py 2015-10-26 14:54:43 +0000
154+++ lib/lp/testing/publication.py 2018-08-21 00:01:12 +0000
155@@ -1,4 +1,4 @@
156-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
157+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
158 # GNU Affero General Public License version 3 (see the file LICENSE).
159
160 """Helpers for testing out publication related code."""
161@@ -12,6 +12,7 @@
162
163 from cStringIO import StringIO
164
165+from six.moves.urllib_parse import unquote
166 from zope.app.publication.requestpublicationregistry import factoryRegistry
167 from zope.authentication.interfaces import IUnauthenticatedPrincipal
168 from zope.component import (
169@@ -98,7 +99,7 @@
170 request, publication = get_request_and_publication(
171 host=url_parts[1], extra_environment={
172 'SERVER_URL': server_url,
173- 'PATH_INFO': path_info})
174+ 'PATH_INFO': unquote(path_info)})
175
176 request.setPublication(publication)
177 # We avoid calling publication.beforePublication because this starts a new
178
179=== modified file 'lib/lp/testing/tests/test_publication.py'
180--- lib/lp/testing/tests/test_publication.py 2013-04-10 09:36:25 +0000
181+++ lib/lp/testing/tests/test_publication.py 2018-08-21 00:01:12 +0000
182@@ -1,4 +1,4 @@
183-# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
184+# Copyright 2010-2018 Canonical Ltd. This software is licensed under the
185 # GNU Affero General Public License version 3 (see the file LICENSE).
186
187 """Tests for the helpers in `lp.testing.publication`."""
188@@ -7,6 +7,7 @@
189
190 from lazr.restful import EntryResource
191 from lazr.restful.utils import get_current_browser_request
192+from six.moves.urllib_parse import quote
193 from zope.browserpage.simpleviewclass import simple
194 from zope.component import (
195 getSiteManager,
196@@ -82,6 +83,15 @@
197 'https://launchpad.dev/' + product.name)
198 self.assertEqual(product, context)
199
200+ def test_traverse_quoted(self):
201+ # test_traverse decodes percent-encoded segments in URLs when
202+ # constructing PATH_INFO so that traversal works.
203+ login(ANONYMOUS)
204+ product = self.factory.makeProduct(name='foo+bar')
205+ context, view, request = test_traverse(
206+ 'https://launchpad.dev/' + quote(product.name))
207+ self.assertEqual(product, context)
208+
209 def test_request_is_current_during_traversal(self):
210 # The request that test_traverse creates is current during
211 # traversal in the sense of get_current_browser_request.