##// END OF EJS Templates
tests: fixed some tests for files pages.
marcink -
r3776:8199476a new-ui
parent child Browse files
Show More
@@ -1,218 +1,220 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import re
22 22
23 23 import pytest
24 24
25 25 from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
26 26 from rhodecode.tests import TestController
27 27
28 28 MATCH_HASH = re.compile(r'<span class="commit_hash">r(\d+):[\da-f]+</span>')
29 29
30 30
31 31 def route_path(name, params=None, **kwargs):
32 32 import urllib
33 33
34 34 base_url = {
35 35 'repo_changelog': '/{repo_name}/changelog',
36 36 'repo_commits': '/{repo_name}/commits',
37 37 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
38 38 'repo_commits_elements': '/{repo_name}/commits_elements',
39 39 }[name].format(**kwargs)
40 40
41 41 if params:
42 42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 43 return base_url
44 44
45 45
46 46 def assert_commits_on_page(response, indexes):
47 47 found_indexes = [int(idx) for idx in MATCH_HASH.findall(response.body)]
48 48 assert found_indexes == indexes
49 49
50 50
51 51 class TestChangelogController(TestController):
52 52
53 53 def test_commits_page(self, backend):
54 54 self.log_user()
55 55 response = self.app.get(
56 56 route_path('repo_commits', repo_name=backend.repo_name))
57 57
58 58 first_idx = -1
59 59 last_idx = -DEFAULT_CHANGELOG_SIZE
60 self.assert_commit_range_on_page(
61 response, first_idx, last_idx, backend)
60 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
62 61
63 62 def test_changelog(self, backend):
64 63 self.log_user()
65 64 response = self.app.get(
66 65 route_path('repo_changelog', repo_name=backend.repo_name))
67 66
68 67 first_idx = -1
69 68 last_idx = -DEFAULT_CHANGELOG_SIZE
70 69 self.assert_commit_range_on_page(
71 70 response, first_idx, last_idx, backend)
72 71
73 72 @pytest.mark.backends("hg", "git")
74 73 def test_changelog_filtered_by_branch(self, backend):
75 74 self.log_user()
76 75 self.app.get(
77 76 route_path('repo_changelog', repo_name=backend.repo_name,
78 77 params=dict(branch=backend.default_branch_name)),
79 78 status=200)
80 79
81 80 @pytest.mark.backends("hg", "git")
82 81 def test_commits_filtered_by_branch(self, backend):
83 82 self.log_user()
84 83 self.app.get(
85 84 route_path('repo_commits', repo_name=backend.repo_name,
86 85 params=dict(branch=backend.default_branch_name)),
87 86 status=200)
88 87
89 88 @pytest.mark.backends("svn")
90 89 def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
91 90 repo = backend['svn-simple-layout']
92 91 response = self.app.get(
93 92 route_path('repo_changelog', repo_name=repo.repo_name,
94 93 params=dict(branch='trunk')),
95 94 status=200)
96 95
97 96 assert_commits_on_page(response, indexes=[15, 12, 7, 3, 2, 1])
98 97
99 98 def test_commits_filtered_by_wrong_branch(self, backend):
100 99 self.log_user()
101 100 branch = 'wrong-branch-name'
102 101 response = self.app.get(
103 102 route_path('repo_commits', repo_name=backend.repo_name,
104 103 params=dict(branch=branch)),
105 104 status=302)
106 105 expected_url = '/{repo}/commits/{branch}'.format(
107 106 repo=backend.repo_name, branch=branch)
108 107 assert expected_url in response.location
109 108 response = response.follow()
110 109 expected_warning = 'Branch {} is not found.'.format(branch)
111 110 assert expected_warning in response.body
112 111
113 112 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
114 113 def test_changelog_filtered_by_branch_with_merges(
115 114 self, autologin_user, backend):
116 115
117 116 # Note: The changelog of branch "b" does not contain the commit "a1"
118 117 # although this is a parent of commit "b1". And branch "b" has commits
119 118 # which have a smaller index than commit "a1".
120 119 commits = [
121 120 {'message': 'a'},
122 121 {'message': 'b', 'branch': 'b'},
123 122 {'message': 'a1', 'parents': ['a']},
124 123 {'message': 'b1', 'branch': 'b', 'parents': ['b', 'a1']},
125 124 ]
126 125 backend.create_repo(commits)
127 126
128 127 self.app.get(
129 128 route_path('repo_changelog', repo_name=backend.repo_name,
130 129 params=dict(branch='b')),
131 130 status=200)
132 131
133 132 @pytest.mark.backends("hg")
134 133 def test_commits_closed_branches(self, autologin_user, backend):
135 134 repo = backend['closed_branch']
136 135 response = self.app.get(
137 136 route_path('repo_commits', repo_name=repo.repo_name,
138 137 params=dict(branch='experimental')),
139 138 status=200)
140 139
141 140 assert_commits_on_page(response, indexes=[3, 1])
142 141
143 142 def test_changelog_pagination(self, backend):
144 143 self.log_user()
145 144 # pagination, walk up to page 6
146 145 changelog_url = route_path(
147 146 'repo_commits', repo_name=backend.repo_name)
148 147
149 148 for page in range(1, 7):
150 149 response = self.app.get(changelog_url, {'page': page})
151 150
152 151 first_idx = -DEFAULT_CHANGELOG_SIZE * (page - 1) - 1
153 152 last_idx = -DEFAULT_CHANGELOG_SIZE * page
154 153 self.assert_commit_range_on_page(response, first_idx, last_idx, backend)
155 154
156 155 def assert_commit_range_on_page(
157 156 self, response, first_idx, last_idx, backend):
158 157 input_template = (
159 158 """<input class="commit-range" """
160 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" id="%(raw_id)s" """
159 """data-commit-id="%(raw_id)s" data-commit-idx="%(idx)s" """
160 """data-short-id="%(short_id)s" id="%(raw_id)s" """
161 161 """name="%(raw_id)s" type="checkbox" value="1" />"""
162 162 )
163 163
164 164 commit_span_template = """<span class="commit_hash">r%s:%s</span>"""
165 165 repo = backend.repo
166 166
167 167 first_commit_on_page = repo.get_commit(commit_idx=first_idx)
168 168 response.mustcontain(
169 169 input_template % {'raw_id': first_commit_on_page.raw_id,
170 'idx': first_commit_on_page.idx})
170 'idx': first_commit_on_page.idx,
171 'short_id': first_commit_on_page.short_id})
171 172
172 173 response.mustcontain(commit_span_template % (
173 174 first_commit_on_page.idx, first_commit_on_page.short_id)
174 175 )
175 176
176 177 last_commit_on_page = repo.get_commit(commit_idx=last_idx)
177 178 response.mustcontain(
178 179 input_template % {'raw_id': last_commit_on_page.raw_id,
179 'idx': last_commit_on_page.idx})
180 'idx': last_commit_on_page.idx,
181 'short_id': last_commit_on_page.short_id})
180 182 response.mustcontain(commit_span_template % (
181 183 last_commit_on_page.idx, last_commit_on_page.short_id)
182 184 )
183 185
184 186 first_commit_of_next_page = repo.get_commit(commit_idx=last_idx - 1)
185 187 first_span_of_next_page = commit_span_template % (
186 188 first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
187 189 assert first_span_of_next_page not in response
188 190
189 191 @pytest.mark.parametrize('test_path', [
190 192 'vcs/exceptions.py',
191 193 '/vcs/exceptions.py',
192 194 '//vcs/exceptions.py'
193 195 ])
194 196 def test_commits_with_filenode(self, backend, test_path):
195 197 self.log_user()
196 198 response = self.app.get(
197 199 route_path('repo_commits_file', repo_name=backend.repo_name,
198 200 commit_id='tip', f_path=test_path),
199 201 )
200 202
201 203 # history commits messages
202 204 response.mustcontain('Added exceptions module, this time for real')
203 205 response.mustcontain('Added not implemented hg backend test case')
204 206 response.mustcontain('Added BaseChangeset class')
205 207
206 208 def test_commits_with_filenode_that_is_dirnode(self, backend):
207 209 self.log_user()
208 210 self.app.get(
209 211 route_path('repo_commits_file', repo_name=backend.repo_name,
210 212 commit_id='tip', f_path='/tests'),
211 213 status=302)
212 214
213 215 def test_commits_with_filenode_not_existing(self, backend):
214 216 self.log_user()
215 217 self.app.get(
216 218 route_path('repo_commits_file', repo_name=backend.repo_name,
217 219 commit_id='tip', f_path='wrong_path'),
218 220 status=302)
@@ -1,1069 +1,1066 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.vcs import nodes
31 31
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.tests import assert_session_flash
34 34 from rhodecode.tests.fixture import Fixture
35 35 from rhodecode.model.db import Session
36 36
37 37 fixture = Fixture()
38 38
39 39
40 40 def get_node_history(backend_type):
41 41 return {
42 42 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 43 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 44 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 45 }[backend_type]
46 46
47 47
48 48 def route_path(name, params=None, **kwargs):
49 49 import urllib
50 50
51 51 base_url = {
52 52 'repo_summary': '/{repo_name}',
53 53 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 54 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 55 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 56 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 57 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 58 'repo_files:default_commit': '/{repo_name}/files',
59 59 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 60 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 61 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 62 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 63 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 64 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 65 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 66 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 67 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 68 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 69 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 70 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 71 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 72 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 73 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 74 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 75 }[name].format(**kwargs)
76 76
77 77 if params:
78 78 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 79 return base_url
80 80
81 81
82 82 def assert_files_in_response(response, files, params):
83 83 template = (
84 84 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 85 _assert_items_in_response(response, files, template, params)
86 86
87 87
88 88 def assert_dirs_in_response(response, dirs, params):
89 89 template = (
90 90 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 91 _assert_items_in_response(response, dirs, template, params)
92 92
93 93
94 94 def _assert_items_in_response(response, items, template, params):
95 95 for item in items:
96 96 item_params = {'name': item}
97 97 item_params.update(params)
98 98 response.mustcontain(template % item_params)
99 99
100 100
101 101 def assert_timeago_in_response(response, items, params):
102 102 for item in items:
103 103 response.mustcontain(h.age_component(params['date']))
104 104
105 105
106 106 @pytest.mark.usefixtures("app")
107 107 class TestFilesViews(object):
108 108
109 109 def test_show_files(self, backend):
110 110 response = self.app.get(
111 111 route_path('repo_files',
112 112 repo_name=backend.repo_name,
113 113 commit_id='tip', f_path='/'))
114 114 commit = backend.repo.get_commit()
115 115
116 116 params = {
117 117 'repo_name': backend.repo_name,
118 118 'commit_id': commit.raw_id,
119 119 'date': commit.date
120 120 }
121 121 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 122 files = [
123 123 '.gitignore',
124 124 '.hgignore',
125 125 '.hgtags',
126 126 # TODO: missing in Git
127 127 # '.travis.yml',
128 128 'MANIFEST.in',
129 129 'README.rst',
130 130 # TODO: File is missing in svn repository
131 131 # 'run_test_and_report.sh',
132 132 'setup.cfg',
133 133 'setup.py',
134 134 'test_and_report.sh',
135 135 'tox.ini',
136 136 ]
137 137 assert_files_in_response(response, files, params)
138 138 assert_timeago_in_response(response, files, params)
139 139
140 140 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 141 repo = backend_hg['subrepos']
142 142 response = self.app.get(
143 143 route_path('repo_files',
144 144 repo_name=repo.repo_name,
145 145 commit_id='tip', f_path='/'))
146 146 assert_response = response.assert_response()
147 147 assert_response.contains_one_link(
148 148 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 149
150 150 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 151 self, backend_hg):
152 152 repo = backend_hg['subrepos']
153 153 response = self.app.get(
154 154 route_path('repo_files',
155 155 repo_name=repo.repo_name,
156 156 commit_id='tip', f_path='/'))
157 157 assert_response = response.assert_response()
158 158 assert_response.contains_one_link(
159 159 'subpaths-path @ 000000000000',
160 160 'http://sub-base.example.com/subpaths-path')
161 161
162 162 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 163 def test_files_menu(self, backend):
164 164 new_branch = "temp_branch_name"
165 165 commits = [
166 166 {'message': 'a'},
167 167 {'message': 'b', 'branch': new_branch}
168 168 ]
169 169 backend.create_repo(commits)
170 170 backend.repo.landing_rev = "branch:%s" % new_branch
171 171 Session().commit()
172 172
173 173 # get response based on tip and not new commit
174 174 response = self.app.get(
175 175 route_path('repo_files',
176 176 repo_name=backend.repo_name,
177 177 commit_id='tip', f_path='/'))
178 178
179 179 # make sure Files menu url is not tip but new commit
180 180 landing_rev = backend.repo.landing_rev[1]
181 181 files_url = route_path('repo_files:default_path',
182 182 repo_name=backend.repo_name,
183 183 commit_id=landing_rev)
184 184
185 185 assert landing_rev != 'tip'
186 186 response.mustcontain(
187 187 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 188
189 189 def test_show_files_commit(self, backend):
190 190 commit = backend.repo.get_commit(commit_idx=32)
191 191
192 192 response = self.app.get(
193 193 route_path('repo_files',
194 194 repo_name=backend.repo_name,
195 195 commit_id=commit.raw_id, f_path='/'))
196 196
197 197 dirs = ['docs', 'tests']
198 198 files = ['README.rst']
199 199 params = {
200 200 'repo_name': backend.repo_name,
201 201 'commit_id': commit.raw_id,
202 202 }
203 203 assert_dirs_in_response(response, dirs, params)
204 204 assert_files_in_response(response, files, params)
205 205
206 206 def test_show_files_different_branch(self, backend):
207 207 branches = dict(
208 208 hg=(150, ['git']),
209 209 # TODO: Git test repository does not contain other branches
210 210 git=(633, ['master']),
211 211 # TODO: Branch support in Subversion
212 212 svn=(150, [])
213 213 )
214 214 idx, branches = branches[backend.alias]
215 215 commit = backend.repo.get_commit(commit_idx=idx)
216 216 response = self.app.get(
217 217 route_path('repo_files',
218 218 repo_name=backend.repo_name,
219 219 commit_id=commit.raw_id, f_path='/'))
220 220
221 221 assert_response = response.assert_response()
222 222 for branch in branches:
223 223 assert_response.element_contains('.tags .branchtag', branch)
224 224
225 225 def test_show_files_paging(self, backend):
226 226 repo = backend.repo
227 227 indexes = [73, 92, 109, 1, 0]
228 228 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 229 for rev in indexes]
230 230
231 231 for idx in idx_map:
232 232 response = self.app.get(
233 233 route_path('repo_files',
234 234 repo_name=backend.repo_name,
235 235 commit_id=idx[1], f_path='/'))
236 236
237 237 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 238
239 239 def test_file_source(self, backend):
240 240 commit = backend.repo.get_commit(commit_idx=167)
241 241 response = self.app.get(
242 242 route_path('repo_files',
243 243 repo_name=backend.repo_name,
244 244 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 245
246 msgbox = """<div class="commit right-content">%s</div>"""
246 msgbox = """<div class="commit">%s</div>"""
247 247 response.mustcontain(msgbox % (commit.message, ))
248 248
249 249 assert_response = response.assert_response()
250 250 if commit.branch:
251 251 assert_response.element_contains(
252 252 '.tags.tags-main .branchtag', commit.branch)
253 253 if commit.tags:
254 254 for tag in commit.tags:
255 255 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 256
257 257 def test_file_source_annotated(self, backend):
258 258 response = self.app.get(
259 259 route_path('repo_files:annotated',
260 260 repo_name=backend.repo_name,
261 261 commit_id='tip', f_path='vcs/nodes.py'))
262 262 expected_commits = {
263 263 'hg': 'r356',
264 264 'git': 'r345',
265 265 'svn': 'r208',
266 266 }
267 267 response.mustcontain(expected_commits[backend.alias])
268 268
269 269 def test_file_source_authors(self, backend):
270 270 response = self.app.get(
271 271 route_path('repo_file_authors',
272 272 repo_name=backend.repo_name,
273 273 commit_id='tip', f_path='vcs/nodes.py'))
274 274 expected_authors = {
275 275 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 276 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 277 'svn': ('marcin', 'lukasz'),
278 278 }
279 279
280 280 for author in expected_authors[backend.alias]:
281 281 response.mustcontain(author)
282 282
283 283 def test_file_source_authors_with_annotation(self, backend):
284 284 response = self.app.get(
285 285 route_path('repo_file_authors',
286 286 repo_name=backend.repo_name,
287 287 commit_id='tip', f_path='vcs/nodes.py',
288 288 params=dict(annotate=1)))
289 289 expected_authors = {
290 290 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 291 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 292 'svn': ('marcin', 'lukasz'),
293 293 }
294 294
295 295 for author in expected_authors[backend.alias]:
296 296 response.mustcontain(author)
297 297
298 298 def test_file_source_history(self, backend, xhr_header):
299 299 response = self.app.get(
300 300 route_path('repo_file_history',
301 301 repo_name=backend.repo_name,
302 302 commit_id='tip', f_path='vcs/nodes.py'),
303 303 extra_environ=xhr_header)
304 304 assert get_node_history(backend.alias) == json.loads(response.body)
305 305
306 306 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 307 simple_repo = backend_svn['svn-simple-layout']
308 308 response = self.app.get(
309 309 route_path('repo_file_history',
310 310 repo_name=simple_repo.repo_name,
311 311 commit_id='tip', f_path='trunk/example.py'),
312 312 extra_environ=xhr_header)
313 313
314 314 expected_data = json.loads(
315 315 fixture.load_resource('svn_node_history_branches.json'))
316
316 317 assert expected_data == response.json
317 318
318 319 def test_file_source_history_with_annotation(self, backend, xhr_header):
319 320 response = self.app.get(
320 321 route_path('repo_file_history',
321 322 repo_name=backend.repo_name,
322 323 commit_id='tip', f_path='vcs/nodes.py',
323 324 params=dict(annotate=1)),
324 325
325 326 extra_environ=xhr_header)
326 327 assert get_node_history(backend.alias) == json.loads(response.body)
327 328
328 329 def test_tree_search_top_level(self, backend, xhr_header):
329 330 commit = backend.repo.get_commit(commit_idx=173)
330 331 response = self.app.get(
331 332 route_path('repo_files_nodelist',
332 333 repo_name=backend.repo_name,
333 334 commit_id=commit.raw_id, f_path='/'),
334 335 extra_environ=xhr_header)
335 336 assert 'nodes' in response.json
336 337 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
337 338
338 339 def test_tree_search_missing_xhr(self, backend):
339 340 self.app.get(
340 341 route_path('repo_files_nodelist',
341 342 repo_name=backend.repo_name,
342 343 commit_id='tip', f_path='/'),
343 344 status=404)
344 345
345 346 def test_tree_search_at_path(self, backend, xhr_header):
346 347 commit = backend.repo.get_commit(commit_idx=173)
347 348 response = self.app.get(
348 349 route_path('repo_files_nodelist',
349 350 repo_name=backend.repo_name,
350 351 commit_id=commit.raw_id, f_path='/docs'),
351 352 extra_environ=xhr_header)
352 353 assert 'nodes' in response.json
353 354 nodes = response.json['nodes']
354 355 assert {'name': 'docs/api', 'type': 'dir'} in nodes
355 356 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
356 357
357 358 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
358 359 commit = backend.repo.get_commit(commit_idx=173)
359 360 response = self.app.get(
360 361 route_path('repo_files_nodelist',
361 362 repo_name=backend.repo_name,
362 363 commit_id=commit.raw_id, f_path='/docs/api'),
363 364 extra_environ=xhr_header)
364 365 assert 'nodes' in response.json
365 366 nodes = response.json['nodes']
366 367 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
367 368
368 369 def test_tree_search_at_path_missing_xhr(self, backend):
369 370 self.app.get(
370 371 route_path('repo_files_nodelist',
371 372 repo_name=backend.repo_name,
372 373 commit_id='tip', f_path='/docs'),
373 374 status=404)
374 375
375 376 def test_nodetree(self, backend, xhr_header):
376 377 commit = backend.repo.get_commit(commit_idx=173)
377 378 response = self.app.get(
378 379 route_path('repo_nodetree_full',
379 380 repo_name=backend.repo_name,
380 381 commit_id=commit.raw_id, f_path='/'),
381 382 extra_environ=xhr_header)
382 383
383 384 assert_response = response.assert_response()
384 385
385 386 for attr in ['data-commit-id', 'data-date', 'data-author']:
386 387 elements = assert_response.get_elements('[{}]'.format(attr))
387 388 assert len(elements) > 1
388 389
389 390 for element in elements:
390 391 assert element.get(attr)
391 392
392 393 def test_nodetree_if_file(self, backend, xhr_header):
393 394 commit = backend.repo.get_commit(commit_idx=173)
394 395 response = self.app.get(
395 396 route_path('repo_nodetree_full',
396 397 repo_name=backend.repo_name,
397 398 commit_id=commit.raw_id, f_path='README.rst'),
398 399 extra_environ=xhr_header)
399 400 assert response.body == ''
400 401
401 402 def test_nodetree_wrong_path(self, backend, xhr_header):
402 403 commit = backend.repo.get_commit(commit_idx=173)
403 404 response = self.app.get(
404 405 route_path('repo_nodetree_full',
405 406 repo_name=backend.repo_name,
406 407 commit_id=commit.raw_id, f_path='/dont-exist'),
407 408 extra_environ=xhr_header)
408 409
409 410 err = 'error: There is no file nor ' \
410 411 'directory at the given path'
411 412 assert err in response.body
412 413
413 414 def test_nodetree_missing_xhr(self, backend):
414 415 self.app.get(
415 416 route_path('repo_nodetree_full',
416 417 repo_name=backend.repo_name,
417 418 commit_id='tip', f_path='/'),
418 419 status=404)
419 420
420 421
421 422 @pytest.mark.usefixtures("app", "autologin_user")
422 423 class TestRawFileHandling(object):
423 424
424 425 def test_download_file(self, backend):
425 426 commit = backend.repo.get_commit(commit_idx=173)
426 427 response = self.app.get(
427 428 route_path('repo_file_download',
428 429 repo_name=backend.repo_name,
429 430 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
430 431
431 432 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
432 433 assert response.content_type == "text/x-python"
433 434
434 435 def test_download_file_wrong_cs(self, backend):
435 436 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
436 437
437 438 response = self.app.get(
438 439 route_path('repo_file_download',
439 440 repo_name=backend.repo_name,
440 441 commit_id=raw_id, f_path='vcs/nodes.svg'),
441 442 status=404)
442 443
443 444 msg = """No such commit exists for this repository"""
444 445 response.mustcontain(msg)
445 446
446 447 def test_download_file_wrong_f_path(self, backend):
447 448 commit = backend.repo.get_commit(commit_idx=173)
448 449 f_path = 'vcs/ERRORnodes.py'
449 450
450 451 response = self.app.get(
451 452 route_path('repo_file_download',
452 453 repo_name=backend.repo_name,
453 454 commit_id=commit.raw_id, f_path=f_path),
454 455 status=404)
455 456
456 457 msg = (
457 458 "There is no file nor directory at the given path: "
458 459 "`%s` at commit %s" % (f_path, commit.short_id))
459 460 response.mustcontain(msg)
460 461
461 462 def test_file_raw(self, backend):
462 463 commit = backend.repo.get_commit(commit_idx=173)
463 464 response = self.app.get(
464 465 route_path('repo_file_raw',
465 466 repo_name=backend.repo_name,
466 467 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
467 468
468 469 assert response.content_type == "text/plain"
469 470
470 471 def test_file_raw_binary(self, backend):
471 472 commit = backend.repo.get_commit()
472 473 response = self.app.get(
473 474 route_path('repo_file_raw',
474 475 repo_name=backend.repo_name,
475 476 commit_id=commit.raw_id,
476 477 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
477 478
478 479 assert response.content_disposition == 'inline'
479 480
480 481 def test_raw_file_wrong_cs(self, backend):
481 482 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
482 483
483 484 response = self.app.get(
484 485 route_path('repo_file_raw',
485 486 repo_name=backend.repo_name,
486 487 commit_id=raw_id, f_path='vcs/nodes.svg'),
487 488 status=404)
488 489
489 490 msg = """No such commit exists for this repository"""
490 491 response.mustcontain(msg)
491 492
492 493 def test_raw_wrong_f_path(self, backend):
493 494 commit = backend.repo.get_commit(commit_idx=173)
494 495 f_path = 'vcs/ERRORnodes.py'
495 496 response = self.app.get(
496 497 route_path('repo_file_raw',
497 498 repo_name=backend.repo_name,
498 499 commit_id=commit.raw_id, f_path=f_path),
499 500 status=404)
500 501
501 502 msg = (
502 503 "There is no file nor directory at the given path: "
503 504 "`%s` at commit %s" % (f_path, commit.short_id))
504 505 response.mustcontain(msg)
505 506
506 507 def test_raw_svg_should_not_be_rendered(self, backend):
507 508 backend.create_repo()
508 509 backend.ensure_file("xss.svg")
509 510 response = self.app.get(
510 511 route_path('repo_file_raw',
511 512 repo_name=backend.repo_name,
512 513 commit_id='tip', f_path='xss.svg'),)
513 514 # If the content type is image/svg+xml then it allows to render HTML
514 515 # and malicious SVG.
515 516 assert response.content_type == "text/plain"
516 517
517 518
518 519 @pytest.mark.usefixtures("app")
519 520 class TestRepositoryArchival(object):
520 521
521 522 def test_archival(self, backend):
522 523 backend.enable_downloads()
523 524 commit = backend.repo.get_commit(commit_idx=173)
524 525 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
525 526
526 527 short = commit.short_id + extension
527 528 fname = commit.raw_id + extension
528 529 filename = '%s-%s' % (backend.repo_name, short)
529 530 response = self.app.get(
530 531 route_path('repo_archivefile',
531 532 repo_name=backend.repo_name,
532 533 fname=fname))
533 534
534 535 assert response.status == '200 OK'
535 536 headers = [
536 537 ('Content-Disposition', 'attachment; filename=%s' % filename),
537 538 ('Content-Type', '%s' % content_type),
538 539 ]
539 540
540 541 for header in headers:
541 542 assert header in response.headers.items()
542 543
543 544 @pytest.mark.parametrize('arch_ext',[
544 545 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
545 546 def test_archival_wrong_ext(self, backend, arch_ext):
546 547 backend.enable_downloads()
547 548 commit = backend.repo.get_commit(commit_idx=173)
548 549
549 550 fname = commit.raw_id + '.' + arch_ext
550 551
551 552 response = self.app.get(
552 553 route_path('repo_archivefile',
553 554 repo_name=backend.repo_name,
554 555 fname=fname))
555 556 response.mustcontain(
556 557 'Unknown archive type for: `{}`'.format(fname))
557 558
558 559 @pytest.mark.parametrize('commit_id', [
559 560 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
560 561 def test_archival_wrong_commit_id(self, backend, commit_id):
561 562 backend.enable_downloads()
562 563 fname = '%s.zip' % commit_id
563 564
564 565 response = self.app.get(
565 566 route_path('repo_archivefile',
566 567 repo_name=backend.repo_name,
567 568 fname=fname))
568 569 response.mustcontain('Unknown commit_id')
569 570
570 571
571 572 @pytest.mark.usefixtures("app")
572 573 class TestFilesDiff(object):
573 574
574 575 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
575 576 def test_file_full_diff(self, backend, diff):
576 577 commit1 = backend.repo.get_commit(commit_idx=-1)
577 578 commit2 = backend.repo.get_commit(commit_idx=-2)
578 579
579 580 response = self.app.get(
580 581 route_path('repo_files_diff',
581 582 repo_name=backend.repo_name,
582 583 f_path='README'),
583 584 params={
584 585 'diff1': commit2.raw_id,
585 586 'diff2': commit1.raw_id,
586 587 'fulldiff': '1',
587 588 'diff': diff,
588 589 })
589 590
590 591 if diff == 'diff':
591 592 # use redirect since this is OLD view redirecting to compare page
592 593 response = response.follow()
593 594
594 595 # It's a symlink to README.rst
595 596 response.mustcontain('README.rst')
596 597 response.mustcontain('No newline at end of file')
597 598
598 599 def test_file_binary_diff(self, backend):
599 600 commits = [
600 601 {'message': 'First commit'},
601 602 {'message': 'Commit with binary',
602 603 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
603 604 ]
604 605 repo = backend.create_repo(commits=commits)
605 606
606 607 response = self.app.get(
607 608 route_path('repo_files_diff',
608 609 repo_name=backend.repo_name,
609 610 f_path='file.bin'),
610 611 params={
611 612 'diff1': repo.get_commit(commit_idx=0).raw_id,
612 613 'diff2': repo.get_commit(commit_idx=1).raw_id,
613 614 'fulldiff': '1',
614 615 'diff': 'diff',
615 616 })
616 617 # use redirect since this is OLD view redirecting to compare page
617 618 response = response.follow()
618 619 response.mustcontain('Expand 1 commit')
619 620 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
620 621
621 622 if backend.alias == 'svn':
622 623 response.mustcontain('new file 10644')
623 624 # TODO(marcink): SVN doesn't yet detect binary changes
624 625 else:
625 626 response.mustcontain('new file 100644')
626 627 response.mustcontain('binary diff hidden')
627 628
628 629 def test_diff_2way(self, backend):
629 630 commit1 = backend.repo.get_commit(commit_idx=-1)
630 631 commit2 = backend.repo.get_commit(commit_idx=-2)
631 632 response = self.app.get(
632 633 route_path('repo_files_diff_2way_redirect',
633 634 repo_name=backend.repo_name,
634 635 f_path='README'),
635 636 params={
636 637 'diff1': commit2.raw_id,
637 638 'diff2': commit1.raw_id,
638 639 })
639 640 # use redirect since this is OLD view redirecting to compare page
640 641 response = response.follow()
641 642
642 643 # It's a symlink to README.rst
643 644 response.mustcontain('README.rst')
644 645 response.mustcontain('No newline at end of file')
645 646
646 647 def test_requires_one_commit_id(self, backend, autologin_user):
647 648 response = self.app.get(
648 649 route_path('repo_files_diff',
649 650 repo_name=backend.repo_name,
650 651 f_path='README.rst'),
651 652 status=400)
652 653 response.mustcontain(
653 654 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
654 655
655 656 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
656 657 repo = vcsbackend.repo
657 658 response = self.app.get(
658 659 route_path('repo_files_diff',
659 660 repo_name=repo.name,
660 661 f_path='does-not-exist-in-any-commit'),
661 662 params={
662 663 'diff1': repo[0].raw_id,
663 664 'diff2': repo[1].raw_id
664 665 })
665 666
666 667 response = response.follow()
667 668 response.mustcontain('No files')
668 669
669 670 def test_returns_redirect_if_file_not_changed(self, backend):
670 671 commit = backend.repo.get_commit(commit_idx=-1)
671 672 response = self.app.get(
672 673 route_path('repo_files_diff_2way_redirect',
673 674 repo_name=backend.repo_name,
674 675 f_path='README'),
675 676 params={
676 677 'diff1': commit.raw_id,
677 678 'diff2': commit.raw_id,
678 679 })
679 680
680 681 response = response.follow()
681 682 response.mustcontain('No files')
682 683 response.mustcontain('No commits in this compare')
683 684
684 685 def test_supports_diff_to_different_path_svn(self, backend_svn):
685 686 #TODO: check this case
686 687 return
687 688
688 689 repo = backend_svn['svn-simple-layout'].scm_instance()
689 690 commit_id_1 = '24'
690 691 commit_id_2 = '26'
691 692
692 693 response = self.app.get(
693 694 route_path('repo_files_diff',
694 695 repo_name=backend_svn.repo_name,
695 696 f_path='trunk/example.py'),
696 697 params={
697 698 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
698 699 'diff2': commit_id_2,
699 700 })
700 701
701 702 response = response.follow()
702 703 response.mustcontain(
703 704 # diff contains this
704 705 "Will print out a useful message on invocation.")
705 706
706 707 # Note: Expecting that we indicate the user what's being compared
707 708 response.mustcontain("trunk/example.py")
708 709 response.mustcontain("tags/v0.2/example.py")
709 710
710 711 def test_show_rev_redirects_to_svn_path(self, backend_svn):
711 712 #TODO: check this case
712 713 return
713 714
714 715 repo = backend_svn['svn-simple-layout'].scm_instance()
715 716 commit_id = repo[-1].raw_id
716 717
717 718 response = self.app.get(
718 719 route_path('repo_files_diff',
719 720 repo_name=backend_svn.repo_name,
720 721 f_path='trunk/example.py'),
721 722 params={
722 723 'diff1': 'branches/argparse/example.py@' + commit_id,
723 724 'diff2': commit_id,
724 725 },
725 726 status=302)
726 727 response = response.follow()
727 728 assert response.headers['Location'].endswith(
728 729 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
729 730
730 731 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
731 732 #TODO: check this case
732 733 return
733 734
734 735 repo = backend_svn['svn-simple-layout'].scm_instance()
735 736 commit_id = repo[-1].raw_id
736 737 response = self.app.get(
737 738 route_path('repo_files_diff',
738 739 repo_name=backend_svn.repo_name,
739 740 f_path='trunk/example.py'),
740 741 params={
741 742 'diff1': 'branches/argparse/example.py@' + commit_id,
742 743 'diff2': commit_id,
743 744 'show_rev': 'Show at Revision',
744 745 'annotate': 'true',
745 746 },
746 747 status=302)
747 748 response = response.follow()
748 749 assert response.headers['Location'].endswith(
749 750 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
750 751
751 752
752 753 @pytest.mark.usefixtures("app", "autologin_user")
753 754 class TestModifyFilesWithWebInterface(object):
754 755
755 756 def test_add_file_view(self, backend):
756 757 self.app.get(
757 758 route_path('repo_files_add_file',
758 759 repo_name=backend.repo_name,
759 760 commit_id='tip', f_path='/')
760 761 )
761 762
762 763 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
763 764 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
764 repo = backend.create_repo()
765 backend.create_repo()
765 766 filename = 'init.py'
766 767 response = self.app.post(
767 768 route_path('repo_files_create_file',
768 769 repo_name=backend.repo_name,
769 770 commit_id='tip', f_path='/'),
770 771 params={
771 772 'content': "",
772 773 'filename': filename,
773 'location': "",
774 774 'csrf_token': csrf_token,
775 775 },
776 776 status=302)
777 assert_session_flash(response,
778 'Successfully committed new file `{}`'.format(
779 os.path.join(filename)))
777 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
778 assert_session_flash(response, expected_msg)
780 779
781 780 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
781 commit_id = backend.repo.get_commit().raw_id
782 782 response = self.app.post(
783 783 route_path('repo_files_create_file',
784 784 repo_name=backend.repo_name,
785 commit_id='tip', f_path='/'),
785 commit_id=commit_id, f_path='/'),
786 786 params={
787 787 'content': "foo",
788 788 'csrf_token': csrf_token,
789 789 },
790 790 status=302)
791 791
792 assert_session_flash(response, 'No filename')
792 assert_session_flash(response, 'No filename specified')
793 793
794 794 def test_add_file_into_repo_errors_and_no_commits(
795 795 self, backend, csrf_token):
796 796 repo = backend.create_repo()
797 797 # Create a file with no filename, it will display an error but
798 798 # the repo has no commits yet
799 799 response = self.app.post(
800 800 route_path('repo_files_create_file',
801 801 repo_name=repo.repo_name,
802 802 commit_id='tip', f_path='/'),
803 803 params={
804 804 'content': "foo",
805 805 'csrf_token': csrf_token,
806 806 },
807 807 status=302)
808 808
809 assert_session_flash(response, 'No filename')
809 assert_session_flash(response, 'No filename specified')
810 810
811 811 # Not allowed, redirect to the summary
812 812 redirected = response.follow()
813 813 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
814 814
815 815 # As there are no commits, displays the summary page with the error of
816 816 # creating a file with no filename
817 817
818 818 assert redirected.request.path == summary_url
819 819
820 @pytest.mark.parametrize("location, filename", [
821 ('/abs', 'foo'),
822 ('../rel', 'foo'),
823 ('file/../foo', 'foo'),
820 @pytest.mark.parametrize("filename, clean_filename", [
821 ('/abs/foo', 'abs/foo'),
822 ('../rel/foo', 'rel/foo'),
823 ('file/../foo/foo', 'file/foo/foo'),
824 824 ])
825 def test_add_file_into_repo_bad_filenames(
826 self, location, filename, backend, csrf_token):
825 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
826 repo = backend.create_repo()
827 commit_id = repo.get_commit().raw_id
828
827 829 response = self.app.post(
828 830 route_path('repo_files_create_file',
829 repo_name=backend.repo_name,
830 commit_id='tip', f_path='/'),
831 repo_name=repo.repo_name,
832 commit_id=commit_id, f_path='/'),
831 833 params={
832 834 'content': "foo",
833 835 'filename': filename,
834 'location': location,
835 836 'csrf_token': csrf_token,
836 837 },
837 838 status=302)
838 839
839 assert_session_flash(
840 response,
841 'The location specified must be a relative path and must not '
842 'contain .. in the path')
840 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
841 assert_session_flash(response, expected_msg)
843 842
844 @pytest.mark.parametrize("cnt, location, filename", [
845 (1, '', 'foo.txt'),
846 (2, 'dir', 'foo.rst'),
847 (3, 'rel/dir', 'foo.bar'),
843 @pytest.mark.parametrize("cnt, filename, content", [
844 (1, 'foo.txt', "Content"),
845 (2, 'dir/foo.rst', "Content"),
846 (3, 'dir/foo-second.rst', "Content"),
847 (4, 'rel/dir/foo.bar', "Content"),
848 848 ])
849 def test_add_file_into_repo(self, cnt, location, filename, backend,
850 csrf_token):
849 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
851 850 repo = backend.create_repo()
851 commit_id = repo.get_commit().raw_id
852 852 response = self.app.post(
853 853 route_path('repo_files_create_file',
854 854 repo_name=repo.repo_name,
855 commit_id='tip', f_path='/'),
855 commit_id=commit_id, f_path='/'),
856 856 params={
857 'content': "foo",
857 'content': content,
858 858 'filename': filename,
859 'location': location,
860 859 'csrf_token': csrf_token,
861 860 },
862 861 status=302)
863 assert_session_flash(response,
864 'Successfully committed new file `{}`'.format(
865 os.path.join(location, filename)))
862
863 expected_msg = 'Successfully committed new file `{}`'.format(filename)
864 assert_session_flash(response, expected_msg)
866 865
867 866 def test_edit_file_view(self, backend):
868 867 response = self.app.get(
869 868 route_path('repo_files_edit_file',
870 869 repo_name=backend.repo_name,
871 870 commit_id=backend.default_head_id,
872 871 f_path='vcs/nodes.py'),
873 872 status=200)
874 873 response.mustcontain("Module holding everything related to vcs nodes.")
875 874
876 875 def test_edit_file_view_not_on_branch(self, backend):
877 876 repo = backend.create_repo()
878 877 backend.ensure_file("vcs/nodes.py")
879 878
880 879 response = self.app.get(
881 880 route_path('repo_files_edit_file',
882 881 repo_name=repo.repo_name,
883 882 commit_id='tip',
884 883 f_path='vcs/nodes.py'),
885 884 status=302)
886 885 assert_session_flash(
887 response,
888 'You can only edit files with commit being a valid branch')
886 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
889 887
890 888 def test_edit_file_view_commit_changes(self, backend, csrf_token):
891 889 repo = backend.create_repo()
892 890 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
893 891
894 892 response = self.app.post(
895 893 route_path('repo_files_update_file',
896 894 repo_name=repo.repo_name,
897 895 commit_id=backend.default_head_id,
898 896 f_path='vcs/nodes.py'),
899 897 params={
900 898 'content': "print 'hello world'",
901 899 'message': 'I committed',
902 900 'filename': "vcs/nodes.py",
903 901 'csrf_token': csrf_token,
904 902 },
905 903 status=302)
906 904 assert_session_flash(
907 905 response, 'Successfully committed changes to file `vcs/nodes.py`')
908 906 tip = repo.get_commit(commit_idx=-1)
909 907 assert tip.message == 'I committed'
910 908
911 909 def test_edit_file_view_commit_changes_default_message(self, backend,
912 910 csrf_token):
913 911 repo = backend.create_repo()
914 912 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
915 913
916 914 commit_id = (
917 915 backend.default_branch_name or
918 916 backend.repo.scm_instance().commit_ids[-1])
919 917
920 918 response = self.app.post(
921 919 route_path('repo_files_update_file',
922 920 repo_name=repo.repo_name,
923 921 commit_id=commit_id,
924 922 f_path='vcs/nodes.py'),
925 923 params={
926 924 'content': "print 'hello world'",
927 925 'message': '',
928 926 'filename': "vcs/nodes.py",
929 927 'csrf_token': csrf_token,
930 928 },
931 929 status=302)
932 930 assert_session_flash(
933 931 response, 'Successfully committed changes to file `vcs/nodes.py`')
934 932 tip = repo.get_commit(commit_idx=-1)
935 933 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
936 934
937 935 def test_delete_file_view(self, backend):
938 936 self.app.get(
939 937 route_path('repo_files_remove_file',
940 938 repo_name=backend.repo_name,
941 939 commit_id=backend.default_head_id,
942 940 f_path='vcs/nodes.py'),
943 941 status=200)
944 942
945 943 def test_delete_file_view_not_on_branch(self, backend):
946 944 repo = backend.create_repo()
947 945 backend.ensure_file('vcs/nodes.py')
948 946
949 947 response = self.app.get(
950 948 route_path('repo_files_remove_file',
951 949 repo_name=repo.repo_name,
952 950 commit_id='tip',
953 951 f_path='vcs/nodes.py'),
954 952 status=302)
955 953 assert_session_flash(
956 response,
957 'You can only delete files with commit being a valid branch')
954 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
958 955
959 956 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 957 repo = backend.create_repo()
961 958 backend.ensure_file("vcs/nodes.py")
962 959
963 960 response = self.app.post(
964 961 route_path('repo_files_delete_file',
965 962 repo_name=repo.repo_name,
966 963 commit_id=backend.default_head_id,
967 964 f_path='vcs/nodes.py'),
968 965 params={
969 966 'message': 'i commited',
970 967 'csrf_token': csrf_token,
971 968 },
972 969 status=302)
973 970 assert_session_flash(
974 971 response, 'Successfully deleted file `vcs/nodes.py`')
975 972
976 973
977 974 @pytest.mark.usefixtures("app")
978 975 class TestFilesViewOtherCases(object):
979 976
980 977 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 978 self, backend_stub, autologin_regular_user, user_regular,
982 979 user_util):
983 980
984 981 repo = backend_stub.create_repo()
985 982 user_util.grant_user_permission_to_repo(
986 983 repo, user_regular, 'repository.write')
987 984 response = self.app.get(
988 985 route_path('repo_files',
989 986 repo_name=repo.repo_name,
990 987 commit_id='tip', f_path='/'))
991 988
992 989 repo_file_add_url = route_path(
993 990 'repo_files_add_file',
994 991 repo_name=repo.repo_name,
995 commit_id=0, f_path='') + '#edit'
992 commit_id=0, f_path='')
996 993
997 994 assert_session_flash(
998 995 response,
999 996 'There are no files yet. <a class="alert-link" '
1000 997 'href="{}">Click here to add a new file.</a>'
1001 998 .format(repo_file_add_url))
1002 999
1003 1000 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 1001 self, backend_stub, autologin_regular_user):
1005 1002 repo = backend_stub.create_repo()
1006 1003 # init session for anon user
1007 1004 route_path('repo_summary', repo_name=repo.repo_name)
1008 1005
1009 1006 repo_file_add_url = route_path(
1010 1007 'repo_files_add_file',
1011 1008 repo_name=repo.repo_name,
1012 commit_id=0, f_path='') + '#edit'
1009 commit_id=0, f_path='')
1013 1010
1014 1011 response = self.app.get(
1015 1012 route_path('repo_files',
1016 1013 repo_name=repo.repo_name,
1017 1014 commit_id='tip', f_path='/'))
1018 1015
1019 1016 assert_session_flash(response, no_=repo_file_add_url)
1020 1017
1021 1018 @pytest.mark.parametrize('file_node', [
1022 1019 'archive/file.zip',
1023 1020 'diff/my-file.txt',
1024 1021 'render.py',
1025 1022 'render',
1026 1023 'remove_file',
1027 1024 'remove_file/to-delete.txt',
1028 1025 ])
1029 1026 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 1027 backend.create_repo()
1031 1028 backend.ensure_file(file_node)
1032 1029
1033 1030 self.app.get(
1034 1031 route_path('repo_files',
1035 1032 repo_name=backend.repo_name,
1036 1033 commit_id='tip', f_path=file_node),
1037 1034 status=200)
1038 1035
1039 1036
1040 1037 class TestAdjustFilePathForSvn(object):
1041 1038 """
1042 1039 SVN specific adjustments of node history in RepoFilesView.
1043 1040 """
1044 1041
1045 1042 def test_returns_path_relative_to_matched_reference(self):
1046 1043 repo = self._repo(branches=['trunk'])
1047 1044 self.assert_file_adjustment('trunk/file', 'file', repo)
1048 1045
1049 1046 def test_does_not_modify_file_if_no_reference_matches(self):
1050 1047 repo = self._repo(branches=['trunk'])
1051 1048 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052 1049
1053 1050 def test_does_not_adjust_partial_directory_names(self):
1054 1051 repo = self._repo(branches=['trun'])
1055 1052 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056 1053
1057 1054 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 1055 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 1056 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060 1057
1061 1058 def assert_file_adjustment(self, f_path, expected, repo):
1062 1059 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 1060 assert result == expected
1064 1061
1065 1062 def _repo(self, branches=None):
1066 1063 repo = mock.Mock()
1067 1064 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 1065 repo.tags = {}
1069 1066 return repo
@@ -1,1530 +1,1526 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 28 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 branch_name, rule)
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_rev[1]
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError):
185 185 msg = _('No such commit exists for this repository')
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 log.warning('Repository error while fetching '
209 'filenode `%s`. Err:%s', path, e)
208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
210 209 h.flash(safe_str(h.escape(e)), category='error')
211 210 raise HTTPNotFound()
212 211
213 212 return file_node
214 213
215 214 def _is_valid_head(self, commit_id, repo):
216 215 branch_name = sha_commit_id = ''
217 216 is_head = False
218 log.debug('Checking if commit_id %s is a head.', commit_id)
219
220 if h.is_svn(repo) and not repo.is_empty():
221 # Note: Subversion only has one head.
222 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
223 is_head = True
224 return branch_name, sha_commit_id, is_head
217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
225 218
226 219 for _branch_name, branch_commit_id in repo.branches.items():
227 220 # simple case we pass in branch name, it's a HEAD
228 221 if commit_id == _branch_name:
229 222 is_head = True
230 223 branch_name = _branch_name
231 224 sha_commit_id = branch_commit_id
232 225 break
233 226 # case when we pass in full sha commit_id, which is a head
234 227 elif commit_id == branch_commit_id:
235 228 is_head = True
236 229 branch_name = _branch_name
237 230 sha_commit_id = branch_commit_id
238 231 break
239 232
233 if h.is_svn(repo) and not repo.is_empty():
234 # Note: Subversion only has one head.
235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 is_head = True
237 return branch_name, sha_commit_id, is_head
238
240 239 # checked branches, means we only need to try to get the branch/commit_sha
241 240 if not repo.is_empty():
242 241 commit = repo.get_commit(commit_id=commit_id)
243 242 if commit:
244 243 branch_name = commit.branch
245 244 sha_commit_id = commit.raw_id
246 245
247 246 return branch_name, sha_commit_id, is_head
248 247
249 248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
250 249
251 250 repo_id = self.db_repo.repo_id
252 251 force_recache = self.get_recache_flag()
253 252
254 253 cache_seconds = safe_int(
255 254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
256 255 cache_on = not force_recache and cache_seconds > 0
257 256 log.debug(
258 257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
259 258 'with caching: %s[TTL: %ss]' % (
260 259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
261 260
262 261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
263 262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
264 263
265 264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
266 265 condition=cache_on)
267 266 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
268 267 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
269 268 ver, repo_id, commit_id, f_path)
270 269
271 270 c.full_load = full_load
272 271 return render(
273 272 'rhodecode:templates/files/files_browser_tree.mako',
274 273 self._get_template_context(c), self.request)
275 274
276 275 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
277 276
278 277 def _get_archive_spec(self, fname):
279 278 log.debug('Detecting archive spec for: `%s`', fname)
280 279
281 280 fileformat = None
282 281 ext = None
283 282 content_type = None
284 283 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285 284
286 285 if fname.endswith(extension):
287 286 fileformat = a_type
288 287 log.debug('archive is of type: %s', fileformat)
289 288 ext = extension
290 289 break
291 290
292 291 if not fileformat:
293 292 raise ValueError()
294 293
295 294 # left over part of whole fname is the commit
296 295 commit_id = fname[:-len(ext)]
297 296
298 297 return commit_id, ext, fileformat, content_type
299 298
300 299 def create_pure_path(self, *parts):
301 300 # Split paths and sanitize them, removing any ../ etc
302 301 sanitized_path = [
303 302 x for x in pathlib2.PurePath(*parts).parts
304 303 if x not in ['.', '..']]
305 304
306 305 pure_path = pathlib2.PurePath(*sanitized_path)
307 306 return pure_path
308 307
309 308 @LoginRequired()
310 309 @HasRepoPermissionAnyDecorator(
311 310 'repository.read', 'repository.write', 'repository.admin')
312 311 @view_config(
313 312 route_name='repo_archivefile', request_method='GET',
314 313 renderer=None)
315 314 def repo_archivefile(self):
316 315 # archive cache config
317 316 from rhodecode import CONFIG
318 317 _ = self.request.translate
319 318 self.load_default_context()
320 319 default_at_path = '/'
321 320 fname = self.request.matchdict['fname']
322 321 subrepos = self.request.GET.get('subrepos') == 'true'
323 322 at_path = self.request.GET.get('at_path') or default_at_path
324 323
325 324 if not self.db_repo.enable_downloads:
326 325 return Response(_('Downloads disabled'))
327 326
328 327 try:
329 328 commit_id, ext, fileformat, content_type = \
330 329 self._get_archive_spec(fname)
331 330 except ValueError:
332 331 return Response(_('Unknown archive type for: `{}`').format(
333 332 h.escape(fname)))
334 333
335 334 try:
336 335 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
337 336 except CommitDoesNotExistError:
338 337 return Response(_('Unknown commit_id {}').format(
339 338 h.escape(commit_id)))
340 339 except EmptyRepositoryError:
341 340 return Response(_('Empty repository'))
342 341
343 342 try:
344 343 at_path = commit.get_node(at_path).path or default_at_path
345 344 except Exception:
346 345 return Response(_('No node at path {} for this repository').format(at_path))
347 346
348 347 path_sha = sha1(at_path)[:8]
349 348
350 349 # original backward compat name of archive
351 350 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
352 351 short_sha = safe_str(commit.short_id)
353 352
354 353 if at_path == default_at_path:
355 354 archive_name = '{}-{}{}{}'.format(
356 355 clean_name,
357 356 '-sub' if subrepos else '',
358 357 short_sha,
359 358 ext)
360 359 # custom path and new name
361 360 else:
362 361 archive_name = '{}-{}{}-{}{}'.format(
363 362 clean_name,
364 363 '-sub' if subrepos else '',
365 364 short_sha,
366 365 path_sha,
367 366 ext)
368 367
369 368 use_cached_archive = False
370 369 archive_cache_enabled = CONFIG.get(
371 370 'archive_cache_dir') and not self.request.GET.get('no_cache')
372 371 cached_archive_path = None
373 372
374 373 if archive_cache_enabled:
375 374 # check if we it's ok to write
376 375 if not os.path.isdir(CONFIG['archive_cache_dir']):
377 376 os.makedirs(CONFIG['archive_cache_dir'])
378 377 cached_archive_path = os.path.join(
379 378 CONFIG['archive_cache_dir'], archive_name)
380 379 if os.path.isfile(cached_archive_path):
381 380 log.debug('Found cached archive in %s', cached_archive_path)
382 381 fd, archive = None, cached_archive_path
383 382 use_cached_archive = True
384 383 else:
385 384 log.debug('Archive %s is not yet cached', archive_name)
386 385
387 386 if not use_cached_archive:
388 387 # generate new archive
389 388 fd, archive = tempfile.mkstemp()
390 389 log.debug('Creating new temp archive in %s', archive)
391 390 try:
392 391 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
393 392 archive_at_path=at_path)
394 393 except ImproperArchiveTypeError:
395 394 return _('Unknown archive type')
396 395 if archive_cache_enabled:
397 396 # if we generated the archive and we have cache enabled
398 397 # let's use this for future
399 398 log.debug('Storing new archive in %s', cached_archive_path)
400 399 shutil.move(archive, cached_archive_path)
401 400 archive = cached_archive_path
402 401
403 402 # store download action
404 403 audit_logger.store_web(
405 404 'repo.archive.download', action_data={
406 405 'user_agent': self.request.user_agent,
407 406 'archive_name': archive_name,
408 407 'archive_spec': fname,
409 408 'archive_cached': use_cached_archive},
410 409 user=self._rhodecode_user,
411 410 repo=self.db_repo,
412 411 commit=True
413 412 )
414 413
415 414 def get_chunked_archive(archive_path):
416 415 with open(archive_path, 'rb') as stream:
417 416 while True:
418 417 data = stream.read(16 * 1024)
419 418 if not data:
420 419 if fd: # fd means we used temporary file
421 420 os.close(fd)
422 421 if not archive_cache_enabled:
423 422 log.debug('Destroying temp archive %s', archive_path)
424 423 os.remove(archive_path)
425 424 break
426 425 yield data
427 426
428 427 response = Response(app_iter=get_chunked_archive(archive))
429 428 response.content_disposition = str(
430 429 'attachment; filename=%s' % archive_name)
431 430 response.content_type = str(content_type)
432 431
433 432 return response
434 433
435 434 def _get_file_node(self, commit_id, f_path):
436 435 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
437 436 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
438 437 try:
439 438 node = commit.get_node(f_path)
440 439 if node.is_dir():
441 440 raise NodeError('%s path is a %s not a file'
442 441 % (node, type(node)))
443 442 except NodeDoesNotExistError:
444 443 commit = EmptyCommit(
445 444 commit_id=commit_id,
446 445 idx=commit.idx,
447 446 repo=commit.repository,
448 447 alias=commit.repository.alias,
449 448 message=commit.message,
450 449 author=commit.author,
451 450 date=commit.date)
452 451 node = FileNode(f_path, '', commit=commit)
453 452 else:
454 453 commit = EmptyCommit(
455 454 repo=self.rhodecode_vcs_repo,
456 455 alias=self.rhodecode_vcs_repo.alias)
457 456 node = FileNode(f_path, '', commit=commit)
458 457 return node
459 458
460 459 @LoginRequired()
461 460 @HasRepoPermissionAnyDecorator(
462 461 'repository.read', 'repository.write', 'repository.admin')
463 462 @view_config(
464 463 route_name='repo_files_diff', request_method='GET',
465 464 renderer=None)
466 465 def repo_files_diff(self):
467 466 c = self.load_default_context()
468 467 f_path = self._get_f_path(self.request.matchdict)
469 468 diff1 = self.request.GET.get('diff1', '')
470 469 diff2 = self.request.GET.get('diff2', '')
471 470
472 471 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
473 472
474 473 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
475 474 line_context = self.request.GET.get('context', 3)
476 475
477 476 if not any((diff1, diff2)):
478 477 h.flash(
479 478 'Need query parameter "diff1" or "diff2" to generate a diff.',
480 479 category='error')
481 480 raise HTTPBadRequest()
482 481
483 482 c.action = self.request.GET.get('diff')
484 483 if c.action not in ['download', 'raw']:
485 484 compare_url = h.route_path(
486 485 'repo_compare',
487 486 repo_name=self.db_repo_name,
488 487 source_ref_type='rev',
489 488 source_ref=diff1,
490 489 target_repo=self.db_repo_name,
491 490 target_ref_type='rev',
492 491 target_ref=diff2,
493 492 _query=dict(f_path=f_path))
494 493 # redirect to new view if we render diff
495 494 raise HTTPFound(compare_url)
496 495
497 496 try:
498 497 node1 = self._get_file_node(diff1, path1)
499 498 node2 = self._get_file_node(diff2, f_path)
500 499 except (RepositoryError, NodeError):
501 500 log.exception("Exception while trying to get node from repository")
502 501 raise HTTPFound(
503 502 h.route_path('repo_files', repo_name=self.db_repo_name,
504 503 commit_id='tip', f_path=f_path))
505 504
506 505 if all(isinstance(node.commit, EmptyCommit)
507 506 for node in (node1, node2)):
508 507 raise HTTPNotFound()
509 508
510 509 c.commit_1 = node1.commit
511 510 c.commit_2 = node2.commit
512 511
513 512 if c.action == 'download':
514 513 _diff = diffs.get_gitdiff(node1, node2,
515 514 ignore_whitespace=ignore_whitespace,
516 515 context=line_context)
517 516 diff = diffs.DiffProcessor(_diff, format='gitdiff')
518 517
519 518 response = Response(self.path_filter.get_raw_patch(diff))
520 519 response.content_type = 'text/plain'
521 520 response.content_disposition = (
522 521 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
523 522 )
524 523 charset = self._get_default_encoding(c)
525 524 if charset:
526 525 response.charset = charset
527 526 return response
528 527
529 528 elif c.action == 'raw':
530 529 _diff = diffs.get_gitdiff(node1, node2,
531 530 ignore_whitespace=ignore_whitespace,
532 531 context=line_context)
533 532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
534 533
535 534 response = Response(self.path_filter.get_raw_patch(diff))
536 535 response.content_type = 'text/plain'
537 536 charset = self._get_default_encoding(c)
538 537 if charset:
539 538 response.charset = charset
540 539 return response
541 540
542 541 # in case we ever end up here
543 542 raise HTTPNotFound()
544 543
545 544 @LoginRequired()
546 545 @HasRepoPermissionAnyDecorator(
547 546 'repository.read', 'repository.write', 'repository.admin')
548 547 @view_config(
549 548 route_name='repo_files_diff_2way_redirect', request_method='GET',
550 549 renderer=None)
551 550 def repo_files_diff_2way_redirect(self):
552 551 """
553 552 Kept only to make OLD links work
554 553 """
555 554 f_path = self._get_f_path_unchecked(self.request.matchdict)
556 555 diff1 = self.request.GET.get('diff1', '')
557 556 diff2 = self.request.GET.get('diff2', '')
558 557
559 558 if not any((diff1, diff2)):
560 559 h.flash(
561 560 'Need query parameter "diff1" or "diff2" to generate a diff.',
562 561 category='error')
563 562 raise HTTPBadRequest()
564 563
565 564 compare_url = h.route_path(
566 565 'repo_compare',
567 566 repo_name=self.db_repo_name,
568 567 source_ref_type='rev',
569 568 source_ref=diff1,
570 569 target_ref_type='rev',
571 570 target_ref=diff2,
572 571 _query=dict(f_path=f_path, diffmode='sideside',
573 572 target_repo=self.db_repo_name,))
574 573 raise HTTPFound(compare_url)
575 574
576 575 @LoginRequired()
577 576 @HasRepoPermissionAnyDecorator(
578 577 'repository.read', 'repository.write', 'repository.admin')
579 578 @view_config(
580 579 route_name='repo_files', request_method='GET',
581 580 renderer=None)
582 581 @view_config(
583 582 route_name='repo_files:default_path', request_method='GET',
584 583 renderer=None)
585 584 @view_config(
586 585 route_name='repo_files:default_commit', request_method='GET',
587 586 renderer=None)
588 587 @view_config(
589 588 route_name='repo_files:rendered', request_method='GET',
590 589 renderer=None)
591 590 @view_config(
592 591 route_name='repo_files:annotated', request_method='GET',
593 592 renderer=None)
594 593 def repo_files(self):
595 594 c = self.load_default_context()
596 595
597 596 view_name = getattr(self.request.matched_route, 'name', None)
598 597
599 598 c.annotate = view_name == 'repo_files:annotated'
600 599 # default is false, but .rst/.md files later are auto rendered, we can
601 600 # overwrite auto rendering by setting this GET flag
602 601 c.renderer = view_name == 'repo_files:rendered' or \
603 602 not self.request.GET.get('no-render', False)
604 603
605 604 # redirect to given commit_id from form if given
606 605 get_commit_id = self.request.GET.get('at_rev', None)
607 606 if get_commit_id:
608 607 self._get_commit_or_redirect(get_commit_id)
609 608
610 609 commit_id, f_path = self._get_commit_and_path()
611 610 c.commit = self._get_commit_or_redirect(commit_id)
612 611 c.branch = self.request.GET.get('branch', None)
613 612 c.f_path = f_path
614 613
615 614 # prev link
616 615 try:
617 616 prev_commit = c.commit.prev(c.branch)
618 617 c.prev_commit = prev_commit
619 618 c.url_prev = h.route_path(
620 619 'repo_files', repo_name=self.db_repo_name,
621 620 commit_id=prev_commit.raw_id, f_path=f_path)
622 621 if c.branch:
623 622 c.url_prev += '?branch=%s' % c.branch
624 623 except (CommitDoesNotExistError, VCSError):
625 624 c.url_prev = '#'
626 625 c.prev_commit = EmptyCommit()
627 626
628 627 # next link
629 628 try:
630 629 next_commit = c.commit.next(c.branch)
631 630 c.next_commit = next_commit
632 631 c.url_next = h.route_path(
633 632 'repo_files', repo_name=self.db_repo_name,
634 633 commit_id=next_commit.raw_id, f_path=f_path)
635 634 if c.branch:
636 635 c.url_next += '?branch=%s' % c.branch
637 636 except (CommitDoesNotExistError, VCSError):
638 637 c.url_next = '#'
639 638 c.next_commit = EmptyCommit()
640 639
641 640 # files or dirs
642 641 try:
643 642 c.file = c.commit.get_node(f_path)
644 643 c.file_author = True
645 644 c.file_tree = ''
646 645
647 646 # load file content
648 647 if c.file.is_file():
649 648 c.lf_node = c.file.get_largefile_node()
650 649
651 650 c.file_source_page = 'true'
652 651 c.file_last_commit = c.file.last_commit
653 652 if c.file.size < c.visual.cut_off_limit_diff:
654 653 if c.annotate: # annotation has precedence over renderer
655 654 c.annotated_lines = filenode_as_annotated_lines_tokens(
656 655 c.file
657 656 )
658 657 else:
659 658 c.renderer = (
660 659 c.renderer and h.renderer_from_filename(c.file.path)
661 660 )
662 661 if not c.renderer:
663 662 c.lines = filenode_as_lines_tokens(c.file)
664 663
665 664 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
666 665 commit_id, self.rhodecode_vcs_repo)
667 666 c.on_branch_head = is_head
668 667
669 668 branch = c.commit.branch if (
670 669 c.commit.branch and '/' not in c.commit.branch) else None
671 670 c.branch_or_raw_id = branch or c.commit.raw_id
672 671 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
673 672
674 673 author = c.file_last_commit.author
675 674 c.authors = [[
676 675 h.email(author),
677 676 h.person(author, 'username_or_name_or_email'),
678 677 1
679 678 ]]
680 679
681 680 else: # load tree content at path
682 681 c.file_source_page = 'false'
683 682 c.authors = []
684 683 # this loads a simple tree without metadata to speed things up
685 684 # later via ajax we call repo_nodetree_full and fetch whole
686 685 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
687 686
688 687 except RepositoryError as e:
689 688 h.flash(safe_str(h.escape(e)), category='error')
690 689 raise HTTPNotFound()
691 690
692 691 if self.request.environ.get('HTTP_X_PJAX'):
693 692 html = render('rhodecode:templates/files/files_pjax.mako',
694 693 self._get_template_context(c), self.request)
695 694 else:
696 695 html = render('rhodecode:templates/files/files.mako',
697 696 self._get_template_context(c), self.request)
698 697 return Response(html)
699 698
700 699 @HasRepoPermissionAnyDecorator(
701 700 'repository.read', 'repository.write', 'repository.admin')
702 701 @view_config(
703 702 route_name='repo_files:annotated_previous', request_method='GET',
704 703 renderer=None)
705 704 def repo_files_annotated_previous(self):
706 705 self.load_default_context()
707 706
708 707 commit_id, f_path = self._get_commit_and_path()
709 708 commit = self._get_commit_or_redirect(commit_id)
710 709 prev_commit_id = commit.raw_id
711 710 line_anchor = self.request.GET.get('line_anchor')
712 711 is_file = False
713 712 try:
714 713 _file = commit.get_node(f_path)
715 714 is_file = _file.is_file()
716 715 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
717 716 pass
718 717
719 718 if is_file:
720 719 history = commit.get_path_history(f_path)
721 720 prev_commit_id = history[1].raw_id \
722 721 if len(history) > 1 else prev_commit_id
723 722 prev_url = h.route_path(
724 723 'repo_files:annotated', repo_name=self.db_repo_name,
725 724 commit_id=prev_commit_id, f_path=f_path,
726 725 _anchor='L{}'.format(line_anchor))
727 726
728 727 raise HTTPFound(prev_url)
729 728
730 729 @LoginRequired()
731 730 @HasRepoPermissionAnyDecorator(
732 731 'repository.read', 'repository.write', 'repository.admin')
733 732 @view_config(
734 733 route_name='repo_nodetree_full', request_method='GET',
735 734 renderer=None, xhr=True)
736 735 @view_config(
737 736 route_name='repo_nodetree_full:default_path', request_method='GET',
738 737 renderer=None, xhr=True)
739 738 def repo_nodetree_full(self):
740 739 """
741 740 Returns rendered html of file tree that contains commit date,
742 741 author, commit_id for the specified combination of
743 742 repo, commit_id and file path
744 743 """
745 744 c = self.load_default_context()
746 745
747 746 commit_id, f_path = self._get_commit_and_path()
748 747 commit = self._get_commit_or_redirect(commit_id)
749 748 try:
750 749 dir_node = commit.get_node(f_path)
751 750 except RepositoryError as e:
752 751 return Response('error: {}'.format(h.escape(safe_str(e))))
753 752
754 753 if dir_node.is_file():
755 754 return Response('')
756 755
757 756 c.file = dir_node
758 757 c.commit = commit
759 758
760 759 html = self._get_tree_at_commit(
761 760 c, commit.raw_id, dir_node.path, full_load=True)
762 761
763 762 return Response(html)
764 763
765 764 def _get_attachement_headers(self, f_path):
766 765 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
767 766 safe_path = f_name.replace('"', '\\"')
768 767 encoded_path = urllib.quote(f_name)
769 768
770 769 return "attachment; " \
771 770 "filename=\"{}\"; " \
772 771 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
773 772
774 773 @LoginRequired()
775 774 @HasRepoPermissionAnyDecorator(
776 775 'repository.read', 'repository.write', 'repository.admin')
777 776 @view_config(
778 777 route_name='repo_file_raw', request_method='GET',
779 778 renderer=None)
780 779 def repo_file_raw(self):
781 780 """
782 781 Action for show as raw, some mimetypes are "rendered",
783 782 those include images, icons.
784 783 """
785 784 c = self.load_default_context()
786 785
787 786 commit_id, f_path = self._get_commit_and_path()
788 787 commit = self._get_commit_or_redirect(commit_id)
789 788 file_node = self._get_filenode_or_redirect(commit, f_path)
790 789
791 790 raw_mimetype_mapping = {
792 791 # map original mimetype to a mimetype used for "show as raw"
793 792 # you can also provide a content-disposition to override the
794 793 # default "attachment" disposition.
795 794 # orig_type: (new_type, new_dispo)
796 795
797 796 # show images inline:
798 797 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
799 798 # for example render an SVG with javascript inside or even render
800 799 # HTML.
801 800 'image/x-icon': ('image/x-icon', 'inline'),
802 801 'image/png': ('image/png', 'inline'),
803 802 'image/gif': ('image/gif', 'inline'),
804 803 'image/jpeg': ('image/jpeg', 'inline'),
805 804 'application/pdf': ('application/pdf', 'inline'),
806 805 }
807 806
808 807 mimetype = file_node.mimetype
809 808 try:
810 809 mimetype, disposition = raw_mimetype_mapping[mimetype]
811 810 except KeyError:
812 811 # we don't know anything special about this, handle it safely
813 812 if file_node.is_binary:
814 813 # do same as download raw for binary files
815 814 mimetype, disposition = 'application/octet-stream', 'attachment'
816 815 else:
817 816 # do not just use the original mimetype, but force text/plain,
818 817 # otherwise it would serve text/html and that might be unsafe.
819 818 # Note: underlying vcs library fakes text/plain mimetype if the
820 819 # mimetype can not be determined and it thinks it is not
821 820 # binary.This might lead to erroneous text display in some
822 821 # cases, but helps in other cases, like with text files
823 822 # without extension.
824 823 mimetype, disposition = 'text/plain', 'inline'
825 824
826 825 if disposition == 'attachment':
827 826 disposition = self._get_attachement_headers(f_path)
828 827
829 828 def stream_node():
830 829 yield file_node.raw_bytes
831 830
832 831 response = Response(app_iter=stream_node())
833 832 response.content_disposition = disposition
834 833 response.content_type = mimetype
835 834
836 835 charset = self._get_default_encoding(c)
837 836 if charset:
838 837 response.charset = charset
839 838
840 839 return response
841 840
842 841 @LoginRequired()
843 842 @HasRepoPermissionAnyDecorator(
844 843 'repository.read', 'repository.write', 'repository.admin')
845 844 @view_config(
846 845 route_name='repo_file_download', request_method='GET',
847 846 renderer=None)
848 847 @view_config(
849 848 route_name='repo_file_download:legacy', request_method='GET',
850 849 renderer=None)
851 850 def repo_file_download(self):
852 851 c = self.load_default_context()
853 852
854 853 commit_id, f_path = self._get_commit_and_path()
855 854 commit = self._get_commit_or_redirect(commit_id)
856 855 file_node = self._get_filenode_or_redirect(commit, f_path)
857 856
858 857 if self.request.GET.get('lf'):
859 858 # only if lf get flag is passed, we download this file
860 859 # as LFS/Largefile
861 860 lf_node = file_node.get_largefile_node()
862 861 if lf_node:
863 862 # overwrite our pointer with the REAL large-file
864 863 file_node = lf_node
865 864
866 865 disposition = self._get_attachement_headers(f_path)
867 866
868 867 def stream_node():
869 868 yield file_node.raw_bytes
870 869
871 870 response = Response(app_iter=stream_node())
872 871 response.content_disposition = disposition
873 872 response.content_type = file_node.mimetype
874 873
875 874 charset = self._get_default_encoding(c)
876 875 if charset:
877 876 response.charset = charset
878 877
879 878 return response
880 879
881 880 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
882 881
883 882 cache_seconds = safe_int(
884 883 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
885 884 cache_on = cache_seconds > 0
886 885 log.debug(
887 886 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
888 887 'with caching: %s[TTL: %ss]' % (
889 888 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
890 889
891 890 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
892 891 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
893 892
894 893 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
895 894 condition=cache_on)
896 895 def compute_file_search(repo_id, commit_id, f_path):
897 896 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
898 897 repo_id, commit_id, f_path)
899 898 try:
900 899 _d, _f = ScmModel().get_nodes(
901 900 repo_name, commit_id, f_path, flat=False)
902 901 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
903 902 log.exception(safe_str(e))
904 903 h.flash(safe_str(h.escape(e)), category='error')
905 904 raise HTTPFound(h.route_path(
906 905 'repo_files', repo_name=self.db_repo_name,
907 906 commit_id='tip', f_path='/'))
908 907 return _d + _f
909 908
910 909 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
911 910
912 911 @LoginRequired()
913 912 @HasRepoPermissionAnyDecorator(
914 913 'repository.read', 'repository.write', 'repository.admin')
915 914 @view_config(
916 915 route_name='repo_files_nodelist', request_method='GET',
917 916 renderer='json_ext', xhr=True)
918 917 def repo_nodelist(self):
919 918 self.load_default_context()
920 919
921 920 commit_id, f_path = self._get_commit_and_path()
922 921 commit = self._get_commit_or_redirect(commit_id)
923 922
924 923 metadata = self._get_nodelist_at_commit(
925 924 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
926 925 return {'nodes': metadata}
927 926
928 927 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
929 928 items = []
930 929 for name, commit_id in branches_or_tags.items():
931 930 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
932 931 items.append((sym_ref, name, ref_type))
933 932 return items
934 933
935 934 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
936 935 return commit_id
937 936
938 937 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
939 938 new_f_path = vcspath.join(name, f_path)
940 939 return u'%s@%s' % (new_f_path, commit_id)
941 940
942 941 def _get_node_history(self, commit_obj, f_path, commits=None):
943 942 """
944 943 get commit history for given node
945 944
946 945 :param commit_obj: commit to calculate history
947 946 :param f_path: path for node to calculate history for
948 947 :param commits: if passed don't calculate history and take
949 948 commits defined in this list
950 949 """
951 950 _ = self.request.translate
952 951
953 952 # calculate history based on tip
954 953 tip = self.rhodecode_vcs_repo.get_commit()
955 954 if commits is None:
956 955 pre_load = ["author", "branch"]
957 956 try:
958 957 commits = tip.get_path_history(f_path, pre_load=pre_load)
959 958 except (NodeDoesNotExistError, CommitError):
960 959 # this node is not present at tip!
961 960 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
962 961
963 962 history = []
964 963 commits_group = ([], _("Changesets"))
965 964 for commit in commits:
966 965 branch = ' (%s)' % commit.branch if commit.branch else ''
967 966 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
968 967 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
969 968 history.append(commits_group)
970 969
971 970 symbolic_reference = self._symbolic_reference
972 971
973 972 if self.rhodecode_vcs_repo.alias == 'svn':
974 973 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
975 974 f_path, self.rhodecode_vcs_repo)
976 975 if adjusted_f_path != f_path:
977 976 log.debug(
978 977 'Recognized svn tag or branch in file "%s", using svn '
979 978 'specific symbolic references', f_path)
980 979 f_path = adjusted_f_path
981 980 symbolic_reference = self._symbolic_reference_svn
982 981
983 982 branches = self._create_references(
984 983 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
985 984 branches_group = (branches, _("Branches"))
986 985
987 986 tags = self._create_references(
988 987 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
989 988 tags_group = (tags, _("Tags"))
990 989
991 990 history.append(branches_group)
992 991 history.append(tags_group)
993 992
994 993 return history, commits
995 994
996 995 @LoginRequired()
997 996 @HasRepoPermissionAnyDecorator(
998 997 'repository.read', 'repository.write', 'repository.admin')
999 998 @view_config(
1000 999 route_name='repo_file_history', request_method='GET',
1001 1000 renderer='json_ext')
1002 1001 def repo_file_history(self):
1003 1002 self.load_default_context()
1004 1003
1005 1004 commit_id, f_path = self._get_commit_and_path()
1006 1005 commit = self._get_commit_or_redirect(commit_id)
1007 1006 file_node = self._get_filenode_or_redirect(commit, f_path)
1008 1007
1009 1008 if file_node.is_file():
1010 1009 file_history, _hist = self._get_node_history(commit, f_path)
1011 1010
1012 1011 res = []
1013 1012 for obj in file_history:
1014 1013 res.append({
1015 1014 'text': obj[1],
1016 1015 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
1017 1016 })
1018 1017
1019 1018 data = {
1020 1019 'more': False,
1021 1020 'results': res
1022 1021 }
1023 1022 return data
1024 1023
1025 1024 log.warning('Cannot fetch history for directory')
1026 1025 raise HTTPBadRequest()
1027 1026
1028 1027 @LoginRequired()
1029 1028 @HasRepoPermissionAnyDecorator(
1030 1029 'repository.read', 'repository.write', 'repository.admin')
1031 1030 @view_config(
1032 1031 route_name='repo_file_authors', request_method='GET',
1033 1032 renderer='rhodecode:templates/files/file_authors_box.mako')
1034 1033 def repo_file_authors(self):
1035 1034 c = self.load_default_context()
1036 1035
1037 1036 commit_id, f_path = self._get_commit_and_path()
1038 1037 commit = self._get_commit_or_redirect(commit_id)
1039 1038 file_node = self._get_filenode_or_redirect(commit, f_path)
1040 1039
1041 1040 if not file_node.is_file():
1042 1041 raise HTTPBadRequest()
1043 1042
1044 1043 c.file_last_commit = file_node.last_commit
1045 1044 if self.request.GET.get('annotate') == '1':
1046 1045 # use _hist from annotation if annotation mode is on
1047 1046 commit_ids = set(x[1] for x in file_node.annotate)
1048 1047 _hist = (
1049 1048 self.rhodecode_vcs_repo.get_commit(commit_id)
1050 1049 for commit_id in commit_ids)
1051 1050 else:
1052 1051 _f_history, _hist = self._get_node_history(commit, f_path)
1053 1052 c.file_author = False
1054 1053
1055 1054 unique = collections.OrderedDict()
1056 1055 for commit in _hist:
1057 1056 author = commit.author
1058 1057 if author not in unique:
1059 1058 unique[commit.author] = [
1060 1059 h.email(author),
1061 1060 h.person(author, 'username_or_name_or_email'),
1062 1061 1 # counter
1063 1062 ]
1064 1063
1065 1064 else:
1066 1065 # increase counter
1067 1066 unique[commit.author][2] += 1
1068 1067
1069 1068 c.authors = [val for val in unique.values()]
1070 1069
1071 1070 return self._get_template_context(c)
1072 1071
1073 1072 @LoginRequired()
1074 1073 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1075 1074 @view_config(
1076 1075 route_name='repo_files_remove_file', request_method='GET',
1077 1076 renderer='rhodecode:templates/files/files_delete.mako')
1078 1077 def repo_files_remove_file(self):
1079 1078 _ = self.request.translate
1080 1079 c = self.load_default_context()
1081 1080 commit_id, f_path = self._get_commit_and_path()
1082 1081
1083 1082 self._ensure_not_locked()
1084 1083 _branch_name, _sha_commit_id, is_head = \
1085 1084 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1086 1085
1087 1086 self.forbid_non_head(is_head, f_path)
1088 1087 self.check_branch_permission(_branch_name)
1089 1088
1090 1089 c.commit = self._get_commit_or_redirect(commit_id)
1091 1090 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1092 1091
1093 1092 c.default_message = _(
1094 1093 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1095 1094 c.f_path = f_path
1096 1095
1097 1096 return self._get_template_context(c)
1098 1097
1099 1098 @LoginRequired()
1100 1099 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1101 1100 @CSRFRequired()
1102 1101 @view_config(
1103 1102 route_name='repo_files_delete_file', request_method='POST',
1104 1103 renderer=None)
1105 1104 def repo_files_delete_file(self):
1106 1105 _ = self.request.translate
1107 1106
1108 1107 c = self.load_default_context()
1109 1108 commit_id, f_path = self._get_commit_and_path()
1110 1109
1111 1110 self._ensure_not_locked()
1112 1111 _branch_name, _sha_commit_id, is_head = \
1113 1112 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1114 1113
1115 1114 self.forbid_non_head(is_head, f_path)
1116 1115 self.check_branch_permission(_branch_name)
1117 1116
1118 1117 c.commit = self._get_commit_or_redirect(commit_id)
1119 1118 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1120 1119
1121 1120 c.default_message = _(
1122 1121 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1123 1122 c.f_path = f_path
1124 1123 node_path = f_path
1125 1124 author = self._rhodecode_db_user.full_contact
1126 1125 message = self.request.POST.get('message') or c.default_message
1127 1126 try:
1128 1127 nodes = {
1129 1128 node_path: {
1130 1129 'content': ''
1131 1130 }
1132 1131 }
1133 1132 ScmModel().delete_nodes(
1134 1133 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1135 1134 message=message,
1136 1135 nodes=nodes,
1137 1136 parent_commit=c.commit,
1138 1137 author=author,
1139 1138 )
1140 1139
1141 1140 h.flash(
1142 1141 _('Successfully deleted file `{}`').format(
1143 1142 h.escape(f_path)), category='success')
1144 1143 except Exception:
1145 1144 log.exception('Error during commit operation')
1146 1145 h.flash(_('Error occurred during commit'), category='error')
1147 1146 raise HTTPFound(
1148 1147 h.route_path('repo_commit', repo_name=self.db_repo_name,
1149 1148 commit_id='tip'))
1150 1149
1151 1150 @LoginRequired()
1152 1151 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1153 1152 @view_config(
1154 1153 route_name='repo_files_edit_file', request_method='GET',
1155 1154 renderer='rhodecode:templates/files/files_edit.mako')
1156 1155 def repo_files_edit_file(self):
1157 1156 _ = self.request.translate
1158 1157 c = self.load_default_context()
1159 1158 commit_id, f_path = self._get_commit_and_path()
1160 1159
1161 1160 self._ensure_not_locked()
1162 1161 _branch_name, _sha_commit_id, is_head = \
1163 1162 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1164 1163
1165 1164 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1166 1165 self.check_branch_permission(_branch_name, commit_id=commit_id)
1167 1166
1168 1167 c.commit = self._get_commit_or_redirect(commit_id)
1169 1168 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1170 1169
1171 1170 if c.file.is_binary:
1172 1171 files_url = h.route_path(
1173 1172 'repo_files',
1174 1173 repo_name=self.db_repo_name,
1175 1174 commit_id=c.commit.raw_id, f_path=f_path)
1176 1175 raise HTTPFound(files_url)
1177 1176
1178 1177 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1179 1178 c.f_path = f_path
1180 1179
1181 1180 return self._get_template_context(c)
1182 1181
1183 1182 @LoginRequired()
1184 1183 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1185 1184 @CSRFRequired()
1186 1185 @view_config(
1187 1186 route_name='repo_files_update_file', request_method='POST',
1188 1187 renderer=None)
1189 1188 def repo_files_update_file(self):
1190 1189 _ = self.request.translate
1191 1190 c = self.load_default_context()
1192 1191 commit_id, f_path = self._get_commit_and_path()
1193 1192
1194 1193 self._ensure_not_locked()
1195 1194
1196 1195 c.commit = self._get_commit_or_redirect(commit_id)
1197 1196 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1198 1197
1199 1198 if c.file.is_binary:
1200 1199 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1201 1200 commit_id=c.commit.raw_id, f_path=f_path))
1202 1201
1203 1202 _branch_name, _sha_commit_id, is_head = \
1204 1203 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1205 1204
1206 1205 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1207 1206 self.check_branch_permission(_branch_name, commit_id=commit_id)
1208 1207
1209 1208 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1210 1209 c.f_path = f_path
1211 1210
1212 1211 old_content = c.file.content
1213 1212 sl = old_content.splitlines(1)
1214 1213 first_line = sl[0] if sl else ''
1215 1214
1216 1215 r_post = self.request.POST
1217 1216 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1218 1217 line_ending_mode = detect_mode(first_line, 0)
1219 1218 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1220 1219
1221 1220 message = r_post.get('message') or c.default_message
1222 1221 org_node_path = c.file.unicode_path
1223 1222 filename = r_post['filename']
1224 1223
1225 1224 root_path = c.file.dir_path
1226 1225 pure_path = self.create_pure_path(root_path, filename)
1227 1226 node_path = safe_unicode(bytes(pure_path))
1228 1227
1229 1228 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1230 1229 commit_id=commit_id)
1231 1230 if content == old_content and node_path == org_node_path:
1232 1231 h.flash(_('No changes detected on {}').format(org_node_path),
1233 1232 category='warning')
1234 1233 raise HTTPFound(default_redirect_url)
1235 1234
1236 1235 try:
1237 1236 mapping = {
1238 1237 org_node_path: {
1239 1238 'org_filename': org_node_path,
1240 1239 'filename': node_path,
1241 1240 'content': content,
1242 1241 'lexer': '',
1243 1242 'op': 'mod',
1244 1243 'mode': c.file.mode
1245 1244 }
1246 1245 }
1247 1246
1248 1247 commit = ScmModel().update_nodes(
1249 1248 user=self._rhodecode_db_user.user_id,
1250 1249 repo=self.db_repo,
1251 1250 message=message,
1252 1251 nodes=mapping,
1253 1252 parent_commit=c.commit,
1254 1253 )
1255 1254
1256 1255 h.flash(_('Successfully committed changes to file `{}`').format(
1257 1256 h.escape(f_path)), category='success')
1258 1257 default_redirect_url = h.route_path(
1259 1258 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1260 1259
1261 1260 except Exception:
1262 1261 log.exception('Error occurred during commit')
1263 1262 h.flash(_('Error occurred during commit'), category='error')
1264 1263
1265 1264 raise HTTPFound(default_redirect_url)
1266 1265
1267 1266 @LoginRequired()
1268 1267 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1269 1268 @view_config(
1270 1269 route_name='repo_files_add_file', request_method='GET',
1271 1270 renderer='rhodecode:templates/files/files_add.mako')
1272 1271 @view_config(
1273 1272 route_name='repo_files_upload_file', request_method='GET',
1274 1273 renderer='rhodecode:templates/files/files_upload.mako')
1275 1274 def repo_files_add_file(self):
1276 1275 _ = self.request.translate
1277 1276 c = self.load_default_context()
1278 1277 commit_id, f_path = self._get_commit_and_path()
1279 1278
1280 1279 self._ensure_not_locked()
1281 1280
1282 1281 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1283 1282 if c.commit is None:
1284 1283 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1285 1284
1286 1285 if self.rhodecode_vcs_repo.is_empty():
1287 1286 # for empty repository we cannot check for current branch, we rely on
1288 1287 # c.commit.branch instead
1289 _branch_name = c.commit.branch
1290 is_head = True
1288 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1291 1289 else:
1292 1290 _branch_name, _sha_commit_id, is_head = \
1293 1291 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1294 1292
1295 1293 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1296 1294 self.check_branch_permission(_branch_name, commit_id=commit_id)
1297 1295
1298 1296 c.default_message = (_('Added file via RhodeCode Enterprise'))
1299 1297 c.f_path = f_path.lstrip('/') # ensure not relative path
1300 1298
1301 1299 return self._get_template_context(c)
1302 1300
1303 1301 @LoginRequired()
1304 1302 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1305 1303 @CSRFRequired()
1306 1304 @view_config(
1307 1305 route_name='repo_files_create_file', request_method='POST',
1308 1306 renderer=None)
1309 1307 def repo_files_create_file(self):
1310 1308 _ = self.request.translate
1311 1309 c = self.load_default_context()
1312 1310 commit_id, f_path = self._get_commit_and_path()
1313 1311
1314 1312 self._ensure_not_locked()
1315 1313
1316 1314 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1317 1315 if c.commit is None:
1318 1316 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1319 1317
1320 1318 # calculate redirect URL
1321 1319 if self.rhodecode_vcs_repo.is_empty():
1322 1320 default_redirect_url = h.route_path(
1323 1321 'repo_summary', repo_name=self.db_repo_name)
1324 1322 else:
1325 1323 default_redirect_url = h.route_path(
1326 1324 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1327 1325
1328 1326 if self.rhodecode_vcs_repo.is_empty():
1329 1327 # for empty repository we cannot check for current branch, we rely on
1330 1328 # c.commit.branch instead
1331 _branch_name = c.commit.branch
1332 is_head = True
1329 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1333 1330 else:
1334 1331 _branch_name, _sha_commit_id, is_head = \
1335 1332 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1336 1333
1337 1334 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1338 1335 self.check_branch_permission(_branch_name, commit_id=commit_id)
1339 1336
1340 1337 c.default_message = (_('Added file via RhodeCode Enterprise'))
1341 1338 c.f_path = f_path
1342 1339
1343 1340 r_post = self.request.POST
1344 1341 message = r_post.get('message') or c.default_message
1345 1342 filename = r_post.get('filename')
1346 1343 unix_mode = 0
1347 1344 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1348 1345
1349 1346 if not filename:
1350 1347 # If there's no commit, redirect to repo summary
1351 1348 if type(c.commit) is EmptyCommit:
1352 1349 redirect_url = h.route_path(
1353 1350 'repo_summary', repo_name=self.db_repo_name)
1354 1351 else:
1355 1352 redirect_url = default_redirect_url
1356 1353 h.flash(_('No filename specified'), category='warning')
1357 1354 raise HTTPFound(redirect_url)
1358 1355
1359 1356 root_path = f_path
1360 1357 pure_path = self.create_pure_path(root_path, filename)
1361 1358 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1362 1359
1363 1360 author = self._rhodecode_db_user.full_contact
1364 1361 nodes = {
1365 1362 node_path: {
1366 1363 'content': content
1367 1364 }
1368 1365 }
1369 1366
1370 1367 try:
1371 1368
1372 1369 commit = ScmModel().create_nodes(
1373 1370 user=self._rhodecode_db_user.user_id,
1374 1371 repo=self.db_repo,
1375 1372 message=message,
1376 1373 nodes=nodes,
1377 1374 parent_commit=c.commit,
1378 1375 author=author,
1379 1376 )
1380 1377
1381 1378 h.flash(_('Successfully committed new file `{}`').format(
1382 1379 h.escape(node_path)), category='success')
1383 1380
1384 1381 default_redirect_url = h.route_path(
1385 1382 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1386 1383
1387 1384 except NonRelativePathError:
1388 1385 log.exception('Non Relative path found')
1389 1386 h.flash(_('The location specified must be a relative path and must not '
1390 1387 'contain .. in the path'), category='warning')
1391 1388 raise HTTPFound(default_redirect_url)
1392 1389 except (NodeError, NodeAlreadyExistsError) as e:
1393 1390 h.flash(_(h.escape(e)), category='error')
1394 1391 except Exception:
1395 1392 log.exception('Error occurred during commit')
1396 1393 h.flash(_('Error occurred during commit'), category='error')
1397 1394
1398 1395 raise HTTPFound(default_redirect_url)
1399 1396
1400 1397 @LoginRequired()
1401 1398 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1402 1399 @CSRFRequired()
1403 1400 @view_config(
1404 1401 route_name='repo_files_upload_file', request_method='POST',
1405 1402 renderer='json_ext')
1406 1403 def repo_files_upload_file(self):
1407 1404 _ = self.request.translate
1408 1405 c = self.load_default_context()
1409 1406 commit_id, f_path = self._get_commit_and_path()
1410 1407
1411 1408 self._ensure_not_locked()
1412 1409
1413 1410 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1414 1411 if c.commit is None:
1415 1412 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1416 1413
1417 1414 # calculate redirect URL
1418 1415 if self.rhodecode_vcs_repo.is_empty():
1419 1416 default_redirect_url = h.route_path(
1420 1417 'repo_summary', repo_name=self.db_repo_name)
1421 1418 else:
1422 1419 default_redirect_url = h.route_path(
1423 1420 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1424 1421
1425 1422 if self.rhodecode_vcs_repo.is_empty():
1426 1423 # for empty repository we cannot check for current branch, we rely on
1427 1424 # c.commit.branch instead
1428 _branch_name = c.commit.branch
1429 is_head = True
1425 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1430 1426 else:
1431 1427 _branch_name, _sha_commit_id, is_head = \
1432 1428 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1433 1429
1434 1430 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1435 1431 if error:
1436 1432 return {
1437 1433 'error': error,
1438 1434 'redirect_url': default_redirect_url
1439 1435 }
1440 1436 error = self.check_branch_permission(_branch_name, json_mode=True)
1441 1437 if error:
1442 1438 return {
1443 1439 'error': error,
1444 1440 'redirect_url': default_redirect_url
1445 1441 }
1446 1442
1447 1443 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1448 1444 c.f_path = f_path
1449 1445
1450 1446 r_post = self.request.POST
1451 1447
1452 1448 message = c.default_message
1453 1449 user_message = r_post.getall('message')
1454 1450 if isinstance(user_message, list) and user_message:
1455 1451 # we take the first from duplicated results if it's not empty
1456 1452 message = user_message[0] if user_message[0] else message
1457 1453
1458 1454 nodes = {}
1459 1455
1460 1456 for file_obj in r_post.getall('files_upload') or []:
1461 1457 content = file_obj.file
1462 1458 filename = file_obj.filename
1463 1459
1464 1460 root_path = f_path
1465 1461 pure_path = self.create_pure_path(root_path, filename)
1466 1462 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1467 1463
1468 1464 nodes[node_path] = {
1469 1465 'content': content
1470 1466 }
1471 1467
1472 1468 if not nodes:
1473 1469 error = 'missing files'
1474 1470 return {
1475 1471 'error': error,
1476 1472 'redirect_url': default_redirect_url
1477 1473 }
1478 1474
1479 1475 author = self._rhodecode_db_user.full_contact
1480 1476
1481 1477 try:
1482 1478 commit = ScmModel().create_nodes(
1483 1479 user=self._rhodecode_db_user.user_id,
1484 1480 repo=self.db_repo,
1485 1481 message=message,
1486 1482 nodes=nodes,
1487 1483 parent_commit=c.commit,
1488 1484 author=author,
1489 1485 )
1490 1486 if len(nodes) == 1:
1491 1487 flash_message = _('Successfully committed {} new files').format(len(nodes))
1492 1488 else:
1493 1489 flash_message = _('Successfully committed 1 new file')
1494 1490
1495 1491 h.flash(flash_message, category='success')
1496 1492
1497 1493 default_redirect_url = h.route_path(
1498 1494 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1499 1495
1500 1496 except NonRelativePathError:
1501 1497 log.exception('Non Relative path found')
1502 1498 error = _('The location specified must be a relative path and must not '
1503 1499 'contain .. in the path')
1504 1500 h.flash(error, category='warning')
1505 1501
1506 1502 return {
1507 1503 'error': error,
1508 1504 'redirect_url': default_redirect_url
1509 1505 }
1510 1506 except (NodeError, NodeAlreadyExistsError) as e:
1511 1507 error = h.escape(e)
1512 1508 h.flash(error, category='error')
1513 1509
1514 1510 return {
1515 1511 'error': error,
1516 1512 'redirect_url': default_redirect_url
1517 1513 }
1518 1514 except Exception:
1519 1515 log.exception('Error occurred during commit')
1520 1516 error = _('Error occurred during commit')
1521 1517 h.flash(error, category='error')
1522 1518 return {
1523 1519 'error': error,
1524 1520 'redirect_url': default_redirect_url
1525 1521 }
1526 1522
1527 1523 return {
1528 1524 'error': None,
1529 1525 'redirect_url': default_redirect_url
1530 1526 }
@@ -1,1 +1,1 b''
1 {"results": [{"text": "Changesets", "children": [{"text": "r625:ead8f28a4bc2", "id": "ead8f28a4bc2f45ecfb148a6b8a89758b9654a84"}, {"text": "r535:c093f94d6d35", "id": "c093f94d6d358f13c55a687da66c30c41cca4153"}, {"text": "r534:559f640ec08b", "id": "559f640ec08b2a14c4a9ac863d8ca273545b8885"}, {"text": "r490:02a940b4ee37", "id": "02a940b4ee371ec64ef5b4c4870a5c89dc7fb98a"}, {"text": "r464:b45a4153a2d7", "id": "b45a4153a2d7adb8a78b63d35d39fac44a4320a6"}, {"text": "r460:0a54e66b9450", "id": "0a54e66b94502409074b163cd93c1233dcc0413f"}, {"text": "r457:a7bf2f6bf3d5", "id": "a7bf2f6bf3d5273da4bcd2032a891acae5a45e2b"}, {"text": "r456:7266de0154b4", "id": "7266de0154b4da7c42ba3d788876056dbf116b5a"}, {"text": "r455:666de4ee6507", "id": "666de4ee65074cd3e37ea01e75f65bd3e4c336bb"}, {"text": "r453:91acc599141c", "id": "91acc599141c87f03e0e3551dcaacf4492632e58"}, {"text": "r442:40a2d5d71b75", "id": "40a2d5d71b758e7eafc84a324ed55142cba22f42"}, {"text": "r440:d1f898326327", "id": "d1f898326327e20524fe22417c22d71064fe54a1"}, {"text": "r420:162a36830c23", "id": "162a36830c23ccf1bf1873157fd0c8d0dfc7c817"}, {"text": "r345:c994f0de03b2", "id": "c994f0de03b2a0aa848a04fc2c0d7e737dba31fc"}, {"text": "r340:5d3d4d2c262e", "id": "5d3d4d2c262e17b247d405feceeb09ff7408c940"}, {"text": "r334:4d4278a6390e", "id": "4d4278a6390e42f4fc777ecf1b9b628e77da8e22"}, {"text": "r298:00dffb625166", "id": "00dffb62516650bc5050d818eb47ea1ca207954d"}, {"text": "r297:47b6be9a812e", "id": "47b6be9a812ec3ed0384001a458a759f0f583fe2"}, {"text": "r289:1589fed841cd", "id": "1589fed841cd9ef33155f8560727809ac3ada2c8"}, {"text": "r285:afafd0ee2821", "id": "afafd0ee28218ab979678213cb96e9e4dbd7359b"}, {"text": "r284:639b115ed2b0", "id": "639b115ed2b02017824005b5ae66282c6e25eba8"}, {"text": "r283:fcf7562d7305", "id": "fcf7562d7305affc94fe20dc89a34aefd2b8aa1e"}, {"text": "r256:ec8cbdb5f364", "id": "ec8cbdb5f364fce7843cbf148c3d95d86f935339"}, {"text": "r255:0d74d2e2bdf3", "id": "0d74d2e2bdf3dcd5ee9fe4fcfe9016c5c6486f35"}, {"text": "r243:6894ad7d8223", "id": "6894ad7d8223b1e6853e9fdaa2c38d3f0cef1e38"}, {"text": "r231:31b3f4b599fa", "id": "31b3f4b599fae5f12cf438c73403679cdf923d75"}, {"text": "r220:3d2515dd21fb", "id": "3d2515dd21fb34fe6c5d0029075a863f3e92f5f6"}, {"text": "r186:f804e27aa496", "id": "f804e27aa4961f2e327f2a10ee373235df20ee21"}, {"text": "r182:7f00513785a1", "id": "7f00513785a13f273a4387ef086bb795b37f013c"}, {"text": "r181:6efcdc61028c", "id": "6efcdc61028c8edd1c787b3439fae71b77a17357"}, {"text": "r175:6c0ce52b229a", "id": "6c0ce52b229aa978889e91b38777f800e85f330b"}, {"text": "r165:09788a0b8a54", "id": "09788a0b8a5455e9678c3959214246574e546d4f"}, {"text": "r163:0164ee729def", "id": "0164ee729def0a253d6dcb594b5ee2a52fef4748"}, {"text": "r140:33fa32233551", "id": "33fa3223355104431402a888fa77a4e9956feb3e"}, {"text": "r126:fa014c12c26d", "id": "fa014c12c26d10ba682fadb78f2a11c24c8118e1"}, {"text": "r111:e686b958768e", "id": "e686b958768ee96af8029fe19c6050b1a8dd3b2b"}, {"text": "r109:ab5721ca0a08", "id": "ab5721ca0a081f26bf43d9051e615af2cc99952f"}, {"text": "r108:c877b68d18e7", "id": "c877b68d18e792a66b7f4c529ea02c8f80801542"}, {"text": "r107:4313566d2e41", "id": "4313566d2e417cb382948f8d9d7c765330356054"}, {"text": "r104:6c2303a79367", "id": "6c2303a793671e807d1cfc70134c9ca0767d98c2"}, {"text": "r102:54386793436c", "id": "54386793436c938cff89326944d4c2702340037d"}, {"text": "r101:54000345d2e7", "id": "54000345d2e78b03a99d561399e8e548de3f3203"}, {"text": "r99:1c6b3677b37e", "id": "1c6b3677b37ea064cb4b51714d8f7498f93f4b2b"}, {"text": "r93:2d03ca750a44", "id": "2d03ca750a44440fb5ea8b751176d1f36f8e8f46"}, {"text": "r92:2a08b128c206", "id": "2a08b128c206db48c2f0b8f70df060e6db0ae4f8"}, {"text": "r91:30c26513ff1e", "id": "30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b"}, {"text": "r82:ac71e9503c2c", "id": "ac71e9503c2ca95542839af0ce7b64011b72ea7c"}, {"text": "r81:12669288fd13", "id": "12669288fd13adba2a9b7dd5b870cc23ffab92d2"}, {"text": "r76:5a0c84f3e6fe", "id": "5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382"}, {"text": "r73:12f2f5e2b38e", "id": "12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5"}, {"text": "r61:5eab1222a7cd", "id": "5eab1222a7cd4bfcbabc218ca6d04276d4e27378"}, {"text": "r60:f50f42baeed5", "id": "f50f42baeed5af6518ef4b0cb2f1423f3851a941"}, {"text": "r59:d7e390a45f6a", "id": "d7e390a45f6aa96f04f5e7f583ad4f867431aa25"}, {"text": "r58:f15c21f97864", "id": "f15c21f97864b4f071cddfbf2750ec2e23859414"}, {"text": "r57:e906ef056cf5", "id": "e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade"}, {"text": "r56:ea2b108b48aa", "id": "ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b"}, {"text": "r50:84dec09632a4", "id": "84dec09632a4458f79f50ddbbd155506c460b4f9"}, {"text": "r48:0115510b70c7", "id": "0115510b70c7229dbc5dc49036b32e7d91d23acd"}, {"text": "r46:2a13f185e452", "id": "2a13f185e4525f9d4b59882791a2d397b90d5ddc"}, {"text": "r30:3bf1c5868e57", "id": "3bf1c5868e570e39569d094f922d33ced2fa3b2b"}, {"text": "r26:b8d040125747", "id": "b8d04012574729d2c29886e53b1a43ef16dd00a1"}, {"text": "r24:6970b057cffe", "id": "6970b057cffe4aab0a792aa634c89f4bebf01441"}, {"text": "r8:dd80b0f6cf50", "id": "dd80b0f6cf5052f17cc738c2951c4f2070200d7f"}, {"text": "r7:ff7ca51e58c5", "id": "ff7ca51e58c505fec0dd2491de52c622bb7a806b"}]}, {"text": "Branches", "children": [{"text": "master", "id": "fd627b9e0dd80b47be81af07c4a98518244ed2f7"}]}, {"text": "Tags", "children": [{"text": "v0.2.2", "id": "137fea89f304a42321d40488091ee2ed419a3686"}, {"text": "v0.2.1", "id": "5051d0fa344d4408a2659d9a0348eb2d41868ecf"}, {"text": "v0.2.0", "id": "599ba911aa24d2981225f3966eb659dfae9e9f30"}, {"text": "v0.1.9", "id": "341d28f0eec5ddf0b6b77871e13c2bbd6bec685c"}, {"text": "v0.1.8", "id": "74ebce002c088b8a5ecf40073db09375515ecd68"}, {"text": "v0.1.7", "id": "4d78bf73b5c22c82b68f902f138f7881b4fffa2c"}, {"text": "v0.1.6", "id": "0205cb3f44223fb3099d12a77a69c81b798772d9"}, {"text": "v0.1.5", "id": "6c0ce52b229aa978889e91b38777f800e85f330b"}, {"text": "v0.1.4", "id": "7d735150934cd7645ac3051903add952390324a5"}, {"text": "v0.1.3", "id": "5a3a8fb005554692b16e21dee62bf02667d8dc3e"}, {"text": "v0.1.2", "id": "0ba5f8a4660034ff25c0cac2a5baabf5d2791d63"}, {"text": "v0.1.11", "id": "c60f01b77c42dce653d6b1d3b04689862c261929"}, {"text": "v0.1.10", "id": "10cddef6b794696066fb346434014f0a56810218"}, {"text": "v0.1.1", "id": "e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0"}]}], "more": false}
1 {"results": [{"text": "Changesets", "children": [{"text": "r625:ead8f28a4bc2", "type": "sha", "id": "ead8f28a4bc2f45ecfb148a6b8a89758b9654a84"}, {"text": "r535:c093f94d6d35", "type": "sha", "id": "c093f94d6d358f13c55a687da66c30c41cca4153"}, {"text": "r534:559f640ec08b", "type": "sha", "id": "559f640ec08b2a14c4a9ac863d8ca273545b8885"}, {"text": "r490:02a940b4ee37", "type": "sha", "id": "02a940b4ee371ec64ef5b4c4870a5c89dc7fb98a"}, {"text": "r464:b45a4153a2d7", "type": "sha", "id": "b45a4153a2d7adb8a78b63d35d39fac44a4320a6"}, {"text": "r460:0a54e66b9450", "type": "sha", "id": "0a54e66b94502409074b163cd93c1233dcc0413f"}, {"text": "r457:a7bf2f6bf3d5", "type": "sha", "id": "a7bf2f6bf3d5273da4bcd2032a891acae5a45e2b"}, {"text": "r456:7266de0154b4", "type": "sha", "id": "7266de0154b4da7c42ba3d788876056dbf116b5a"}, {"text": "r455:666de4ee6507", "type": "sha", "id": "666de4ee65074cd3e37ea01e75f65bd3e4c336bb"}, {"text": "r453:91acc599141c", "type": "sha", "id": "91acc599141c87f03e0e3551dcaacf4492632e58"}, {"text": "r442:40a2d5d71b75", "type": "sha", "id": "40a2d5d71b758e7eafc84a324ed55142cba22f42"}, {"text": "r440:d1f898326327", "type": "sha", "id": "d1f898326327e20524fe22417c22d71064fe54a1"}, {"text": "r420:162a36830c23", "type": "sha", "id": "162a36830c23ccf1bf1873157fd0c8d0dfc7c817"}, {"text": "r345:c994f0de03b2", "type": "sha", "id": "c994f0de03b2a0aa848a04fc2c0d7e737dba31fc"}, {"text": "r340:5d3d4d2c262e", "type": "sha", "id": "5d3d4d2c262e17b247d405feceeb09ff7408c940"}, {"text": "r334:4d4278a6390e", "type": "sha", "id": "4d4278a6390e42f4fc777ecf1b9b628e77da8e22"}, {"text": "r298:00dffb625166", "type": "sha", "id": "00dffb62516650bc5050d818eb47ea1ca207954d"}, {"text": "r297:47b6be9a812e", "type": "sha", "id": "47b6be9a812ec3ed0384001a458a759f0f583fe2"}, {"text": "r289:1589fed841cd", "type": "sha", "id": "1589fed841cd9ef33155f8560727809ac3ada2c8"}, {"text": "r285:afafd0ee2821", "type": "sha", "id": "afafd0ee28218ab979678213cb96e9e4dbd7359b"}, {"text": "r284:639b115ed2b0", "type": "sha", "id": "639b115ed2b02017824005b5ae66282c6e25eba8"}, {"text": "r283:fcf7562d7305", "type": "sha", "id": "fcf7562d7305affc94fe20dc89a34aefd2b8aa1e"}, {"text": "r256:ec8cbdb5f364", "type": "sha", "id": "ec8cbdb5f364fce7843cbf148c3d95d86f935339"}, {"text": "r255:0d74d2e2bdf3", "type": "sha", "id": "0d74d2e2bdf3dcd5ee9fe4fcfe9016c5c6486f35"}, {"text": "r243:6894ad7d8223", "type": "sha", "id": "6894ad7d8223b1e6853e9fdaa2c38d3f0cef1e38"}, {"text": "r231:31b3f4b599fa", "type": "sha", "id": "31b3f4b599fae5f12cf438c73403679cdf923d75"}, {"text": "r220:3d2515dd21fb", "type": "sha", "id": "3d2515dd21fb34fe6c5d0029075a863f3e92f5f6"}, {"text": "r186:f804e27aa496", "type": "sha", "id": "f804e27aa4961f2e327f2a10ee373235df20ee21"}, {"text": "r182:7f00513785a1", "type": "sha", "id": "7f00513785a13f273a4387ef086bb795b37f013c"}, {"text": "r181:6efcdc61028c", "type": "sha", "id": "6efcdc61028c8edd1c787b3439fae71b77a17357"}, {"text": "r175:6c0ce52b229a", "type": "sha", "id": "6c0ce52b229aa978889e91b38777f800e85f330b"}, {"text": "r165:09788a0b8a54", "type": "sha", "id": "09788a0b8a5455e9678c3959214246574e546d4f"}, {"text": "r163:0164ee729def", "type": "sha", "id": "0164ee729def0a253d6dcb594b5ee2a52fef4748"}, {"text": "r140:33fa32233551", "type": "sha", "id": "33fa3223355104431402a888fa77a4e9956feb3e"}, {"text": "r126:fa014c12c26d", "type": "sha", "id": "fa014c12c26d10ba682fadb78f2a11c24c8118e1"}, {"text": "r111:e686b958768e", "type": "sha", "id": "e686b958768ee96af8029fe19c6050b1a8dd3b2b"}, {"text": "r109:ab5721ca0a08", "type": "sha", "id": "ab5721ca0a081f26bf43d9051e615af2cc99952f"}, {"text": "r108:c877b68d18e7", "type": "sha", "id": "c877b68d18e792a66b7f4c529ea02c8f80801542"}, {"text": "r107:4313566d2e41", "type": "sha", "id": "4313566d2e417cb382948f8d9d7c765330356054"}, {"text": "r104:6c2303a79367", "type": "sha", "id": "6c2303a793671e807d1cfc70134c9ca0767d98c2"}, {"text": "r102:54386793436c", "type": "sha", "id": "54386793436c938cff89326944d4c2702340037d"}, {"text": "r101:54000345d2e7", "type": "sha", "id": "54000345d2e78b03a99d561399e8e548de3f3203"}, {"text": "r99:1c6b3677b37e", "type": "sha", "id": "1c6b3677b37ea064cb4b51714d8f7498f93f4b2b"}, {"text": "r93:2d03ca750a44", "type": "sha", "id": "2d03ca750a44440fb5ea8b751176d1f36f8e8f46"}, {"text": "r92:2a08b128c206", "type": "sha", "id": "2a08b128c206db48c2f0b8f70df060e6db0ae4f8"}, {"text": "r91:30c26513ff1e", "type": "sha", "id": "30c26513ff1eb8e5ce0e1c6b477ee5dc50e2f34b"}, {"text": "r82:ac71e9503c2c", "type": "sha", "id": "ac71e9503c2ca95542839af0ce7b64011b72ea7c"}, {"text": "r81:12669288fd13", "type": "sha", "id": "12669288fd13adba2a9b7dd5b870cc23ffab92d2"}, {"text": "r76:5a0c84f3e6fe", "type": "sha", "id": "5a0c84f3e6fe3473e4c8427199d5a6fc71a9b382"}, {"text": "r73:12f2f5e2b38e", "type": "sha", "id": "12f2f5e2b38e6ff3fbdb5d722efed9aa72ecb0d5"}, {"text": "r61:5eab1222a7cd", "type": "sha", "id": "5eab1222a7cd4bfcbabc218ca6d04276d4e27378"}, {"text": "r60:f50f42baeed5", "type": "sha", "id": "f50f42baeed5af6518ef4b0cb2f1423f3851a941"}, {"text": "r59:d7e390a45f6a", "type": "sha", "id": "d7e390a45f6aa96f04f5e7f583ad4f867431aa25"}, {"text": "r58:f15c21f97864", "type": "sha", "id": "f15c21f97864b4f071cddfbf2750ec2e23859414"}, {"text": "r57:e906ef056cf5", "type": "sha", "id": "e906ef056cf539a4e4e5fc8003eaf7cf14dd8ade"}, {"text": "r56:ea2b108b48aa", "type": "sha", "id": "ea2b108b48aa8f8c9c4a941f66c1a03315ca1c3b"}, {"text": "r50:84dec09632a4", "type": "sha", "id": "84dec09632a4458f79f50ddbbd155506c460b4f9"}, {"text": "r48:0115510b70c7", "type": "sha", "id": "0115510b70c7229dbc5dc49036b32e7d91d23acd"}, {"text": "r46:2a13f185e452", "type": "sha", "id": "2a13f185e4525f9d4b59882791a2d397b90d5ddc"}, {"text": "r30:3bf1c5868e57", "type": "sha", "id": "3bf1c5868e570e39569d094f922d33ced2fa3b2b"}, {"text": "r26:b8d040125747", "type": "sha", "id": "b8d04012574729d2c29886e53b1a43ef16dd00a1"}, {"text": "r24:6970b057cffe", "type": "sha", "id": "6970b057cffe4aab0a792aa634c89f4bebf01441"}, {"text": "r8:dd80b0f6cf50", "type": "sha", "id": "dd80b0f6cf5052f17cc738c2951c4f2070200d7f"}, {"text": "r7:ff7ca51e58c5", "type": "sha", "id": "ff7ca51e58c505fec0dd2491de52c622bb7a806b"}]}, {"text": "Branches", "children": [{"text": "master", "type": "branch", "id": "fd627b9e0dd80b47be81af07c4a98518244ed2f7"}]}, {"text": "Tags", "children": [{"text": "v0.2.2", "type": "tag", "id": "137fea89f304a42321d40488091ee2ed419a3686"}, {"text": "v0.2.1", "type": "tag", "id": "5051d0fa344d4408a2659d9a0348eb2d41868ecf"}, {"text": "v0.2.0", "type": "tag", "id": "599ba911aa24d2981225f3966eb659dfae9e9f30"}, {"text": "v0.1.9", "type": "tag", "id": "341d28f0eec5ddf0b6b77871e13c2bbd6bec685c"}, {"text": "v0.1.8", "type": "tag", "id": "74ebce002c088b8a5ecf40073db09375515ecd68"}, {"text": "v0.1.7", "type": "tag", "id": "4d78bf73b5c22c82b68f902f138f7881b4fffa2c"}, {"text": "v0.1.6", "type": "tag", "id": "0205cb3f44223fb3099d12a77a69c81b798772d9"}, {"text": "v0.1.5", "type": "tag", "id": "6c0ce52b229aa978889e91b38777f800e85f330b"}, {"text": "v0.1.4", "type": "tag", "id": "7d735150934cd7645ac3051903add952390324a5"}, {"text": "v0.1.3", "type": "tag", "id": "5a3a8fb005554692b16e21dee62bf02667d8dc3e"}, {"text": "v0.1.2", "type": "tag", "id": "0ba5f8a4660034ff25c0cac2a5baabf5d2791d63"}, {"text": "v0.1.11", "type": "tag", "id": "c60f01b77c42dce653d6b1d3b04689862c261929"}, {"text": "v0.1.10", "type": "tag", "id": "10cddef6b794696066fb346434014f0a56810218"}, {"text": "v0.1.1", "type": "tag", "id": "e6ea6d16e2f26250124a1f4b4fe37a912f9d86a0"}]}], "more": false}
@@ -1,1 +1,1 b''
1 {"results": [{"text": "Changesets", "children": [{"text": "r648:dbec37a0d5ca (default)", "id": "dbec37a0d5cab8ff39af4cfc4a4cd3996e4acfc6"}, {"text": "r639:1d20ed9eda94 (default)", "id": "1d20ed9eda9482d46ff0a6af5812550218b3ff15"}, {"text": "r547:0173395e8227 (default)", "id": "0173395e822797f098799ed95c1a81b6a547a9ad"}, {"text": "r546:afbb45ade933 (default)", "id": "afbb45ade933a8182f1d8ec5d4d1bb2de2572043"}, {"text": "r502:6f093e30cac3 (default)", "id": "6f093e30cac34e6b4b11275a9f22f80c5d7ad1f7"}, {"text": "r476:c7e2212dd2ae (default)", "id": "c7e2212dd2ae975d1d06534a3d7e317165c06960"}, {"text": "r472:45477506df79 (default)", "id": "45477506df79f701bf69419aac3e1f0fed3c5bcf"}, {"text": "r469:5fc76cb25d11 (default)", "id": "5fc76cb25d11e07c60de040f78b8cd265ff10d53"}, {"text": "r468:b073433cf899 (default)", "id": "b073433cf8994969ee5cd7cce84cbe587bb880b2"}, {"text": "r467:7a74dbfcacd1 (default)", "id": "7a74dbfcacd1dbcb58bb9c860b2f29fbb22c4c96"}, {"text": "r465:71ee52cc4d62 (default)", "id": "71ee52cc4d629096bdbee036325975dac2af4501"}, {"text": "r452:a5b217d26c5f (default)", "id": "a5b217d26c5f111e72bae4de672b084ee0fbf75c"}, {"text": "r450:47aedd538bf6 (default)", "id": "47aedd538bf616eedcb0e7d630ea476df0e159c7"}, {"text": "r432:8e4915fa32d7 (default)", "id": "8e4915fa32d727dcbf09746f637a5f82e539511e"}, {"text": "r356:25213a5fbb04 (default)", "id": "25213a5fbb048dff8ba65d21e466a835536e5b70"}, {"text": "r351:23debcedddc1 (default)", "id": "23debcedddc1c23c14be33e713e7786d4a9de471"}, {"text": "r342:61e25b2a90a1 (default)", "id": "61e25b2a90a19e7fffd75dea1e4c7e20df526bbe"}, {"text": "r318:fb95b340e0d0 (webvcs)", "id": "fb95b340e0d03fa51f33c56c991c08077c99303e"}, {"text": "r303:bda35e0e564f (default)", "id": "bda35e0e564fbbc5cd26fe0a37fb647a254c99fe"}, {"text": "r302:97ff74896d7d (default)", "id": "97ff74896d7dbf3115a337a421d44b55154acc89"}, {"text": "r293:cec3473c3fdb (default)", "id": "cec3473c3fdb9599c98067182a075b49bde570f9"}, {"text": "r289:0e86c43eef86 (default)", "id": "0e86c43eef866a013a587666a877c879899599bb"}, {"text": "r288:91a27c312808 (default)", "id": "91a27c312808100cf20a602f78befbbff9d89bfd"}, {"text": "r287:400e36a1670a (default)", "id": "400e36a1670a57d11e3edcb5b07bf82c30006d0b"}, {"text": "r261:014fb17dfc95 (default)", "id": "014fb17dfc95b0995e838c565376bf9a993e230a"}, {"text": "r260:cca7aebbc4d6 (default)", "id": "cca7aebbc4d6125798446b11e69dc8847834a982"}, {"text": "r258:14cdb2957c01 (workdir)", "id": "14cdb2957c011a5feba36f50d960d9832ba0f0c1"}, {"text": "r245:34df20118ed7 (default)", "id": "34df20118ed74b5987d22a579e8a60e903da5bf8"}, {"text": "r233:0375d9042a64 (workdir)", "id": "0375d9042a64a1ac1641528f0f0668f9a339e86d"}, {"text": "r222:94aa45fc1806 (workdir)", "id": "94aa45fc1806c04d4ba640933edf682c22478453"}, {"text": "r188:7ed99bc73881 (default)", "id": "7ed99bc738818879941e3ce20243f8856a7cfc84"}, {"text": "r184:1e85975528bc (default)", "id": "1e85975528bcebe853732a9e5fb8dbf4461f6bb2"}, {"text": "r183:ed30beddde7b (default)", "id": "ed30beddde7bbddb26042625be19bcd11576c1dd"}, {"text": "r177:a6664e18181c (default)", "id": "a6664e18181c6fc81b751a8d01474e7e1a3fe7fc"}, {"text": "r167:8911406ad776 (default)", "id": "8911406ad776fdd3d0b9932a2e89677e57405a48"}, {"text": "r165:aa957ed78c35 (default)", "id": "aa957ed78c35a1541f508d2ec90e501b0a9e3167"}, {"text": "r140:48e11b73e94c (default)", "id": "48e11b73e94c0db33e736eaeea692f990cb0b5f1"}, {"text": "r126:adf3cbf48329 (default)", "id": "adf3cbf483298563b968a6c673cd5bde5f7d5eea"}, {"text": "r113:6249fd0fb2cf (git)", "id": "6249fd0fb2cfb1411e764129f598e2cf0de79a6f"}, {"text": "r109:75feb4c33e81 (default)", "id": "75feb4c33e81186c87eac740cee2447330288412"}, {"text": "r108:9a4dc232ecdc (default)", "id": "9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d"}, {"text": "r107:595cce4efa21 (default)", "id": "595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d"}, {"text": "r104:4a8bd421fbc2 (default)", "id": "4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da"}, {"text": "r102:57be63fc8f85 (default)", "id": "57be63fc8f85e65a0106a53187f7316f8c487ffa"}, {"text": "r101:5530bd87f7e2 (git)", "id": "5530bd87f7e2e124a64d07cb2654c997682128be"}, {"text": "r99:e516008b1c93 (default)", "id": "e516008b1c93f142263dc4b7961787cbad654ce1"}, {"text": "r93:41f43fc74b8b (default)", "id": "41f43fc74b8b285984554532eb105ac3be5c434f"}, {"text": "r92:cc66b61b8455 (default)", "id": "cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e"}, {"text": "r91:73ab5b616b32 (default)", "id": "73ab5b616b3271b0518682fb4988ce421de8099f"}, {"text": "r82:e0da75f308c0 (default)", "id": "e0da75f308c0f18f98e9ce6257626009fdda2b39"}, {"text": "r81:fb2e41e0f081 (default)", "id": "fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611"}, {"text": "r76:602ae2f5e7ad (default)", "id": "602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028"}, {"text": "r73:a066b25d5df7 (default)", "id": "a066b25d5df7016b45a41b7e2a78c33b57adc235"}, {"text": "r61:637a933c9059 (web)", "id": "637a933c905958ce5151f154147c25c1c7b68832"}, {"text": "r60:0c21004effeb (web)", "id": "0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc"}, {"text": "r59:a1f39c56d3f1 (web)", "id": "a1f39c56d3f1d52d5fb5920370a2a2716cd9a444"}, {"text": "r58:97d32df05c71 (web)", "id": "97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f"}, {"text": "r57:08eaf1451771 (web)", "id": "08eaf14517718dccea4b67755a93368341aca919"}, {"text": "r56:22f71ad26526 (web)", "id": "22f71ad265265a53238359c883aa976e725aa07d"}, {"text": "r49:97501f02b7b4 (web)", "id": "97501f02b7b4330924b647755663a2d90a5e638d"}, {"text": "r47:86ede6754f2b (web)", "id": "86ede6754f2b27309452bb11f997386ae01d0e5a"}, {"text": "r45:014c40c0203c (web)", "id": "014c40c0203c423dc19ecf94644f7cac9d4cdce0"}, {"text": "r30:ee87846a61c1 (default)", "id": "ee87846a61c12153b51543bf860e1026c6d3dcba"}, {"text": "r26:9bb326a04ae5 (default)", "id": "9bb326a04ae5d98d437dece54be04f830cf1edd9"}, {"text": "r24:536c1a194283 (default)", "id": "536c1a19428381cfea92ac44985304f6a8049569"}, {"text": "r8:dc5d2c0661b6 (default)", "id": "dc5d2c0661b61928834a785d3e64a3f80d3aad9c"}, {"text": "r7:3803844fdbd3 (default)", "id": "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"}]}, {"text": "Branches", "children": [{"text": "default", "id": "2062ec7beeeaf9f44a1c25c41479565040b930b2"}, {"text": "stable", "id": "4f7e2131323e0749a740c0a56ab68ae9269c562a"}]}, {"text": "Tags", "children": [{"text": "v0.2.0", "id": "2c96c02def9a7c997f33047761a53943e6254396"}, {"text": "v0.1.9", "id": "8680b1d1cee3aa3c1ab3734b76ee164bbedbc5c9"}, {"text": "v0.1.8", "id": "ecb25ba9c96faf1e65a0bc3fd914918420a2f116"}, {"text": "v0.1.7", "id": "f67633a2894edaf28513706d558205fa93df9209"}, {"text": "v0.1.6", "id": "02b38c0eb6f982174750c0e309ff9faddc0c7e12"}, {"text": "v0.1.5", "id": "a6664e18181c6fc81b751a8d01474e7e1a3fe7fc"}, {"text": "v0.1.4", "id": "fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200"}, {"text": "v0.1.3", "id": "17544fbfcd33ffb439e2b728b5d526b1ef30bfcf"}, {"text": "v0.1.2", "id": "a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720"}, {"text": "v0.1.11", "id": "fef5bfe1dc17611d5fb59a7f6f95c55c3606f933"}, {"text": "v0.1.10", "id": "92831aebf2f8dd4879e897024b89d09af214df1c"}, {"text": "v0.1.1", "id": "eb3a60fc964309c1a318b8dfe26aa2d1586c85ae"}, {"text": "tip", "id": "2062ec7beeeaf9f44a1c25c41479565040b930b2"}]}], "more": false}
1 {"results": [{"text": "Changesets", "children": [{"text": "r648:dbec37a0d5ca (default)", "type": "sha", "id": "dbec37a0d5cab8ff39af4cfc4a4cd3996e4acfc6"}, {"text": "r639:1d20ed9eda94 (default)", "type": "sha", "id": "1d20ed9eda9482d46ff0a6af5812550218b3ff15"}, {"text": "r547:0173395e8227 (default)", "type": "sha", "id": "0173395e822797f098799ed95c1a81b6a547a9ad"}, {"text": "r546:afbb45ade933 (default)", "type": "sha", "id": "afbb45ade933a8182f1d8ec5d4d1bb2de2572043"}, {"text": "r502:6f093e30cac3 (default)", "type": "sha", "id": "6f093e30cac34e6b4b11275a9f22f80c5d7ad1f7"}, {"text": "r476:c7e2212dd2ae (default)", "type": "sha", "id": "c7e2212dd2ae975d1d06534a3d7e317165c06960"}, {"text": "r472:45477506df79 (default)", "type": "sha", "id": "45477506df79f701bf69419aac3e1f0fed3c5bcf"}, {"text": "r469:5fc76cb25d11 (default)", "type": "sha", "id": "5fc76cb25d11e07c60de040f78b8cd265ff10d53"}, {"text": "r468:b073433cf899 (default)", "type": "sha", "id": "b073433cf8994969ee5cd7cce84cbe587bb880b2"}, {"text": "r467:7a74dbfcacd1 (default)", "type": "sha", "id": "7a74dbfcacd1dbcb58bb9c860b2f29fbb22c4c96"}, {"text": "r465:71ee52cc4d62 (default)", "type": "sha", "id": "71ee52cc4d629096bdbee036325975dac2af4501"}, {"text": "r452:a5b217d26c5f (default)", "type": "sha", "id": "a5b217d26c5f111e72bae4de672b084ee0fbf75c"}, {"text": "r450:47aedd538bf6 (default)", "type": "sha", "id": "47aedd538bf616eedcb0e7d630ea476df0e159c7"}, {"text": "r432:8e4915fa32d7 (default)", "type": "sha", "id": "8e4915fa32d727dcbf09746f637a5f82e539511e"}, {"text": "r356:25213a5fbb04 (default)", "type": "sha", "id": "25213a5fbb048dff8ba65d21e466a835536e5b70"}, {"text": "r351:23debcedddc1 (default)", "type": "sha", "id": "23debcedddc1c23c14be33e713e7786d4a9de471"}, {"text": "r342:61e25b2a90a1 (default)", "type": "sha", "id": "61e25b2a90a19e7fffd75dea1e4c7e20df526bbe"}, {"text": "r318:fb95b340e0d0 (webvcs)", "type": "sha", "id": "fb95b340e0d03fa51f33c56c991c08077c99303e"}, {"text": "r303:bda35e0e564f (default)", "type": "sha", "id": "bda35e0e564fbbc5cd26fe0a37fb647a254c99fe"}, {"text": "r302:97ff74896d7d (default)", "type": "sha", "id": "97ff74896d7dbf3115a337a421d44b55154acc89"}, {"text": "r293:cec3473c3fdb (default)", "type": "sha", "id": "cec3473c3fdb9599c98067182a075b49bde570f9"}, {"text": "r289:0e86c43eef86 (default)", "type": "sha", "id": "0e86c43eef866a013a587666a877c879899599bb"}, {"text": "r288:91a27c312808 (default)", "type": "sha", "id": "91a27c312808100cf20a602f78befbbff9d89bfd"}, {"text": "r287:400e36a1670a (default)", "type": "sha", "id": "400e36a1670a57d11e3edcb5b07bf82c30006d0b"}, {"text": "r261:014fb17dfc95 (default)", "type": "sha", "id": "014fb17dfc95b0995e838c565376bf9a993e230a"}, {"text": "r260:cca7aebbc4d6 (default)", "type": "sha", "id": "cca7aebbc4d6125798446b11e69dc8847834a982"}, {"text": "r258:14cdb2957c01 (workdir)", "type": "sha", "id": "14cdb2957c011a5feba36f50d960d9832ba0f0c1"}, {"text": "r245:34df20118ed7 (default)", "type": "sha", "id": "34df20118ed74b5987d22a579e8a60e903da5bf8"}, {"text": "r233:0375d9042a64 (workdir)", "type": "sha", "id": "0375d9042a64a1ac1641528f0f0668f9a339e86d"}, {"text": "r222:94aa45fc1806 (workdir)", "type": "sha", "id": "94aa45fc1806c04d4ba640933edf682c22478453"}, {"text": "r188:7ed99bc73881 (default)", "type": "sha", "id": "7ed99bc738818879941e3ce20243f8856a7cfc84"}, {"text": "r184:1e85975528bc (default)", "type": "sha", "id": "1e85975528bcebe853732a9e5fb8dbf4461f6bb2"}, {"text": "r183:ed30beddde7b (default)", "type": "sha", "id": "ed30beddde7bbddb26042625be19bcd11576c1dd"}, {"text": "r177:a6664e18181c (default)", "type": "sha", "id": "a6664e18181c6fc81b751a8d01474e7e1a3fe7fc"}, {"text": "r167:8911406ad776 (default)", "type": "sha", "id": "8911406ad776fdd3d0b9932a2e89677e57405a48"}, {"text": "r165:aa957ed78c35 (default)", "type": "sha", "id": "aa957ed78c35a1541f508d2ec90e501b0a9e3167"}, {"text": "r140:48e11b73e94c (default)", "type": "sha", "id": "48e11b73e94c0db33e736eaeea692f990cb0b5f1"}, {"text": "r126:adf3cbf48329 (default)", "type": "sha", "id": "adf3cbf483298563b968a6c673cd5bde5f7d5eea"}, {"text": "r113:6249fd0fb2cf (git)", "type": "sha", "id": "6249fd0fb2cfb1411e764129f598e2cf0de79a6f"}, {"text": "r109:75feb4c33e81 (default)", "type": "sha", "id": "75feb4c33e81186c87eac740cee2447330288412"}, {"text": "r108:9a4dc232ecdc (default)", "type": "sha", "id": "9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d"}, {"text": "r107:595cce4efa21 (default)", "type": "sha", "id": "595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d"}, {"text": "r104:4a8bd421fbc2 (default)", "type": "sha", "id": "4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da"}, {"text": "r102:57be63fc8f85 (default)", "type": "sha", "id": "57be63fc8f85e65a0106a53187f7316f8c487ffa"}, {"text": "r101:5530bd87f7e2 (git)", "type": "sha", "id": "5530bd87f7e2e124a64d07cb2654c997682128be"}, {"text": "r99:e516008b1c93 (default)", "type": "sha", "id": "e516008b1c93f142263dc4b7961787cbad654ce1"}, {"text": "r93:41f43fc74b8b (default)", "type": "sha", "id": "41f43fc74b8b285984554532eb105ac3be5c434f"}, {"text": "r92:cc66b61b8455 (default)", "type": "sha", "id": "cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e"}, {"text": "r91:73ab5b616b32 (default)", "type": "sha", "id": "73ab5b616b3271b0518682fb4988ce421de8099f"}, {"text": "r82:e0da75f308c0 (default)", "type": "sha", "id": "e0da75f308c0f18f98e9ce6257626009fdda2b39"}, {"text": "r81:fb2e41e0f081 (default)", "type": "sha", "id": "fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611"}, {"text": "r76:602ae2f5e7ad (default)", "type": "sha", "id": "602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028"}, {"text": "r73:a066b25d5df7 (default)", "type": "sha", "id": "a066b25d5df7016b45a41b7e2a78c33b57adc235"}, {"text": "r61:637a933c9059 (web)", "type": "sha", "id": "637a933c905958ce5151f154147c25c1c7b68832"}, {"text": "r60:0c21004effeb (web)", "type": "sha", "id": "0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc"}, {"text": "r59:a1f39c56d3f1 (web)", "type": "sha", "id": "a1f39c56d3f1d52d5fb5920370a2a2716cd9a444"}, {"text": "r58:97d32df05c71 (web)", "type": "sha", "id": "97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f"}, {"text": "r57:08eaf1451771 (web)", "type": "sha", "id": "08eaf14517718dccea4b67755a93368341aca919"}, {"text": "r56:22f71ad26526 (web)", "type": "sha", "id": "22f71ad265265a53238359c883aa976e725aa07d"}, {"text": "r49:97501f02b7b4 (web)", "type": "sha", "id": "97501f02b7b4330924b647755663a2d90a5e638d"}, {"text": "r47:86ede6754f2b (web)", "type": "sha", "id": "86ede6754f2b27309452bb11f997386ae01d0e5a"}, {"text": "r45:014c40c0203c (web)", "type": "sha", "id": "014c40c0203c423dc19ecf94644f7cac9d4cdce0"}, {"text": "r30:ee87846a61c1 (default)", "type": "sha", "id": "ee87846a61c12153b51543bf860e1026c6d3dcba"}, {"text": "r26:9bb326a04ae5 (default)", "type": "sha", "id": "9bb326a04ae5d98d437dece54be04f830cf1edd9"}, {"text": "r24:536c1a194283 (default)", "type": "sha", "id": "536c1a19428381cfea92ac44985304f6a8049569"}, {"text": "r8:dc5d2c0661b6 (default)", "type": "sha", "id": "dc5d2c0661b61928834a785d3e64a3f80d3aad9c"}, {"text": "r7:3803844fdbd3 (default)", "type": "sha", "id": "3803844fdbd3b711175fc3da9bdacfcd6d29a6fb"}]}, {"text": "Branches", "children": [{"text": "default", "type": "branch", "id": "2062ec7beeeaf9f44a1c25c41479565040b930b2"}, {"text": "stable", "type": "branch", "id": "4f7e2131323e0749a740c0a56ab68ae9269c562a"}]}, {"text": "Tags", "children": [{"text": "v0.2.0", "type": "tag", "id": "2c96c02def9a7c997f33047761a53943e6254396"}, {"text": "v0.1.9", "type": "tag", "id": "8680b1d1cee3aa3c1ab3734b76ee164bbedbc5c9"}, {"text": "v0.1.8", "type": "tag", "id": "ecb25ba9c96faf1e65a0bc3fd914918420a2f116"}, {"text": "v0.1.7", "type": "tag", "id": "f67633a2894edaf28513706d558205fa93df9209"}, {"text": "v0.1.6", "type": "tag", "id": "02b38c0eb6f982174750c0e309ff9faddc0c7e12"}, {"text": "v0.1.5", "type": "tag", "id": "a6664e18181c6fc81b751a8d01474e7e1a3fe7fc"}, {"text": "v0.1.4", "type": "tag", "id": "fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200"}, {"text": "v0.1.3", "type": "tag", "id": "17544fbfcd33ffb439e2b728b5d526b1ef30bfcf"}, {"text": "v0.1.2", "type": "tag", "id": "a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720"}, {"text": "v0.1.11", "type": "tag", "id": "fef5bfe1dc17611d5fb59a7f6f95c55c3606f933"}, {"text": "v0.1.10", "type": "tag", "id": "92831aebf2f8dd4879e897024b89d09af214df1c"}, {"text": "v0.1.1", "type": "tag", "id": "eb3a60fc964309c1a318b8dfe26aa2d1586c85ae"}, {"text": "tip", "type": "tag", "id": "2062ec7beeeaf9f44a1c25c41479565040b930b2"}]}], "more": false} No newline at end of file
@@ -1,1 +1,1 b''
1 {"more": false, "results": [{"text": "Changesets", "children": [{"text": "r15:16", "id": "16"}, {"text": "r12:13", "id": "13"}, {"text": "r7:8", "id": "8"}, {"text": "r3:4", "id": "4"}, {"text": "r2:3", "id": "3"}]}, {"text": "Branches", "children": [{"text": "branches/add-docs", "id": "branches/add-docs/example.py@26"}, {"text": "branches/argparse", "id": "branches/argparse/example.py@26"}, {"text": "trunk", "id": "trunk/example.py@26"}]}, {"text": "Tags", "children": [{"text": "tags/v0.1", "id": "tags/v0.1/example.py@26"}, {"text": "tags/v0.2", "id": "tags/v0.2/example.py@26"}, {"text": "tags/v0.3", "id": "tags/v0.3/example.py@26"}, {"text": "tags/v0.5", "id": "tags/v0.5/example.py@26"}]}]} No newline at end of file
1 {"results": [{"text": "Changesets", "children": [{"text": "r15:16", "type": "sha", "id": "16"}, {"text": "r12:13", "type": "sha", "id": "13"}, {"text": "r7:8", "type": "sha", "id": "8"}, {"text": "r3:4", "type": "sha", "id": "4"}, {"text": "r2:3", "type": "sha", "id": "3"}]}, {"text": "Branches", "children": [{"text": "branches/add-docs", "type": "branch", "id": "branches/add-docs/example.py@26"}, {"text": "branches/argparse", "type": "branch", "id": "branches/argparse/example.py@26"}, {"text": "trunk", "type": "branch", "id": "trunk/example.py@26"}]}, {"text": "Tags", "children": [{"text": "tags/v0.1", "type": "tag", "id": "tags/v0.1/example.py@26"}, {"text": "tags/v0.2", "type": "tag", "id": "tags/v0.2/example.py@26"}, {"text": "tags/v0.3", "type": "tag", "id": "tags/v0.3/example.py@26"}, {"text": "tags/v0.5", "type": "tag", "id": "tags/v0.5/example.py@26"}]}], "more": false} No newline at end of file
@@ -1,1 +1,1 b''
1 {"results": [{"text": "Changesets", "children": [{"text": "r382:383", "id": "383"}, {"text": "r323:324", "id": "324"}, {"text": "r322:323", "id": "323"}, {"text": "r299:300", "id": "300"}, {"text": "r277:278", "id": "278"}, {"text": "r273:274", "id": "274"}, {"text": "r270:271", "id": "271"}, {"text": "r269:270", "id": "270"}, {"text": "r263:264", "id": "264"}, {"text": "r261:262", "id": "262"}, {"text": "r251:252", "id": "252"}, {"text": "r208:209", "id": "209"}, {"text": "r202:203", "id": "203"}, {"text": "r173:174", "id": "174"}, {"text": "r172:173", "id": "173"}, {"text": "r171:172", "id": "172"}, {"text": "r145:146", "id": "146"}, {"text": "r144:145", "id": "145"}, {"text": "r140:141", "id": "141"}, {"text": "r134:135", "id": "135"}, {"text": "r107:108", "id": "108"}, {"text": "r106:107", "id": "107"}, {"text": "r100:101", "id": "101"}, {"text": "r94:95", "id": "95"}, {"text": "r85:86", "id": "86"}, {"text": "r73:74", "id": "74"}, {"text": "r72:73", "id": "73"}, {"text": "r71:72", "id": "72"}, {"text": "r69:70", "id": "70"}, {"text": "r67:68", "id": "68"}, {"text": "r63:64", "id": "64"}, {"text": "r62:63", "id": "63"}, {"text": "r61:62", "id": "62"}, {"text": "r50:51", "id": "51"}, {"text": "r49:50", "id": "50"}, {"text": "r48:49", "id": "49"}, {"text": "r47:48", "id": "48"}, {"text": "r46:47", "id": "47"}, {"text": "r45:46", "id": "46"}, {"text": "r41:42", "id": "42"}, {"text": "r39:40", "id": "40"}, {"text": "r37:38", "id": "38"}, {"text": "r25:26", "id": "26"}, {"text": "r23:24", "id": "24"}, {"text": "r8:9", "id": "9"}, {"text": "r7:8", "id": "8"}]}, {"text": "Branches", "children": []}, {"text": "Tags", "children": []}], "more": false} No newline at end of file
1 {"results": [{"text": "Changesets", "children": [{"text": "r382:383", "type": "sha", "id": "383"}, {"text": "r323:324", "type": "sha", "id": "324"}, {"text": "r322:323", "type": "sha", "id": "323"}, {"text": "r299:300", "type": "sha", "id": "300"}, {"text": "r277:278", "type": "sha", "id": "278"}, {"text": "r273:274", "type": "sha", "id": "274"}, {"text": "r270:271", "type": "sha", "id": "271"}, {"text": "r269:270", "type": "sha", "id": "270"}, {"text": "r263:264", "type": "sha", "id": "264"}, {"text": "r261:262", "type": "sha", "id": "262"}, {"text": "r251:252", "type": "sha", "id": "252"}, {"text": "r208:209", "type": "sha", "id": "209"}, {"text": "r202:203", "type": "sha", "id": "203"}, {"text": "r173:174", "type": "sha", "id": "174"}, {"text": "r172:173", "type": "sha", "id": "173"}, {"text": "r171:172", "type": "sha", "id": "172"}, {"text": "r145:146", "type": "sha", "id": "146"}, {"text": "r144:145", "type": "sha", "id": "145"}, {"text": "r140:141", "type": "sha", "id": "141"}, {"text": "r134:135", "type": "sha", "id": "135"}, {"text": "r107:108", "type": "sha", "id": "108"}, {"text": "r106:107", "type": "sha", "id": "107"}, {"text": "r100:101", "type": "sha", "id": "101"}, {"text": "r94:95", "type": "sha", "id": "95"}, {"text": "r85:86", "type": "sha", "id": "86"}, {"text": "r73:74", "type": "sha", "id": "74"}, {"text": "r72:73", "type": "sha", "id": "73"}, {"text": "r71:72", "type": "sha", "id": "72"}, {"text": "r69:70", "type": "sha", "id": "70"}, {"text": "r67:68", "type": "sha", "id": "68"}, {"text": "r63:64", "type": "sha", "id": "64"}, {"text": "r62:63", "type": "sha", "id": "63"}, {"text": "r61:62", "type": "sha", "id": "62"}, {"text": "r50:51", "type": "sha", "id": "51"}, {"text": "r49:50", "type": "sha", "id": "50"}, {"text": "r48:49", "type": "sha", "id": "49"}, {"text": "r47:48", "type": "sha", "id": "48"}, {"text": "r46:47", "type": "sha", "id": "47"}, {"text": "r45:46", "type": "sha", "id": "46"}, {"text": "r41:42", "type": "sha", "id": "42"}, {"text": "r39:40", "type": "sha", "id": "40"}, {"text": "r37:38", "type": "sha", "id": "38"}, {"text": "r25:26", "type": "sha", "id": "26"}, {"text": "r23:24", "type": "sha", "id": "24"}, {"text": "r8:9", "type": "sha", "id": "9"}, {"text": "r7:8", "type": "sha", "id": "8"}]}, {"text": "Branches", "children": []}, {"text": "Tags", "children": []}], "more": false} No newline at end of file
@@ -1,118 +1,118 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 from pyramid.httpexceptions import HTTPException, HTTPBadRequest
24 24
25 25 from rhodecode.lib.middleware.vcs import (
26 26 detect_vcs_request, VCS_TYPE_KEY, VCS_TYPE_SKIP)
27 27
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31
32 32 def vcs_detection_tween_factory(handler, registry):
33 33
34 34 def vcs_detection_tween(request):
35 35 """
36 36 Do detection of vcs type, and save results for other layers to re-use
37 37 this information
38 38 """
39 39 vcs_server_enabled = request.registry.settings.get('vcs.server.enable')
40 40 vcs_handler = vcs_server_enabled and detect_vcs_request(
41 41 request.environ, request.registry.settings.get('vcs.backends'))
42 42
43 43 if vcs_handler:
44 44 # save detected VCS type for later re-use
45 45 request.environ[VCS_TYPE_KEY] = vcs_handler.SCM
46 46 request.vcs_call = vcs_handler.SCM
47 47
48 48 log.debug('Processing request with `%s` handler', handler)
49 49 return handler(request)
50 50
51 51 # mark that we didn't detect an VCS, and we can skip detection later on
52 52 request.environ[VCS_TYPE_KEY] = VCS_TYPE_SKIP
53 53
54 54 log.debug('Processing request with `%s` handler', handler)
55 55 return handler(request)
56 56
57 57 return vcs_detection_tween
58 58
59 59
60 60 def junk_encoding_detector(request):
61 61 """
62 62 Detect bad encoded GET params, and fail immediately with BadRequest
63 63 """
64 64
65 65 try:
66 66 request.GET.get("", None)
67 67 except UnicodeDecodeError:
68 68 raise HTTPBadRequest("Invalid bytes in query string.")
69 69
70 70
71 71 def bad_url_data_detector(request):
72 72 """
73 73 Detect invalid bytes in a path.
74 74 """
75 75 try:
76 76 request.path_info
77 77 except UnicodeDecodeError:
78 78 raise HTTPBadRequest("Invalid bytes in URL.")
79 79
80 80
81 81 def junk_form_data_detector(request):
82 82 """
83 83 Detect bad encoded POST params, and fail immediately with BadRequest
84 84 """
85 85
86 86 if request.method == "POST":
87 87 try:
88 88 request.POST.get("", None)
89 89 except ValueError:
90 90 raise HTTPBadRequest("Invalid bytes in form data.")
91 91
92 92
93 93 def sanity_check_factory(handler, registry):
94 94 def sanity_check(request):
95 log.debug('Checking URL sanity')
95 log.debug('Checking current URL sanity for bad data')
96 96 try:
97 97 junk_encoding_detector(request)
98 98 bad_url_data_detector(request)
99 99 junk_form_data_detector(request)
100 100 except HTTPException as exc:
101 101 return exc
102 102
103 103 return handler(request)
104 104
105 105 return sanity_check
106 106
107 107
108 108 def includeme(config):
109 109 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
110 110 'pyramid.events.BeforeRender')
111 111 config.add_subscriber('rhodecode.subscribers.set_user_lang',
112 112 'pyramid.events.NewRequest')
113 113 config.add_subscriber('rhodecode.subscribers.add_localizer',
114 114 'pyramid.events.NewRequest')
115 115 config.add_subscriber('rhodecode.subscribers.add_request_user_context',
116 116 'pyramid.events.ContextFound')
117 117 config.add_tween('rhodecode.tweens.vcs_detection_tween_factory')
118 118 config.add_tween('rhodecode.tweens.sanity_check_factory')
General Comments 0
You need to be logged in to leave comments. Login now