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