##// END OF EJS Templates
archival: allowed using .tbz2 and .tgz extensions.
marcink -
r3736:ab6407f1 new-ui
parent child Browse files
Show More
@@ -1,1069 +1,1069 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 right-content">%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 assert expected_data == response.json
316 assert expected_data == response.json
317
317
318 def test_file_source_history_with_annotation(self, backend, xhr_header):
318 def test_file_source_history_with_annotation(self, backend, xhr_header):
319 response = self.app.get(
319 response = self.app.get(
320 route_path('repo_file_history',
320 route_path('repo_file_history',
321 repo_name=backend.repo_name,
321 repo_name=backend.repo_name,
322 commit_id='tip', f_path='vcs/nodes.py',
322 commit_id='tip', f_path='vcs/nodes.py',
323 params=dict(annotate=1)),
323 params=dict(annotate=1)),
324
324
325 extra_environ=xhr_header)
325 extra_environ=xhr_header)
326 assert get_node_history(backend.alias) == json.loads(response.body)
326 assert get_node_history(backend.alias) == json.loads(response.body)
327
327
328 def test_tree_search_top_level(self, backend, xhr_header):
328 def test_tree_search_top_level(self, backend, xhr_header):
329 commit = backend.repo.get_commit(commit_idx=173)
329 commit = backend.repo.get_commit(commit_idx=173)
330 response = self.app.get(
330 response = self.app.get(
331 route_path('repo_files_nodelist',
331 route_path('repo_files_nodelist',
332 repo_name=backend.repo_name,
332 repo_name=backend.repo_name,
333 commit_id=commit.raw_id, f_path='/'),
333 commit_id=commit.raw_id, f_path='/'),
334 extra_environ=xhr_header)
334 extra_environ=xhr_header)
335 assert 'nodes' in response.json
335 assert 'nodes' in response.json
336 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
336 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
337
337
338 def test_tree_search_missing_xhr(self, backend):
338 def test_tree_search_missing_xhr(self, backend):
339 self.app.get(
339 self.app.get(
340 route_path('repo_files_nodelist',
340 route_path('repo_files_nodelist',
341 repo_name=backend.repo_name,
341 repo_name=backend.repo_name,
342 commit_id='tip', f_path='/'),
342 commit_id='tip', f_path='/'),
343 status=404)
343 status=404)
344
344
345 def test_tree_search_at_path(self, backend, xhr_header):
345 def test_tree_search_at_path(self, backend, xhr_header):
346 commit = backend.repo.get_commit(commit_idx=173)
346 commit = backend.repo.get_commit(commit_idx=173)
347 response = self.app.get(
347 response = self.app.get(
348 route_path('repo_files_nodelist',
348 route_path('repo_files_nodelist',
349 repo_name=backend.repo_name,
349 repo_name=backend.repo_name,
350 commit_id=commit.raw_id, f_path='/docs'),
350 commit_id=commit.raw_id, f_path='/docs'),
351 extra_environ=xhr_header)
351 extra_environ=xhr_header)
352 assert 'nodes' in response.json
352 assert 'nodes' in response.json
353 nodes = response.json['nodes']
353 nodes = response.json['nodes']
354 assert {'name': 'docs/api', 'type': 'dir'} in nodes
354 assert {'name': 'docs/api', 'type': 'dir'} in nodes
355 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
355 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
356
356
357 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
357 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
358 commit = backend.repo.get_commit(commit_idx=173)
358 commit = backend.repo.get_commit(commit_idx=173)
359 response = self.app.get(
359 response = self.app.get(
360 route_path('repo_files_nodelist',
360 route_path('repo_files_nodelist',
361 repo_name=backend.repo_name,
361 repo_name=backend.repo_name,
362 commit_id=commit.raw_id, f_path='/docs/api'),
362 commit_id=commit.raw_id, f_path='/docs/api'),
363 extra_environ=xhr_header)
363 extra_environ=xhr_header)
364 assert 'nodes' in response.json
364 assert 'nodes' in response.json
365 nodes = response.json['nodes']
365 nodes = response.json['nodes']
366 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
366 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
367
367
368 def test_tree_search_at_path_missing_xhr(self, backend):
368 def test_tree_search_at_path_missing_xhr(self, backend):
369 self.app.get(
369 self.app.get(
370 route_path('repo_files_nodelist',
370 route_path('repo_files_nodelist',
371 repo_name=backend.repo_name,
371 repo_name=backend.repo_name,
372 commit_id='tip', f_path='/docs'),
372 commit_id='tip', f_path='/docs'),
373 status=404)
373 status=404)
374
374
375 def test_nodetree(self, backend, xhr_header):
375 def test_nodetree(self, backend, xhr_header):
376 commit = backend.repo.get_commit(commit_idx=173)
376 commit = backend.repo.get_commit(commit_idx=173)
377 response = self.app.get(
377 response = self.app.get(
378 route_path('repo_nodetree_full',
378 route_path('repo_nodetree_full',
379 repo_name=backend.repo_name,
379 repo_name=backend.repo_name,
380 commit_id=commit.raw_id, f_path='/'),
380 commit_id=commit.raw_id, f_path='/'),
381 extra_environ=xhr_header)
381 extra_environ=xhr_header)
382
382
383 assert_response = response.assert_response()
383 assert_response = response.assert_response()
384
384
385 for attr in ['data-commit-id', 'data-date', 'data-author']:
385 for attr in ['data-commit-id', 'data-date', 'data-author']:
386 elements = assert_response.get_elements('[{}]'.format(attr))
386 elements = assert_response.get_elements('[{}]'.format(attr))
387 assert len(elements) > 1
387 assert len(elements) > 1
388
388
389 for element in elements:
389 for element in elements:
390 assert element.get(attr)
390 assert element.get(attr)
391
391
392 def test_nodetree_if_file(self, backend, xhr_header):
392 def test_nodetree_if_file(self, backend, xhr_header):
393 commit = backend.repo.get_commit(commit_idx=173)
393 commit = backend.repo.get_commit(commit_idx=173)
394 response = self.app.get(
394 response = self.app.get(
395 route_path('repo_nodetree_full',
395 route_path('repo_nodetree_full',
396 repo_name=backend.repo_name,
396 repo_name=backend.repo_name,
397 commit_id=commit.raw_id, f_path='README.rst'),
397 commit_id=commit.raw_id, f_path='README.rst'),
398 extra_environ=xhr_header)
398 extra_environ=xhr_header)
399 assert response.body == ''
399 assert response.body == ''
400
400
401 def test_nodetree_wrong_path(self, backend, xhr_header):
401 def test_nodetree_wrong_path(self, backend, xhr_header):
402 commit = backend.repo.get_commit(commit_idx=173)
402 commit = backend.repo.get_commit(commit_idx=173)
403 response = self.app.get(
403 response = self.app.get(
404 route_path('repo_nodetree_full',
404 route_path('repo_nodetree_full',
405 repo_name=backend.repo_name,
405 repo_name=backend.repo_name,
406 commit_id=commit.raw_id, f_path='/dont-exist'),
406 commit_id=commit.raw_id, f_path='/dont-exist'),
407 extra_environ=xhr_header)
407 extra_environ=xhr_header)
408
408
409 err = 'error: There is no file nor ' \
409 err = 'error: There is no file nor ' \
410 'directory at the given path'
410 'directory at the given path'
411 assert err in response.body
411 assert err in response.body
412
412
413 def test_nodetree_missing_xhr(self, backend):
413 def test_nodetree_missing_xhr(self, backend):
414 self.app.get(
414 self.app.get(
415 route_path('repo_nodetree_full',
415 route_path('repo_nodetree_full',
416 repo_name=backend.repo_name,
416 repo_name=backend.repo_name,
417 commit_id='tip', f_path='/'),
417 commit_id='tip', f_path='/'),
418 status=404)
418 status=404)
419
419
420
420
421 @pytest.mark.usefixtures("app", "autologin_user")
421 @pytest.mark.usefixtures("app", "autologin_user")
422 class TestRawFileHandling(object):
422 class TestRawFileHandling(object):
423
423
424 def test_download_file(self, backend):
424 def test_download_file(self, backend):
425 commit = backend.repo.get_commit(commit_idx=173)
425 commit = backend.repo.get_commit(commit_idx=173)
426 response = self.app.get(
426 response = self.app.get(
427 route_path('repo_file_download',
427 route_path('repo_file_download',
428 repo_name=backend.repo_name,
428 repo_name=backend.repo_name,
429 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
429 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
430
430
431 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
431 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
432 assert response.content_type == "text/x-python"
432 assert response.content_type == "text/x-python"
433
433
434 def test_download_file_wrong_cs(self, backend):
434 def test_download_file_wrong_cs(self, backend):
435 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
435 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
436
436
437 response = self.app.get(
437 response = self.app.get(
438 route_path('repo_file_download',
438 route_path('repo_file_download',
439 repo_name=backend.repo_name,
439 repo_name=backend.repo_name,
440 commit_id=raw_id, f_path='vcs/nodes.svg'),
440 commit_id=raw_id, f_path='vcs/nodes.svg'),
441 status=404)
441 status=404)
442
442
443 msg = """No such commit exists for this repository"""
443 msg = """No such commit exists for this repository"""
444 response.mustcontain(msg)
444 response.mustcontain(msg)
445
445
446 def test_download_file_wrong_f_path(self, backend):
446 def test_download_file_wrong_f_path(self, backend):
447 commit = backend.repo.get_commit(commit_idx=173)
447 commit = backend.repo.get_commit(commit_idx=173)
448 f_path = 'vcs/ERRORnodes.py'
448 f_path = 'vcs/ERRORnodes.py'
449
449
450 response = self.app.get(
450 response = self.app.get(
451 route_path('repo_file_download',
451 route_path('repo_file_download',
452 repo_name=backend.repo_name,
452 repo_name=backend.repo_name,
453 commit_id=commit.raw_id, f_path=f_path),
453 commit_id=commit.raw_id, f_path=f_path),
454 status=404)
454 status=404)
455
455
456 msg = (
456 msg = (
457 "There is no file nor directory at the given path: "
457 "There is no file nor directory at the given path: "
458 "`%s` at commit %s" % (f_path, commit.short_id))
458 "`%s` at commit %s" % (f_path, commit.short_id))
459 response.mustcontain(msg)
459 response.mustcontain(msg)
460
460
461 def test_file_raw(self, backend):
461 def test_file_raw(self, backend):
462 commit = backend.repo.get_commit(commit_idx=173)
462 commit = backend.repo.get_commit(commit_idx=173)
463 response = self.app.get(
463 response = self.app.get(
464 route_path('repo_file_raw',
464 route_path('repo_file_raw',
465 repo_name=backend.repo_name,
465 repo_name=backend.repo_name,
466 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
466 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
467
467
468 assert response.content_type == "text/plain"
468 assert response.content_type == "text/plain"
469
469
470 def test_file_raw_binary(self, backend):
470 def test_file_raw_binary(self, backend):
471 commit = backend.repo.get_commit()
471 commit = backend.repo.get_commit()
472 response = self.app.get(
472 response = self.app.get(
473 route_path('repo_file_raw',
473 route_path('repo_file_raw',
474 repo_name=backend.repo_name,
474 repo_name=backend.repo_name,
475 commit_id=commit.raw_id,
475 commit_id=commit.raw_id,
476 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
476 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
477
477
478 assert response.content_disposition == 'inline'
478 assert response.content_disposition == 'inline'
479
479
480 def test_raw_file_wrong_cs(self, backend):
480 def test_raw_file_wrong_cs(self, backend):
481 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
481 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
482
482
483 response = self.app.get(
483 response = self.app.get(
484 route_path('repo_file_raw',
484 route_path('repo_file_raw',
485 repo_name=backend.repo_name,
485 repo_name=backend.repo_name,
486 commit_id=raw_id, f_path='vcs/nodes.svg'),
486 commit_id=raw_id, f_path='vcs/nodes.svg'),
487 status=404)
487 status=404)
488
488
489 msg = """No such commit exists for this repository"""
489 msg = """No such commit exists for this repository"""
490 response.mustcontain(msg)
490 response.mustcontain(msg)
491
491
492 def test_raw_wrong_f_path(self, backend):
492 def test_raw_wrong_f_path(self, backend):
493 commit = backend.repo.get_commit(commit_idx=173)
493 commit = backend.repo.get_commit(commit_idx=173)
494 f_path = 'vcs/ERRORnodes.py'
494 f_path = 'vcs/ERRORnodes.py'
495 response = self.app.get(
495 response = self.app.get(
496 route_path('repo_file_raw',
496 route_path('repo_file_raw',
497 repo_name=backend.repo_name,
497 repo_name=backend.repo_name,
498 commit_id=commit.raw_id, f_path=f_path),
498 commit_id=commit.raw_id, f_path=f_path),
499 status=404)
499 status=404)
500
500
501 msg = (
501 msg = (
502 "There is no file nor directory at the given path: "
502 "There is no file nor directory at the given path: "
503 "`%s` at commit %s" % (f_path, commit.short_id))
503 "`%s` at commit %s" % (f_path, commit.short_id))
504 response.mustcontain(msg)
504 response.mustcontain(msg)
505
505
506 def test_raw_svg_should_not_be_rendered(self, backend):
506 def test_raw_svg_should_not_be_rendered(self, backend):
507 backend.create_repo()
507 backend.create_repo()
508 backend.ensure_file("xss.svg")
508 backend.ensure_file("xss.svg")
509 response = self.app.get(
509 response = self.app.get(
510 route_path('repo_file_raw',
510 route_path('repo_file_raw',
511 repo_name=backend.repo_name,
511 repo_name=backend.repo_name,
512 commit_id='tip', f_path='xss.svg'),)
512 commit_id='tip', f_path='xss.svg'),)
513 # If the content type is image/svg+xml then it allows to render HTML
513 # If the content type is image/svg+xml then it allows to render HTML
514 # and malicious SVG.
514 # and malicious SVG.
515 assert response.content_type == "text/plain"
515 assert response.content_type == "text/plain"
516
516
517
517
518 @pytest.mark.usefixtures("app")
518 @pytest.mark.usefixtures("app")
519 class TestRepositoryArchival(object):
519 class TestRepositoryArchival(object):
520
520
521 def test_archival(self, backend):
521 def test_archival(self, backend):
522 backend.enable_downloads()
522 backend.enable_downloads()
523 commit = backend.repo.get_commit(commit_idx=173)
523 commit = backend.repo.get_commit(commit_idx=173)
524 for archive, info in settings.ARCHIVE_SPECS.items():
524 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
525 mime_type, arch_ext = info
525
526 short = commit.short_id + arch_ext
526 short = commit.short_id + extension
527 fname = commit.raw_id + arch_ext
527 fname = commit.raw_id + extension
528 filename = '%s-%s' % (backend.repo_name, short)
528 filename = '%s-%s' % (backend.repo_name, short)
529 response = self.app.get(
529 response = self.app.get(
530 route_path('repo_archivefile',
530 route_path('repo_archivefile',
531 repo_name=backend.repo_name,
531 repo_name=backend.repo_name,
532 fname=fname))
532 fname=fname))
533
533
534 assert response.status == '200 OK'
534 assert response.status == '200 OK'
535 headers = [
535 headers = [
536 ('Content-Disposition', 'attachment; filename=%s' % filename),
536 ('Content-Disposition', 'attachment; filename=%s' % filename),
537 ('Content-Type', '%s' % mime_type),
537 ('Content-Type', '%s' % content_type),
538 ]
538 ]
539
539
540 for header in headers:
540 for header in headers:
541 assert header in response.headers.items()
541 assert header in response.headers.items()
542
542
543 @pytest.mark.parametrize('arch_ext',[
543 @pytest.mark.parametrize('arch_ext',[
544 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
544 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
545 def test_archival_wrong_ext(self, backend, arch_ext):
545 def test_archival_wrong_ext(self, backend, arch_ext):
546 backend.enable_downloads()
546 backend.enable_downloads()
547 commit = backend.repo.get_commit(commit_idx=173)
547 commit = backend.repo.get_commit(commit_idx=173)
548
548
549 fname = commit.raw_id + '.' + arch_ext
549 fname = commit.raw_id + '.' + arch_ext
550
550
551 response = self.app.get(
551 response = self.app.get(
552 route_path('repo_archivefile',
552 route_path('repo_archivefile',
553 repo_name=backend.repo_name,
553 repo_name=backend.repo_name,
554 fname=fname))
554 fname=fname))
555 response.mustcontain(
555 response.mustcontain(
556 'Unknown archive type for: `{}`'.format(fname))
556 'Unknown archive type for: `{}`'.format(fname))
557
557
558 @pytest.mark.parametrize('commit_id', [
558 @pytest.mark.parametrize('commit_id', [
559 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
559 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
560 def test_archival_wrong_commit_id(self, backend, commit_id):
560 def test_archival_wrong_commit_id(self, backend, commit_id):
561 backend.enable_downloads()
561 backend.enable_downloads()
562 fname = '%s.zip' % commit_id
562 fname = '%s.zip' % commit_id
563
563
564 response = self.app.get(
564 response = self.app.get(
565 route_path('repo_archivefile',
565 route_path('repo_archivefile',
566 repo_name=backend.repo_name,
566 repo_name=backend.repo_name,
567 fname=fname))
567 fname=fname))
568 response.mustcontain('Unknown commit_id')
568 response.mustcontain('Unknown commit_id')
569
569
570
570
571 @pytest.mark.usefixtures("app")
571 @pytest.mark.usefixtures("app")
572 class TestFilesDiff(object):
572 class TestFilesDiff(object):
573
573
574 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
574 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
575 def test_file_full_diff(self, backend, diff):
575 def test_file_full_diff(self, backend, diff):
576 commit1 = backend.repo.get_commit(commit_idx=-1)
576 commit1 = backend.repo.get_commit(commit_idx=-1)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
578
578
579 response = self.app.get(
579 response = self.app.get(
580 route_path('repo_files_diff',
580 route_path('repo_files_diff',
581 repo_name=backend.repo_name,
581 repo_name=backend.repo_name,
582 f_path='README'),
582 f_path='README'),
583 params={
583 params={
584 'diff1': commit2.raw_id,
584 'diff1': commit2.raw_id,
585 'diff2': commit1.raw_id,
585 'diff2': commit1.raw_id,
586 'fulldiff': '1',
586 'fulldiff': '1',
587 'diff': diff,
587 'diff': diff,
588 })
588 })
589
589
590 if diff == 'diff':
590 if diff == 'diff':
591 # use redirect since this is OLD view redirecting to compare page
591 # use redirect since this is OLD view redirecting to compare page
592 response = response.follow()
592 response = response.follow()
593
593
594 # It's a symlink to README.rst
594 # It's a symlink to README.rst
595 response.mustcontain('README.rst')
595 response.mustcontain('README.rst')
596 response.mustcontain('No newline at end of file')
596 response.mustcontain('No newline at end of file')
597
597
598 def test_file_binary_diff(self, backend):
598 def test_file_binary_diff(self, backend):
599 commits = [
599 commits = [
600 {'message': 'First commit'},
600 {'message': 'First commit'},
601 {'message': 'Commit with binary',
601 {'message': 'Commit with binary',
602 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
602 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
603 ]
603 ]
604 repo = backend.create_repo(commits=commits)
604 repo = backend.create_repo(commits=commits)
605
605
606 response = self.app.get(
606 response = self.app.get(
607 route_path('repo_files_diff',
607 route_path('repo_files_diff',
608 repo_name=backend.repo_name,
608 repo_name=backend.repo_name,
609 f_path='file.bin'),
609 f_path='file.bin'),
610 params={
610 params={
611 'diff1': repo.get_commit(commit_idx=0).raw_id,
611 'diff1': repo.get_commit(commit_idx=0).raw_id,
612 'diff2': repo.get_commit(commit_idx=1).raw_id,
612 'diff2': repo.get_commit(commit_idx=1).raw_id,
613 'fulldiff': '1',
613 'fulldiff': '1',
614 'diff': 'diff',
614 'diff': 'diff',
615 })
615 })
616 # use redirect since this is OLD view redirecting to compare page
616 # use redirect since this is OLD view redirecting to compare page
617 response = response.follow()
617 response = response.follow()
618 response.mustcontain('Expand 1 commit')
618 response.mustcontain('Expand 1 commit')
619 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
619 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
620
620
621 if backend.alias == 'svn':
621 if backend.alias == 'svn':
622 response.mustcontain('new file 10644')
622 response.mustcontain('new file 10644')
623 # TODO(marcink): SVN doesn't yet detect binary changes
623 # TODO(marcink): SVN doesn't yet detect binary changes
624 else:
624 else:
625 response.mustcontain('new file 100644')
625 response.mustcontain('new file 100644')
626 response.mustcontain('binary diff hidden')
626 response.mustcontain('binary diff hidden')
627
627
628 def test_diff_2way(self, backend):
628 def test_diff_2way(self, backend):
629 commit1 = backend.repo.get_commit(commit_idx=-1)
629 commit1 = backend.repo.get_commit(commit_idx=-1)
630 commit2 = backend.repo.get_commit(commit_idx=-2)
630 commit2 = backend.repo.get_commit(commit_idx=-2)
631 response = self.app.get(
631 response = self.app.get(
632 route_path('repo_files_diff_2way_redirect',
632 route_path('repo_files_diff_2way_redirect',
633 repo_name=backend.repo_name,
633 repo_name=backend.repo_name,
634 f_path='README'),
634 f_path='README'),
635 params={
635 params={
636 'diff1': commit2.raw_id,
636 'diff1': commit2.raw_id,
637 'diff2': commit1.raw_id,
637 'diff2': commit1.raw_id,
638 })
638 })
639 # use redirect since this is OLD view redirecting to compare page
639 # use redirect since this is OLD view redirecting to compare page
640 response = response.follow()
640 response = response.follow()
641
641
642 # It's a symlink to README.rst
642 # It's a symlink to README.rst
643 response.mustcontain('README.rst')
643 response.mustcontain('README.rst')
644 response.mustcontain('No newline at end of file')
644 response.mustcontain('No newline at end of file')
645
645
646 def test_requires_one_commit_id(self, backend, autologin_user):
646 def test_requires_one_commit_id(self, backend, autologin_user):
647 response = self.app.get(
647 response = self.app.get(
648 route_path('repo_files_diff',
648 route_path('repo_files_diff',
649 repo_name=backend.repo_name,
649 repo_name=backend.repo_name,
650 f_path='README.rst'),
650 f_path='README.rst'),
651 status=400)
651 status=400)
652 response.mustcontain(
652 response.mustcontain(
653 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
653 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
654
654
655 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
655 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
656 repo = vcsbackend.repo
656 repo = vcsbackend.repo
657 response = self.app.get(
657 response = self.app.get(
658 route_path('repo_files_diff',
658 route_path('repo_files_diff',
659 repo_name=repo.name,
659 repo_name=repo.name,
660 f_path='does-not-exist-in-any-commit'),
660 f_path='does-not-exist-in-any-commit'),
661 params={
661 params={
662 'diff1': repo[0].raw_id,
662 'diff1': repo[0].raw_id,
663 'diff2': repo[1].raw_id
663 'diff2': repo[1].raw_id
664 })
664 })
665
665
666 response = response.follow()
666 response = response.follow()
667 response.mustcontain('No files')
667 response.mustcontain('No files')
668
668
669 def test_returns_redirect_if_file_not_changed(self, backend):
669 def test_returns_redirect_if_file_not_changed(self, backend):
670 commit = backend.repo.get_commit(commit_idx=-1)
670 commit = backend.repo.get_commit(commit_idx=-1)
671 response = self.app.get(
671 response = self.app.get(
672 route_path('repo_files_diff_2way_redirect',
672 route_path('repo_files_diff_2way_redirect',
673 repo_name=backend.repo_name,
673 repo_name=backend.repo_name,
674 f_path='README'),
674 f_path='README'),
675 params={
675 params={
676 'diff1': commit.raw_id,
676 'diff1': commit.raw_id,
677 'diff2': commit.raw_id,
677 'diff2': commit.raw_id,
678 })
678 })
679
679
680 response = response.follow()
680 response = response.follow()
681 response.mustcontain('No files')
681 response.mustcontain('No files')
682 response.mustcontain('No commits in this compare')
682 response.mustcontain('No commits in this compare')
683
683
684 def test_supports_diff_to_different_path_svn(self, backend_svn):
684 def test_supports_diff_to_different_path_svn(self, backend_svn):
685 #TODO: check this case
685 #TODO: check this case
686 return
686 return
687
687
688 repo = backend_svn['svn-simple-layout'].scm_instance()
688 repo = backend_svn['svn-simple-layout'].scm_instance()
689 commit_id_1 = '24'
689 commit_id_1 = '24'
690 commit_id_2 = '26'
690 commit_id_2 = '26'
691
691
692 response = self.app.get(
692 response = self.app.get(
693 route_path('repo_files_diff',
693 route_path('repo_files_diff',
694 repo_name=backend_svn.repo_name,
694 repo_name=backend_svn.repo_name,
695 f_path='trunk/example.py'),
695 f_path='trunk/example.py'),
696 params={
696 params={
697 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
697 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
698 'diff2': commit_id_2,
698 'diff2': commit_id_2,
699 })
699 })
700
700
701 response = response.follow()
701 response = response.follow()
702 response.mustcontain(
702 response.mustcontain(
703 # diff contains this
703 # diff contains this
704 "Will print out a useful message on invocation.")
704 "Will print out a useful message on invocation.")
705
705
706 # Note: Expecting that we indicate the user what's being compared
706 # Note: Expecting that we indicate the user what's being compared
707 response.mustcontain("trunk/example.py")
707 response.mustcontain("trunk/example.py")
708 response.mustcontain("tags/v0.2/example.py")
708 response.mustcontain("tags/v0.2/example.py")
709
709
710 def test_show_rev_redirects_to_svn_path(self, backend_svn):
710 def test_show_rev_redirects_to_svn_path(self, backend_svn):
711 #TODO: check this case
711 #TODO: check this case
712 return
712 return
713
713
714 repo = backend_svn['svn-simple-layout'].scm_instance()
714 repo = backend_svn['svn-simple-layout'].scm_instance()
715 commit_id = repo[-1].raw_id
715 commit_id = repo[-1].raw_id
716
716
717 response = self.app.get(
717 response = self.app.get(
718 route_path('repo_files_diff',
718 route_path('repo_files_diff',
719 repo_name=backend_svn.repo_name,
719 repo_name=backend_svn.repo_name,
720 f_path='trunk/example.py'),
720 f_path='trunk/example.py'),
721 params={
721 params={
722 'diff1': 'branches/argparse/example.py@' + commit_id,
722 'diff1': 'branches/argparse/example.py@' + commit_id,
723 'diff2': commit_id,
723 'diff2': commit_id,
724 },
724 },
725 status=302)
725 status=302)
726 response = response.follow()
726 response = response.follow()
727 assert response.headers['Location'].endswith(
727 assert response.headers['Location'].endswith(
728 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
728 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
729
729
730 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
730 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
731 #TODO: check this case
731 #TODO: check this case
732 return
732 return
733
733
734 repo = backend_svn['svn-simple-layout'].scm_instance()
734 repo = backend_svn['svn-simple-layout'].scm_instance()
735 commit_id = repo[-1].raw_id
735 commit_id = repo[-1].raw_id
736 response = self.app.get(
736 response = self.app.get(
737 route_path('repo_files_diff',
737 route_path('repo_files_diff',
738 repo_name=backend_svn.repo_name,
738 repo_name=backend_svn.repo_name,
739 f_path='trunk/example.py'),
739 f_path='trunk/example.py'),
740 params={
740 params={
741 'diff1': 'branches/argparse/example.py@' + commit_id,
741 'diff1': 'branches/argparse/example.py@' + commit_id,
742 'diff2': commit_id,
742 'diff2': commit_id,
743 'show_rev': 'Show at Revision',
743 'show_rev': 'Show at Revision',
744 'annotate': 'true',
744 'annotate': 'true',
745 },
745 },
746 status=302)
746 status=302)
747 response = response.follow()
747 response = response.follow()
748 assert response.headers['Location'].endswith(
748 assert response.headers['Location'].endswith(
749 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
749 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
750
750
751
751
752 @pytest.mark.usefixtures("app", "autologin_user")
752 @pytest.mark.usefixtures("app", "autologin_user")
753 class TestModifyFilesWithWebInterface(object):
753 class TestModifyFilesWithWebInterface(object):
754
754
755 def test_add_file_view(self, backend):
755 def test_add_file_view(self, backend):
756 self.app.get(
756 self.app.get(
757 route_path('repo_files_add_file',
757 route_path('repo_files_add_file',
758 repo_name=backend.repo_name,
758 repo_name=backend.repo_name,
759 commit_id='tip', f_path='/')
759 commit_id='tip', f_path='/')
760 )
760 )
761
761
762 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
762 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
763 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
763 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
764 repo = backend.create_repo()
764 repo = backend.create_repo()
765 filename = 'init.py'
765 filename = 'init.py'
766 response = self.app.post(
766 response = self.app.post(
767 route_path('repo_files_create_file',
767 route_path('repo_files_create_file',
768 repo_name=backend.repo_name,
768 repo_name=backend.repo_name,
769 commit_id='tip', f_path='/'),
769 commit_id='tip', f_path='/'),
770 params={
770 params={
771 'content': "",
771 'content': "",
772 'filename': filename,
772 'filename': filename,
773 'location': "",
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 assert_session_flash(response,
778 'Successfully committed new file `{}`'.format(
778 'Successfully committed new file `{}`'.format(
779 os.path.join(filename)))
779 os.path.join(filename)))
780
780
781 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
781 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
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='tip', 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')
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')
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("location, filename", [
821 ('/abs', 'foo'),
821 ('/abs', 'foo'),
822 ('../rel', 'foo'),
822 ('../rel', 'foo'),
823 ('file/../foo', 'foo'),
823 ('file/../foo', 'foo'),
824 ])
824 ])
825 def test_add_file_into_repo_bad_filenames(
825 def test_add_file_into_repo_bad_filenames(
826 self, location, filename, backend, csrf_token):
826 self, location, filename, backend, csrf_token):
827 response = self.app.post(
827 response = self.app.post(
828 route_path('repo_files_create_file',
828 route_path('repo_files_create_file',
829 repo_name=backend.repo_name,
829 repo_name=backend.repo_name,
830 commit_id='tip', f_path='/'),
830 commit_id='tip', f_path='/'),
831 params={
831 params={
832 'content': "foo",
832 'content': "foo",
833 'filename': filename,
833 'filename': filename,
834 'location': location,
834 'location': location,
835 'csrf_token': csrf_token,
835 'csrf_token': csrf_token,
836 },
836 },
837 status=302)
837 status=302)
838
838
839 assert_session_flash(
839 assert_session_flash(
840 response,
840 response,
841 'The location specified must be a relative path and must not '
841 'The location specified must be a relative path and must not '
842 'contain .. in the path')
842 'contain .. in the path')
843
843
844 @pytest.mark.parametrize("cnt, location, filename", [
844 @pytest.mark.parametrize("cnt, location, filename", [
845 (1, '', 'foo.txt'),
845 (1, '', 'foo.txt'),
846 (2, 'dir', 'foo.rst'),
846 (2, 'dir', 'foo.rst'),
847 (3, 'rel/dir', 'foo.bar'),
847 (3, 'rel/dir', 'foo.bar'),
848 ])
848 ])
849 def test_add_file_into_repo(self, cnt, location, filename, backend,
849 def test_add_file_into_repo(self, cnt, location, filename, backend,
850 csrf_token):
850 csrf_token):
851 repo = backend.create_repo()
851 repo = backend.create_repo()
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='tip', f_path='/'),
856 params={
856 params={
857 'content': "foo",
857 'content': "foo",
858 'filename': filename,
858 'filename': filename,
859 'location': location,
859 'location': location,
860 'csrf_token': csrf_token,
860 'csrf_token': csrf_token,
861 },
861 },
862 status=302)
862 status=302)
863 assert_session_flash(response,
863 assert_session_flash(response,
864 'Successfully committed new file `{}`'.format(
864 'Successfully committed new file `{}`'.format(
865 os.path.join(location, filename)))
865 os.path.join(location, filename)))
866
866
867 def test_edit_file_view(self, backend):
867 def test_edit_file_view(self, backend):
868 response = self.app.get(
868 response = self.app.get(
869 route_path('repo_files_edit_file',
869 route_path('repo_files_edit_file',
870 repo_name=backend.repo_name,
870 repo_name=backend.repo_name,
871 commit_id=backend.default_head_id,
871 commit_id=backend.default_head_id,
872 f_path='vcs/nodes.py'),
872 f_path='vcs/nodes.py'),
873 status=200)
873 status=200)
874 response.mustcontain("Module holding everything related to vcs nodes.")
874 response.mustcontain("Module holding everything related to vcs nodes.")
875
875
876 def test_edit_file_view_not_on_branch(self, backend):
876 def test_edit_file_view_not_on_branch(self, backend):
877 repo = backend.create_repo()
877 repo = backend.create_repo()
878 backend.ensure_file("vcs/nodes.py")
878 backend.ensure_file("vcs/nodes.py")
879
879
880 response = self.app.get(
880 response = self.app.get(
881 route_path('repo_files_edit_file',
881 route_path('repo_files_edit_file',
882 repo_name=repo.repo_name,
882 repo_name=repo.repo_name,
883 commit_id='tip',
883 commit_id='tip',
884 f_path='vcs/nodes.py'),
884 f_path='vcs/nodes.py'),
885 status=302)
885 status=302)
886 assert_session_flash(
886 assert_session_flash(
887 response,
887 response,
888 'You can only edit files with commit being a valid branch')
888 'You can only edit files with commit being a valid branch')
889
889
890 def test_edit_file_view_commit_changes(self, backend, csrf_token):
890 def test_edit_file_view_commit_changes(self, backend, csrf_token):
891 repo = backend.create_repo()
891 repo = backend.create_repo()
892 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
892 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
893
893
894 response = self.app.post(
894 response = self.app.post(
895 route_path('repo_files_update_file',
895 route_path('repo_files_update_file',
896 repo_name=repo.repo_name,
896 repo_name=repo.repo_name,
897 commit_id=backend.default_head_id,
897 commit_id=backend.default_head_id,
898 f_path='vcs/nodes.py'),
898 f_path='vcs/nodes.py'),
899 params={
899 params={
900 'content': "print 'hello world'",
900 'content': "print 'hello world'",
901 'message': 'I committed',
901 'message': 'I committed',
902 'filename': "vcs/nodes.py",
902 'filename': "vcs/nodes.py",
903 'csrf_token': csrf_token,
903 'csrf_token': csrf_token,
904 },
904 },
905 status=302)
905 status=302)
906 assert_session_flash(
906 assert_session_flash(
907 response, 'Successfully committed changes to file `vcs/nodes.py`')
907 response, 'Successfully committed changes to file `vcs/nodes.py`')
908 tip = repo.get_commit(commit_idx=-1)
908 tip = repo.get_commit(commit_idx=-1)
909 assert tip.message == 'I committed'
909 assert tip.message == 'I committed'
910
910
911 def test_edit_file_view_commit_changes_default_message(self, backend,
911 def test_edit_file_view_commit_changes_default_message(self, backend,
912 csrf_token):
912 csrf_token):
913 repo = backend.create_repo()
913 repo = backend.create_repo()
914 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
914 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
915
915
916 commit_id = (
916 commit_id = (
917 backend.default_branch_name or
917 backend.default_branch_name or
918 backend.repo.scm_instance().commit_ids[-1])
918 backend.repo.scm_instance().commit_ids[-1])
919
919
920 response = self.app.post(
920 response = self.app.post(
921 route_path('repo_files_update_file',
921 route_path('repo_files_update_file',
922 repo_name=repo.repo_name,
922 repo_name=repo.repo_name,
923 commit_id=commit_id,
923 commit_id=commit_id,
924 f_path='vcs/nodes.py'),
924 f_path='vcs/nodes.py'),
925 params={
925 params={
926 'content': "print 'hello world'",
926 'content': "print 'hello world'",
927 'message': '',
927 'message': '',
928 'filename': "vcs/nodes.py",
928 'filename': "vcs/nodes.py",
929 'csrf_token': csrf_token,
929 'csrf_token': csrf_token,
930 },
930 },
931 status=302)
931 status=302)
932 assert_session_flash(
932 assert_session_flash(
933 response, 'Successfully committed changes to file `vcs/nodes.py`')
933 response, 'Successfully committed changes to file `vcs/nodes.py`')
934 tip = repo.get_commit(commit_idx=-1)
934 tip = repo.get_commit(commit_idx=-1)
935 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
935 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
936
936
937 def test_delete_file_view(self, backend):
937 def test_delete_file_view(self, backend):
938 self.app.get(
938 self.app.get(
939 route_path('repo_files_remove_file',
939 route_path('repo_files_remove_file',
940 repo_name=backend.repo_name,
940 repo_name=backend.repo_name,
941 commit_id=backend.default_head_id,
941 commit_id=backend.default_head_id,
942 f_path='vcs/nodes.py'),
942 f_path='vcs/nodes.py'),
943 status=200)
943 status=200)
944
944
945 def test_delete_file_view_not_on_branch(self, backend):
945 def test_delete_file_view_not_on_branch(self, backend):
946 repo = backend.create_repo()
946 repo = backend.create_repo()
947 backend.ensure_file('vcs/nodes.py')
947 backend.ensure_file('vcs/nodes.py')
948
948
949 response = self.app.get(
949 response = self.app.get(
950 route_path('repo_files_remove_file',
950 route_path('repo_files_remove_file',
951 repo_name=repo.repo_name,
951 repo_name=repo.repo_name,
952 commit_id='tip',
952 commit_id='tip',
953 f_path='vcs/nodes.py'),
953 f_path='vcs/nodes.py'),
954 status=302)
954 status=302)
955 assert_session_flash(
955 assert_session_flash(
956 response,
956 response,
957 'You can only delete files with commit being a valid branch')
957 'You can only delete files with commit being a valid branch')
958
958
959 def test_delete_file_view_commit_changes(self, backend, csrf_token):
959 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 repo = backend.create_repo()
960 repo = backend.create_repo()
961 backend.ensure_file("vcs/nodes.py")
961 backend.ensure_file("vcs/nodes.py")
962
962
963 response = self.app.post(
963 response = self.app.post(
964 route_path('repo_files_delete_file',
964 route_path('repo_files_delete_file',
965 repo_name=repo.repo_name,
965 repo_name=repo.repo_name,
966 commit_id=backend.default_head_id,
966 commit_id=backend.default_head_id,
967 f_path='vcs/nodes.py'),
967 f_path='vcs/nodes.py'),
968 params={
968 params={
969 'message': 'i commited',
969 'message': 'i commited',
970 'csrf_token': csrf_token,
970 'csrf_token': csrf_token,
971 },
971 },
972 status=302)
972 status=302)
973 assert_session_flash(
973 assert_session_flash(
974 response, 'Successfully deleted file `vcs/nodes.py`')
974 response, 'Successfully deleted file `vcs/nodes.py`')
975
975
976
976
977 @pytest.mark.usefixtures("app")
977 @pytest.mark.usefixtures("app")
978 class TestFilesViewOtherCases(object):
978 class TestFilesViewOtherCases(object):
979
979
980 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
980 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 self, backend_stub, autologin_regular_user, user_regular,
981 self, backend_stub, autologin_regular_user, user_regular,
982 user_util):
982 user_util):
983
983
984 repo = backend_stub.create_repo()
984 repo = backend_stub.create_repo()
985 user_util.grant_user_permission_to_repo(
985 user_util.grant_user_permission_to_repo(
986 repo, user_regular, 'repository.write')
986 repo, user_regular, 'repository.write')
987 response = self.app.get(
987 response = self.app.get(
988 route_path('repo_files',
988 route_path('repo_files',
989 repo_name=repo.repo_name,
989 repo_name=repo.repo_name,
990 commit_id='tip', f_path='/'))
990 commit_id='tip', f_path='/'))
991
991
992 repo_file_add_url = route_path(
992 repo_file_add_url = route_path(
993 'repo_files_add_file',
993 'repo_files_add_file',
994 repo_name=repo.repo_name,
994 repo_name=repo.repo_name,
995 commit_id=0, f_path='') + '#edit'
995 commit_id=0, f_path='') + '#edit'
996
996
997 assert_session_flash(
997 assert_session_flash(
998 response,
998 response,
999 'There are no files yet. <a class="alert-link" '
999 'There are no files yet. <a class="alert-link" '
1000 'href="{}">Click here to add a new file.</a>'
1000 'href="{}">Click here to add a new file.</a>'
1001 .format(repo_file_add_url))
1001 .format(repo_file_add_url))
1002
1002
1003 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1003 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 self, backend_stub, autologin_regular_user):
1004 self, backend_stub, autologin_regular_user):
1005 repo = backend_stub.create_repo()
1005 repo = backend_stub.create_repo()
1006 # init session for anon user
1006 # init session for anon user
1007 route_path('repo_summary', repo_name=repo.repo_name)
1007 route_path('repo_summary', repo_name=repo.repo_name)
1008
1008
1009 repo_file_add_url = route_path(
1009 repo_file_add_url = route_path(
1010 'repo_files_add_file',
1010 'repo_files_add_file',
1011 repo_name=repo.repo_name,
1011 repo_name=repo.repo_name,
1012 commit_id=0, f_path='') + '#edit'
1012 commit_id=0, f_path='') + '#edit'
1013
1013
1014 response = self.app.get(
1014 response = self.app.get(
1015 route_path('repo_files',
1015 route_path('repo_files',
1016 repo_name=repo.repo_name,
1016 repo_name=repo.repo_name,
1017 commit_id='tip', f_path='/'))
1017 commit_id='tip', f_path='/'))
1018
1018
1019 assert_session_flash(response, no_=repo_file_add_url)
1019 assert_session_flash(response, no_=repo_file_add_url)
1020
1020
1021 @pytest.mark.parametrize('file_node', [
1021 @pytest.mark.parametrize('file_node', [
1022 'archive/file.zip',
1022 'archive/file.zip',
1023 'diff/my-file.txt',
1023 'diff/my-file.txt',
1024 'render.py',
1024 'render.py',
1025 'render',
1025 'render',
1026 'remove_file',
1026 'remove_file',
1027 'remove_file/to-delete.txt',
1027 'remove_file/to-delete.txt',
1028 ])
1028 ])
1029 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1029 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 backend.create_repo()
1030 backend.create_repo()
1031 backend.ensure_file(file_node)
1031 backend.ensure_file(file_node)
1032
1032
1033 self.app.get(
1033 self.app.get(
1034 route_path('repo_files',
1034 route_path('repo_files',
1035 repo_name=backend.repo_name,
1035 repo_name=backend.repo_name,
1036 commit_id='tip', f_path=file_node),
1036 commit_id='tip', f_path=file_node),
1037 status=200)
1037 status=200)
1038
1038
1039
1039
1040 class TestAdjustFilePathForSvn(object):
1040 class TestAdjustFilePathForSvn(object):
1041 """
1041 """
1042 SVN specific adjustments of node history in RepoFilesView.
1042 SVN specific adjustments of node history in RepoFilesView.
1043 """
1043 """
1044
1044
1045 def test_returns_path_relative_to_matched_reference(self):
1045 def test_returns_path_relative_to_matched_reference(self):
1046 repo = self._repo(branches=['trunk'])
1046 repo = self._repo(branches=['trunk'])
1047 self.assert_file_adjustment('trunk/file', 'file', repo)
1047 self.assert_file_adjustment('trunk/file', 'file', repo)
1048
1048
1049 def test_does_not_modify_file_if_no_reference_matches(self):
1049 def test_does_not_modify_file_if_no_reference_matches(self):
1050 repo = self._repo(branches=['trunk'])
1050 repo = self._repo(branches=['trunk'])
1051 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1051 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052
1052
1053 def test_does_not_adjust_partial_directory_names(self):
1053 def test_does_not_adjust_partial_directory_names(self):
1054 repo = self._repo(branches=['trun'])
1054 repo = self._repo(branches=['trun'])
1055 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1055 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056
1056
1057 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1057 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1058 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1059 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060
1060
1061 def assert_file_adjustment(self, f_path, expected, repo):
1061 def assert_file_adjustment(self, f_path, expected, repo):
1062 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1062 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 assert result == expected
1063 assert result == expected
1064
1064
1065 def _repo(self, branches=None):
1065 def _repo(self, branches=None):
1066 repo = mock.Mock()
1066 repo = mock.Mock()
1067 repo.branches = OrderedDict((name, '0') for name in branches or [])
1067 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 repo.tags = {}
1068 repo.tags = {}
1069 return repo
1069 return repo
@@ -1,1418 +1,1417 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
28
29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 from pyramid.view import view_config
30 from pyramid.view import view_config
31 from pyramid.renderers import render
31 from pyramid.renderers import render
32 from pyramid.response import Response
32 from pyramid.response import Response
33
33
34 import rhodecode
34 import rhodecode
35 from rhodecode.apps._base import RepoAppView
35 from rhodecode.apps._base import RepoAppView
36
36
37
37
38 from rhodecode.lib import diffs, helpers as h, rc_cache
38 from rhodecode.lib import diffs, helpers as h, rc_cache
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.view_utils import parse_path_ref
40 from rhodecode.lib.view_utils import parse_path_ref
41 from rhodecode.lib.exceptions import NonRelativePathError
41 from rhodecode.lib.exceptions import NonRelativePathError
42 from rhodecode.lib.codeblocks import (
42 from rhodecode.lib.codeblocks import (
43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
44 from rhodecode.lib.utils2 import (
44 from rhodecode.lib.utils2 import (
45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1)
45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1)
46 from rhodecode.lib.auth import (
46 from rhodecode.lib.auth import (
47 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
47 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
48 from rhodecode.lib.vcs import path as vcspath
48 from rhodecode.lib.vcs import path as vcspath
49 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 from rhodecode.lib.vcs.conf import settings
50 from rhodecode.lib.vcs.conf import settings
51 from rhodecode.lib.vcs.nodes import FileNode
51 from rhodecode.lib.vcs.nodes import FileNode
52 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
53 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
53 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
54 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
54 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
55 NodeDoesNotExistError, CommitError, NodeError)
55 NodeDoesNotExistError, CommitError, NodeError)
56
56
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.db import Repository
58 from rhodecode.model.db import Repository
59
59
60 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
61
61
62
62
63 class RepoFilesView(RepoAppView):
63 class RepoFilesView(RepoAppView):
64
64
65 @staticmethod
65 @staticmethod
66 def adjust_file_path_for_svn(f_path, repo):
66 def adjust_file_path_for_svn(f_path, repo):
67 """
67 """
68 Computes the relative path of `f_path`.
68 Computes the relative path of `f_path`.
69
69
70 This is mainly based on prefix matching of the recognized tags and
70 This is mainly based on prefix matching of the recognized tags and
71 branches in the underlying repository.
71 branches in the underlying repository.
72 """
72 """
73 tags_and_branches = itertools.chain(
73 tags_and_branches = itertools.chain(
74 repo.branches.iterkeys(),
74 repo.branches.iterkeys(),
75 repo.tags.iterkeys())
75 repo.tags.iterkeys())
76 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
76 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
77
77
78 for name in tags_and_branches:
78 for name in tags_and_branches:
79 if f_path.startswith('{}/'.format(name)):
79 if f_path.startswith('{}/'.format(name)):
80 f_path = vcspath.relpath(f_path, name)
80 f_path = vcspath.relpath(f_path, name)
81 break
81 break
82 return f_path
82 return f_path
83
83
84 def load_default_context(self):
84 def load_default_context(self):
85 c = self._get_local_tmpl_context(include_app_defaults=True)
85 c = self._get_local_tmpl_context(include_app_defaults=True)
86 c.rhodecode_repo = self.rhodecode_vcs_repo
86 c.rhodecode_repo = self.rhodecode_vcs_repo
87 c.enable_downloads = self.db_repo.enable_downloads
87 c.enable_downloads = self.db_repo.enable_downloads
88 return c
88 return c
89
89
90 def _ensure_not_locked(self):
90 def _ensure_not_locked(self):
91 _ = self.request.translate
91 _ = self.request.translate
92
92
93 repo = self.db_repo
93 repo = self.db_repo
94 if repo.enable_locking and repo.locked[0]:
94 if repo.enable_locking and repo.locked[0]:
95 h.flash(_('This repository has been locked by %s on %s')
95 h.flash(_('This repository has been locked by %s on %s')
96 % (h.person_by_id(repo.locked[0]),
96 % (h.person_by_id(repo.locked[0]),
97 h.format_date(h.time_to_datetime(repo.locked[1]))),
97 h.format_date(h.time_to_datetime(repo.locked[1]))),
98 'warning')
98 'warning')
99 files_url = h.route_path(
99 files_url = h.route_path(
100 'repo_files:default_path',
100 'repo_files:default_path',
101 repo_name=self.db_repo_name, commit_id='tip')
101 repo_name=self.db_repo_name, commit_id='tip')
102 raise HTTPFound(files_url)
102 raise HTTPFound(files_url)
103
103
104 def check_branch_permission(self, branch_name):
104 def check_branch_permission(self, branch_name):
105 _ = self.request.translate
105 _ = self.request.translate
106
106
107 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
107 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
108 self.db_repo_name, branch_name)
108 self.db_repo_name, branch_name)
109 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
109 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
110 h.flash(
110 h.flash(
111 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
111 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
112 'warning')
112 'warning')
113 files_url = h.route_path(
113 files_url = h.route_path(
114 'repo_files:default_path',
114 'repo_files:default_path',
115 repo_name=self.db_repo_name, commit_id='tip')
115 repo_name=self.db_repo_name, commit_id='tip')
116 raise HTTPFound(files_url)
116 raise HTTPFound(files_url)
117
117
118 def _get_commit_and_path(self):
118 def _get_commit_and_path(self):
119 default_commit_id = self.db_repo.landing_rev[1]
119 default_commit_id = self.db_repo.landing_rev[1]
120 default_f_path = '/'
120 default_f_path = '/'
121
121
122 commit_id = self.request.matchdict.get(
122 commit_id = self.request.matchdict.get(
123 'commit_id', default_commit_id)
123 'commit_id', default_commit_id)
124 f_path = self._get_f_path(self.request.matchdict, default_f_path)
124 f_path = self._get_f_path(self.request.matchdict, default_f_path)
125 return commit_id, f_path
125 return commit_id, f_path
126
126
127 def _get_default_encoding(self, c):
127 def _get_default_encoding(self, c):
128 enc_list = getattr(c, 'default_encodings', [])
128 enc_list = getattr(c, 'default_encodings', [])
129 return enc_list[0] if enc_list else 'UTF-8'
129 return enc_list[0] if enc_list else 'UTF-8'
130
130
131 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
131 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
132 """
132 """
133 This is a safe way to get commit. If an error occurs it redirects to
133 This is a safe way to get commit. If an error occurs it redirects to
134 tip with proper message
134 tip with proper message
135
135
136 :param commit_id: id of commit to fetch
136 :param commit_id: id of commit to fetch
137 :param redirect_after: toggle redirection
137 :param redirect_after: toggle redirection
138 """
138 """
139 _ = self.request.translate
139 _ = self.request.translate
140
140
141 try:
141 try:
142 return self.rhodecode_vcs_repo.get_commit(commit_id)
142 return self.rhodecode_vcs_repo.get_commit(commit_id)
143 except EmptyRepositoryError:
143 except EmptyRepositoryError:
144 if not redirect_after:
144 if not redirect_after:
145 return None
145 return None
146
146
147 _url = h.route_path(
147 _url = h.route_path(
148 'repo_files_add_file',
148 'repo_files_add_file',
149 repo_name=self.db_repo_name, commit_id=0, f_path='',
149 repo_name=self.db_repo_name, commit_id=0, f_path='',
150 _anchor='edit')
150 _anchor='edit')
151
151
152 if h.HasRepoPermissionAny(
152 if h.HasRepoPermissionAny(
153 'repository.write', 'repository.admin')(self.db_repo_name):
153 'repository.write', 'repository.admin')(self.db_repo_name):
154 add_new = h.link_to(
154 add_new = h.link_to(
155 _('Click here to add a new file.'), _url, class_="alert-link")
155 _('Click here to add a new file.'), _url, class_="alert-link")
156 else:
156 else:
157 add_new = ""
157 add_new = ""
158
158
159 h.flash(h.literal(
159 h.flash(h.literal(
160 _('There are no files yet. %s') % add_new), category='warning')
160 _('There are no files yet. %s') % add_new), category='warning')
161 raise HTTPFound(
161 raise HTTPFound(
162 h.route_path('repo_summary', repo_name=self.db_repo_name))
162 h.route_path('repo_summary', repo_name=self.db_repo_name))
163
163
164 except (CommitDoesNotExistError, LookupError):
164 except (CommitDoesNotExistError, LookupError):
165 msg = _('No such commit exists for this repository')
165 msg = _('No such commit exists for this repository')
166 h.flash(msg, category='error')
166 h.flash(msg, category='error')
167 raise HTTPNotFound()
167 raise HTTPNotFound()
168 except RepositoryError as e:
168 except RepositoryError as e:
169 h.flash(safe_str(h.escape(e)), category='error')
169 h.flash(safe_str(h.escape(e)), category='error')
170 raise HTTPNotFound()
170 raise HTTPNotFound()
171
171
172 def _get_filenode_or_redirect(self, commit_obj, path):
172 def _get_filenode_or_redirect(self, commit_obj, path):
173 """
173 """
174 Returns file_node, if error occurs or given path is directory,
174 Returns file_node, if error occurs or given path is directory,
175 it'll redirect to top level path
175 it'll redirect to top level path
176 """
176 """
177 _ = self.request.translate
177 _ = self.request.translate
178
178
179 try:
179 try:
180 file_node = commit_obj.get_node(path)
180 file_node = commit_obj.get_node(path)
181 if file_node.is_dir():
181 if file_node.is_dir():
182 raise RepositoryError('The given path is a directory')
182 raise RepositoryError('The given path is a directory')
183 except CommitDoesNotExistError:
183 except CommitDoesNotExistError:
184 log.exception('No such commit exists for this repository')
184 log.exception('No such commit exists for this repository')
185 h.flash(_('No such commit exists for this repository'), category='error')
185 h.flash(_('No such commit exists for this repository'), category='error')
186 raise HTTPNotFound()
186 raise HTTPNotFound()
187 except RepositoryError as e:
187 except RepositoryError as e:
188 log.warning('Repository error while fetching '
188 log.warning('Repository error while fetching '
189 'filenode `%s`. Err:%s', path, e)
189 'filenode `%s`. Err:%s', path, e)
190 h.flash(safe_str(h.escape(e)), category='error')
190 h.flash(safe_str(h.escape(e)), category='error')
191 raise HTTPNotFound()
191 raise HTTPNotFound()
192
192
193 return file_node
193 return file_node
194
194
195 def _is_valid_head(self, commit_id, repo):
195 def _is_valid_head(self, commit_id, repo):
196 branch_name = sha_commit_id = ''
196 branch_name = sha_commit_id = ''
197 is_head = False
197 is_head = False
198
198
199 if h.is_svn(repo) and not repo.is_empty():
199 if h.is_svn(repo) and not repo.is_empty():
200 # Note: Subversion only has one head.
200 # Note: Subversion only has one head.
201 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
201 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
202 is_head = True
202 is_head = True
203 return branch_name, sha_commit_id, is_head
203 return branch_name, sha_commit_id, is_head
204
204
205 for _branch_name, branch_commit_id in repo.branches.items():
205 for _branch_name, branch_commit_id in repo.branches.items():
206 # simple case we pass in branch name, it's a HEAD
206 # simple case we pass in branch name, it's a HEAD
207 if commit_id == _branch_name:
207 if commit_id == _branch_name:
208 is_head = True
208 is_head = True
209 branch_name = _branch_name
209 branch_name = _branch_name
210 sha_commit_id = branch_commit_id
210 sha_commit_id = branch_commit_id
211 break
211 break
212 # case when we pass in full sha commit_id, which is a head
212 # case when we pass in full sha commit_id, which is a head
213 elif commit_id == branch_commit_id:
213 elif commit_id == branch_commit_id:
214 is_head = True
214 is_head = True
215 branch_name = _branch_name
215 branch_name = _branch_name
216 sha_commit_id = branch_commit_id
216 sha_commit_id = branch_commit_id
217 break
217 break
218
218
219 # checked branches, means we only need to try to get the branch/commit_sha
219 # checked branches, means we only need to try to get the branch/commit_sha
220 if not repo.is_empty:
220 if not repo.is_empty:
221 commit = repo.get_commit(commit_id=commit_id)
221 commit = repo.get_commit(commit_id=commit_id)
222 if commit:
222 if commit:
223 branch_name = commit.branch
223 branch_name = commit.branch
224 sha_commit_id = commit.raw_id
224 sha_commit_id = commit.raw_id
225
225
226 return branch_name, sha_commit_id, is_head
226 return branch_name, sha_commit_id, is_head
227
227
228 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
228 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
229
229
230 repo_id = self.db_repo.repo_id
230 repo_id = self.db_repo.repo_id
231 force_recache = self.get_recache_flag()
231 force_recache = self.get_recache_flag()
232
232
233 cache_seconds = safe_int(
233 cache_seconds = safe_int(
234 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
234 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
235 cache_on = not force_recache and cache_seconds > 0
235 cache_on = not force_recache and cache_seconds > 0
236 log.debug(
236 log.debug(
237 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
237 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
238 'with caching: %s[TTL: %ss]' % (
238 'with caching: %s[TTL: %ss]' % (
239 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
239 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
240
240
241 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
241 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
242 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
242 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
243
243
244 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
244 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
245 condition=cache_on)
245 condition=cache_on)
246 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
246 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
247 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
247 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
248 ver, repo_id, commit_id, f_path)
248 ver, repo_id, commit_id, f_path)
249
249
250 c.full_load = full_load
250 c.full_load = full_load
251 return render(
251 return render(
252 'rhodecode:templates/files/files_browser_tree.mako',
252 'rhodecode:templates/files/files_browser_tree.mako',
253 self._get_template_context(c), self.request)
253 self._get_template_context(c), self.request)
254
254
255 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
255 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
256
256
257 def _get_archive_spec(self, fname):
257 def _get_archive_spec(self, fname):
258 log.debug('Detecting archive spec for: `%s`', fname)
258 log.debug('Detecting archive spec for: `%s`', fname)
259
259
260 fileformat = None
260 fileformat = None
261 ext = None
261 ext = None
262 content_type = None
262 content_type = None
263 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
263 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
264 content_type, extension = ext_data
265
264
266 if fname.endswith(extension):
265 if fname.endswith(extension):
267 fileformat = a_type
266 fileformat = a_type
268 log.debug('archive is of type: %s', fileformat)
267 log.debug('archive is of type: %s', fileformat)
269 ext = extension
268 ext = extension
270 break
269 break
271
270
272 if not fileformat:
271 if not fileformat:
273 raise ValueError()
272 raise ValueError()
274
273
275 # left over part of whole fname is the commit
274 # left over part of whole fname is the commit
276 commit_id = fname[:-len(ext)]
275 commit_id = fname[:-len(ext)]
277
276
278 return commit_id, ext, fileformat, content_type
277 return commit_id, ext, fileformat, content_type
279
278
280 @LoginRequired()
279 @LoginRequired()
281 @HasRepoPermissionAnyDecorator(
280 @HasRepoPermissionAnyDecorator(
282 'repository.read', 'repository.write', 'repository.admin')
281 'repository.read', 'repository.write', 'repository.admin')
283 @view_config(
282 @view_config(
284 route_name='repo_archivefile', request_method='GET',
283 route_name='repo_archivefile', request_method='GET',
285 renderer=None)
284 renderer=None)
286 def repo_archivefile(self):
285 def repo_archivefile(self):
287 # archive cache config
286 # archive cache config
288 from rhodecode import CONFIG
287 from rhodecode import CONFIG
289 _ = self.request.translate
288 _ = self.request.translate
290 self.load_default_context()
289 self.load_default_context()
291 default_at_path = '/'
290 default_at_path = '/'
292 fname = self.request.matchdict['fname']
291 fname = self.request.matchdict['fname']
293 subrepos = self.request.GET.get('subrepos') == 'true'
292 subrepos = self.request.GET.get('subrepos') == 'true'
294 at_path = self.request.GET.get('at_path') or default_at_path
293 at_path = self.request.GET.get('at_path') or default_at_path
295
294
296 if not self.db_repo.enable_downloads:
295 if not self.db_repo.enable_downloads:
297 return Response(_('Downloads disabled'))
296 return Response(_('Downloads disabled'))
298
297
299 try:
298 try:
300 commit_id, ext, fileformat, content_type = \
299 commit_id, ext, fileformat, content_type = \
301 self._get_archive_spec(fname)
300 self._get_archive_spec(fname)
302 except ValueError:
301 except ValueError:
303 return Response(_('Unknown archive type for: `{}`').format(
302 return Response(_('Unknown archive type for: `{}`').format(
304 h.escape(fname)))
303 h.escape(fname)))
305
304
306 try:
305 try:
307 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
306 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
308 except CommitDoesNotExistError:
307 except CommitDoesNotExistError:
309 return Response(_('Unknown commit_id {}').format(
308 return Response(_('Unknown commit_id {}').format(
310 h.escape(commit_id)))
309 h.escape(commit_id)))
311 except EmptyRepositoryError:
310 except EmptyRepositoryError:
312 return Response(_('Empty repository'))
311 return Response(_('Empty repository'))
313
312
314 try:
313 try:
315 at_path = commit.get_node(at_path).path or default_at_path
314 at_path = commit.get_node(at_path).path or default_at_path
316 except Exception:
315 except Exception:
317 return Response(_('No node at path {} for this repository').format(at_path))
316 return Response(_('No node at path {} for this repository').format(at_path))
318
317
319 path_sha = sha1(at_path)[:8]
318 path_sha = sha1(at_path)[:8]
320
319
321 # original backward compat name of archive
320 # original backward compat name of archive
322 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
321 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
323 short_sha = safe_str(commit.short_id)
322 short_sha = safe_str(commit.short_id)
324
323
325 if at_path == default_at_path:
324 if at_path == default_at_path:
326 archive_name = '{}-{}{}{}'.format(
325 archive_name = '{}-{}{}{}'.format(
327 clean_name,
326 clean_name,
328 '-sub' if subrepos else '',
327 '-sub' if subrepos else '',
329 short_sha,
328 short_sha,
330 ext)
329 ext)
331 # custom path and new name
330 # custom path and new name
332 else:
331 else:
333 archive_name = '{}-{}{}-{}{}'.format(
332 archive_name = '{}-{}{}-{}{}'.format(
334 clean_name,
333 clean_name,
335 '-sub' if subrepos else '',
334 '-sub' if subrepos else '',
336 short_sha,
335 short_sha,
337 path_sha,
336 path_sha,
338 ext)
337 ext)
339
338
340 use_cached_archive = False
339 use_cached_archive = False
341 archive_cache_enabled = CONFIG.get(
340 archive_cache_enabled = CONFIG.get(
342 'archive_cache_dir') and not self.request.GET.get('no_cache')
341 'archive_cache_dir') and not self.request.GET.get('no_cache')
343 cached_archive_path = None
342 cached_archive_path = None
344
343
345 if archive_cache_enabled:
344 if archive_cache_enabled:
346 # check if we it's ok to write
345 # check if we it's ok to write
347 if not os.path.isdir(CONFIG['archive_cache_dir']):
346 if not os.path.isdir(CONFIG['archive_cache_dir']):
348 os.makedirs(CONFIG['archive_cache_dir'])
347 os.makedirs(CONFIG['archive_cache_dir'])
349 cached_archive_path = os.path.join(
348 cached_archive_path = os.path.join(
350 CONFIG['archive_cache_dir'], archive_name)
349 CONFIG['archive_cache_dir'], archive_name)
351 if os.path.isfile(cached_archive_path):
350 if os.path.isfile(cached_archive_path):
352 log.debug('Found cached archive in %s', cached_archive_path)
351 log.debug('Found cached archive in %s', cached_archive_path)
353 fd, archive = None, cached_archive_path
352 fd, archive = None, cached_archive_path
354 use_cached_archive = True
353 use_cached_archive = True
355 else:
354 else:
356 log.debug('Archive %s is not yet cached', archive_name)
355 log.debug('Archive %s is not yet cached', archive_name)
357
356
358 if not use_cached_archive:
357 if not use_cached_archive:
359 # generate new archive
358 # generate new archive
360 fd, archive = tempfile.mkstemp()
359 fd, archive = tempfile.mkstemp()
361 log.debug('Creating new temp archive in %s', archive)
360 log.debug('Creating new temp archive in %s', archive)
362 try:
361 try:
363 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
362 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
364 archive_at_path=at_path)
363 archive_at_path=at_path)
365 except ImproperArchiveTypeError:
364 except ImproperArchiveTypeError:
366 return _('Unknown archive type')
365 return _('Unknown archive type')
367 if archive_cache_enabled:
366 if archive_cache_enabled:
368 # if we generated the archive and we have cache enabled
367 # if we generated the archive and we have cache enabled
369 # let's use this for future
368 # let's use this for future
370 log.debug('Storing new archive in %s', cached_archive_path)
369 log.debug('Storing new archive in %s', cached_archive_path)
371 shutil.move(archive, cached_archive_path)
370 shutil.move(archive, cached_archive_path)
372 archive = cached_archive_path
371 archive = cached_archive_path
373
372
374 # store download action
373 # store download action
375 audit_logger.store_web(
374 audit_logger.store_web(
376 'repo.archive.download', action_data={
375 'repo.archive.download', action_data={
377 'user_agent': self.request.user_agent,
376 'user_agent': self.request.user_agent,
378 'archive_name': archive_name,
377 'archive_name': archive_name,
379 'archive_spec': fname,
378 'archive_spec': fname,
380 'archive_cached': use_cached_archive},
379 'archive_cached': use_cached_archive},
381 user=self._rhodecode_user,
380 user=self._rhodecode_user,
382 repo=self.db_repo,
381 repo=self.db_repo,
383 commit=True
382 commit=True
384 )
383 )
385
384
386 def get_chunked_archive(archive_path):
385 def get_chunked_archive(archive_path):
387 with open(archive_path, 'rb') as stream:
386 with open(archive_path, 'rb') as stream:
388 while True:
387 while True:
389 data = stream.read(16 * 1024)
388 data = stream.read(16 * 1024)
390 if not data:
389 if not data:
391 if fd: # fd means we used temporary file
390 if fd: # fd means we used temporary file
392 os.close(fd)
391 os.close(fd)
393 if not archive_cache_enabled:
392 if not archive_cache_enabled:
394 log.debug('Destroying temp archive %s', archive_path)
393 log.debug('Destroying temp archive %s', archive_path)
395 os.remove(archive_path)
394 os.remove(archive_path)
396 break
395 break
397 yield data
396 yield data
398
397
399 response = Response(app_iter=get_chunked_archive(archive))
398 response = Response(app_iter=get_chunked_archive(archive))
400 response.content_disposition = str(
399 response.content_disposition = str(
401 'attachment; filename=%s' % archive_name)
400 'attachment; filename=%s' % archive_name)
402 response.content_type = str(content_type)
401 response.content_type = str(content_type)
403
402
404 return response
403 return response
405
404
406 def _get_file_node(self, commit_id, f_path):
405 def _get_file_node(self, commit_id, f_path):
407 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
406 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
408 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
407 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
409 try:
408 try:
410 node = commit.get_node(f_path)
409 node = commit.get_node(f_path)
411 if node.is_dir():
410 if node.is_dir():
412 raise NodeError('%s path is a %s not a file'
411 raise NodeError('%s path is a %s not a file'
413 % (node, type(node)))
412 % (node, type(node)))
414 except NodeDoesNotExistError:
413 except NodeDoesNotExistError:
415 commit = EmptyCommit(
414 commit = EmptyCommit(
416 commit_id=commit_id,
415 commit_id=commit_id,
417 idx=commit.idx,
416 idx=commit.idx,
418 repo=commit.repository,
417 repo=commit.repository,
419 alias=commit.repository.alias,
418 alias=commit.repository.alias,
420 message=commit.message,
419 message=commit.message,
421 author=commit.author,
420 author=commit.author,
422 date=commit.date)
421 date=commit.date)
423 node = FileNode(f_path, '', commit=commit)
422 node = FileNode(f_path, '', commit=commit)
424 else:
423 else:
425 commit = EmptyCommit(
424 commit = EmptyCommit(
426 repo=self.rhodecode_vcs_repo,
425 repo=self.rhodecode_vcs_repo,
427 alias=self.rhodecode_vcs_repo.alias)
426 alias=self.rhodecode_vcs_repo.alias)
428 node = FileNode(f_path, '', commit=commit)
427 node = FileNode(f_path, '', commit=commit)
429 return node
428 return node
430
429
431 @LoginRequired()
430 @LoginRequired()
432 @HasRepoPermissionAnyDecorator(
431 @HasRepoPermissionAnyDecorator(
433 'repository.read', 'repository.write', 'repository.admin')
432 'repository.read', 'repository.write', 'repository.admin')
434 @view_config(
433 @view_config(
435 route_name='repo_files_diff', request_method='GET',
434 route_name='repo_files_diff', request_method='GET',
436 renderer=None)
435 renderer=None)
437 def repo_files_diff(self):
436 def repo_files_diff(self):
438 c = self.load_default_context()
437 c = self.load_default_context()
439 f_path = self._get_f_path(self.request.matchdict)
438 f_path = self._get_f_path(self.request.matchdict)
440 diff1 = self.request.GET.get('diff1', '')
439 diff1 = self.request.GET.get('diff1', '')
441 diff2 = self.request.GET.get('diff2', '')
440 diff2 = self.request.GET.get('diff2', '')
442
441
443 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
442 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
444
443
445 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
444 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
446 line_context = self.request.GET.get('context', 3)
445 line_context = self.request.GET.get('context', 3)
447
446
448 if not any((diff1, diff2)):
447 if not any((diff1, diff2)):
449 h.flash(
448 h.flash(
450 'Need query parameter "diff1" or "diff2" to generate a diff.',
449 'Need query parameter "diff1" or "diff2" to generate a diff.',
451 category='error')
450 category='error')
452 raise HTTPBadRequest()
451 raise HTTPBadRequest()
453
452
454 c.action = self.request.GET.get('diff')
453 c.action = self.request.GET.get('diff')
455 if c.action not in ['download', 'raw']:
454 if c.action not in ['download', 'raw']:
456 compare_url = h.route_path(
455 compare_url = h.route_path(
457 'repo_compare',
456 'repo_compare',
458 repo_name=self.db_repo_name,
457 repo_name=self.db_repo_name,
459 source_ref_type='rev',
458 source_ref_type='rev',
460 source_ref=diff1,
459 source_ref=diff1,
461 target_repo=self.db_repo_name,
460 target_repo=self.db_repo_name,
462 target_ref_type='rev',
461 target_ref_type='rev',
463 target_ref=diff2,
462 target_ref=diff2,
464 _query=dict(f_path=f_path))
463 _query=dict(f_path=f_path))
465 # redirect to new view if we render diff
464 # redirect to new view if we render diff
466 raise HTTPFound(compare_url)
465 raise HTTPFound(compare_url)
467
466
468 try:
467 try:
469 node1 = self._get_file_node(diff1, path1)
468 node1 = self._get_file_node(diff1, path1)
470 node2 = self._get_file_node(diff2, f_path)
469 node2 = self._get_file_node(diff2, f_path)
471 except (RepositoryError, NodeError):
470 except (RepositoryError, NodeError):
472 log.exception("Exception while trying to get node from repository")
471 log.exception("Exception while trying to get node from repository")
473 raise HTTPFound(
472 raise HTTPFound(
474 h.route_path('repo_files', repo_name=self.db_repo_name,
473 h.route_path('repo_files', repo_name=self.db_repo_name,
475 commit_id='tip', f_path=f_path))
474 commit_id='tip', f_path=f_path))
476
475
477 if all(isinstance(node.commit, EmptyCommit)
476 if all(isinstance(node.commit, EmptyCommit)
478 for node in (node1, node2)):
477 for node in (node1, node2)):
479 raise HTTPNotFound()
478 raise HTTPNotFound()
480
479
481 c.commit_1 = node1.commit
480 c.commit_1 = node1.commit
482 c.commit_2 = node2.commit
481 c.commit_2 = node2.commit
483
482
484 if c.action == 'download':
483 if c.action == 'download':
485 _diff = diffs.get_gitdiff(node1, node2,
484 _diff = diffs.get_gitdiff(node1, node2,
486 ignore_whitespace=ignore_whitespace,
485 ignore_whitespace=ignore_whitespace,
487 context=line_context)
486 context=line_context)
488 diff = diffs.DiffProcessor(_diff, format='gitdiff')
487 diff = diffs.DiffProcessor(_diff, format='gitdiff')
489
488
490 response = Response(self.path_filter.get_raw_patch(diff))
489 response = Response(self.path_filter.get_raw_patch(diff))
491 response.content_type = 'text/plain'
490 response.content_type = 'text/plain'
492 response.content_disposition = (
491 response.content_disposition = (
493 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
492 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
494 )
493 )
495 charset = self._get_default_encoding(c)
494 charset = self._get_default_encoding(c)
496 if charset:
495 if charset:
497 response.charset = charset
496 response.charset = charset
498 return response
497 return response
499
498
500 elif c.action == 'raw':
499 elif c.action == 'raw':
501 _diff = diffs.get_gitdiff(node1, node2,
500 _diff = diffs.get_gitdiff(node1, node2,
502 ignore_whitespace=ignore_whitespace,
501 ignore_whitespace=ignore_whitespace,
503 context=line_context)
502 context=line_context)
504 diff = diffs.DiffProcessor(_diff, format='gitdiff')
503 diff = diffs.DiffProcessor(_diff, format='gitdiff')
505
504
506 response = Response(self.path_filter.get_raw_patch(diff))
505 response = Response(self.path_filter.get_raw_patch(diff))
507 response.content_type = 'text/plain'
506 response.content_type = 'text/plain'
508 charset = self._get_default_encoding(c)
507 charset = self._get_default_encoding(c)
509 if charset:
508 if charset:
510 response.charset = charset
509 response.charset = charset
511 return response
510 return response
512
511
513 # in case we ever end up here
512 # in case we ever end up here
514 raise HTTPNotFound()
513 raise HTTPNotFound()
515
514
516 @LoginRequired()
515 @LoginRequired()
517 @HasRepoPermissionAnyDecorator(
516 @HasRepoPermissionAnyDecorator(
518 'repository.read', 'repository.write', 'repository.admin')
517 'repository.read', 'repository.write', 'repository.admin')
519 @view_config(
518 @view_config(
520 route_name='repo_files_diff_2way_redirect', request_method='GET',
519 route_name='repo_files_diff_2way_redirect', request_method='GET',
521 renderer=None)
520 renderer=None)
522 def repo_files_diff_2way_redirect(self):
521 def repo_files_diff_2way_redirect(self):
523 """
522 """
524 Kept only to make OLD links work
523 Kept only to make OLD links work
525 """
524 """
526 f_path = self._get_f_path_unchecked(self.request.matchdict)
525 f_path = self._get_f_path_unchecked(self.request.matchdict)
527 diff1 = self.request.GET.get('diff1', '')
526 diff1 = self.request.GET.get('diff1', '')
528 diff2 = self.request.GET.get('diff2', '')
527 diff2 = self.request.GET.get('diff2', '')
529
528
530 if not any((diff1, diff2)):
529 if not any((diff1, diff2)):
531 h.flash(
530 h.flash(
532 'Need query parameter "diff1" or "diff2" to generate a diff.',
531 'Need query parameter "diff1" or "diff2" to generate a diff.',
533 category='error')
532 category='error')
534 raise HTTPBadRequest()
533 raise HTTPBadRequest()
535
534
536 compare_url = h.route_path(
535 compare_url = h.route_path(
537 'repo_compare',
536 'repo_compare',
538 repo_name=self.db_repo_name,
537 repo_name=self.db_repo_name,
539 source_ref_type='rev',
538 source_ref_type='rev',
540 source_ref=diff1,
539 source_ref=diff1,
541 target_ref_type='rev',
540 target_ref_type='rev',
542 target_ref=diff2,
541 target_ref=diff2,
543 _query=dict(f_path=f_path, diffmode='sideside',
542 _query=dict(f_path=f_path, diffmode='sideside',
544 target_repo=self.db_repo_name,))
543 target_repo=self.db_repo_name,))
545 raise HTTPFound(compare_url)
544 raise HTTPFound(compare_url)
546
545
547 @LoginRequired()
546 @LoginRequired()
548 @HasRepoPermissionAnyDecorator(
547 @HasRepoPermissionAnyDecorator(
549 'repository.read', 'repository.write', 'repository.admin')
548 'repository.read', 'repository.write', 'repository.admin')
550 @view_config(
549 @view_config(
551 route_name='repo_files', request_method='GET',
550 route_name='repo_files', request_method='GET',
552 renderer=None)
551 renderer=None)
553 @view_config(
552 @view_config(
554 route_name='repo_files:default_path', request_method='GET',
553 route_name='repo_files:default_path', request_method='GET',
555 renderer=None)
554 renderer=None)
556 @view_config(
555 @view_config(
557 route_name='repo_files:default_commit', request_method='GET',
556 route_name='repo_files:default_commit', request_method='GET',
558 renderer=None)
557 renderer=None)
559 @view_config(
558 @view_config(
560 route_name='repo_files:rendered', request_method='GET',
559 route_name='repo_files:rendered', request_method='GET',
561 renderer=None)
560 renderer=None)
562 @view_config(
561 @view_config(
563 route_name='repo_files:annotated', request_method='GET',
562 route_name='repo_files:annotated', request_method='GET',
564 renderer=None)
563 renderer=None)
565 def repo_files(self):
564 def repo_files(self):
566 c = self.load_default_context()
565 c = self.load_default_context()
567
566
568 view_name = getattr(self.request.matched_route, 'name', None)
567 view_name = getattr(self.request.matched_route, 'name', None)
569
568
570 c.annotate = view_name == 'repo_files:annotated'
569 c.annotate = view_name == 'repo_files:annotated'
571 # default is false, but .rst/.md files later are auto rendered, we can
570 # default is false, but .rst/.md files later are auto rendered, we can
572 # overwrite auto rendering by setting this GET flag
571 # overwrite auto rendering by setting this GET flag
573 c.renderer = view_name == 'repo_files:rendered' or \
572 c.renderer = view_name == 'repo_files:rendered' or \
574 not self.request.GET.get('no-render', False)
573 not self.request.GET.get('no-render', False)
575
574
576 # redirect to given commit_id from form if given
575 # redirect to given commit_id from form if given
577 get_commit_id = self.request.GET.get('at_rev', None)
576 get_commit_id = self.request.GET.get('at_rev', None)
578 if get_commit_id:
577 if get_commit_id:
579 self._get_commit_or_redirect(get_commit_id)
578 self._get_commit_or_redirect(get_commit_id)
580
579
581 commit_id, f_path = self._get_commit_and_path()
580 commit_id, f_path = self._get_commit_and_path()
582 c.commit = self._get_commit_or_redirect(commit_id)
581 c.commit = self._get_commit_or_redirect(commit_id)
583 c.branch = self.request.GET.get('branch', None)
582 c.branch = self.request.GET.get('branch', None)
584 c.f_path = f_path
583 c.f_path = f_path
585
584
586 # prev link
585 # prev link
587 try:
586 try:
588 prev_commit = c.commit.prev(c.branch)
587 prev_commit = c.commit.prev(c.branch)
589 c.prev_commit = prev_commit
588 c.prev_commit = prev_commit
590 c.url_prev = h.route_path(
589 c.url_prev = h.route_path(
591 'repo_files', repo_name=self.db_repo_name,
590 'repo_files', repo_name=self.db_repo_name,
592 commit_id=prev_commit.raw_id, f_path=f_path)
591 commit_id=prev_commit.raw_id, f_path=f_path)
593 if c.branch:
592 if c.branch:
594 c.url_prev += '?branch=%s' % c.branch
593 c.url_prev += '?branch=%s' % c.branch
595 except (CommitDoesNotExistError, VCSError):
594 except (CommitDoesNotExistError, VCSError):
596 c.url_prev = '#'
595 c.url_prev = '#'
597 c.prev_commit = EmptyCommit()
596 c.prev_commit = EmptyCommit()
598
597
599 # next link
598 # next link
600 try:
599 try:
601 next_commit = c.commit.next(c.branch)
600 next_commit = c.commit.next(c.branch)
602 c.next_commit = next_commit
601 c.next_commit = next_commit
603 c.url_next = h.route_path(
602 c.url_next = h.route_path(
604 'repo_files', repo_name=self.db_repo_name,
603 'repo_files', repo_name=self.db_repo_name,
605 commit_id=next_commit.raw_id, f_path=f_path)
604 commit_id=next_commit.raw_id, f_path=f_path)
606 if c.branch:
605 if c.branch:
607 c.url_next += '?branch=%s' % c.branch
606 c.url_next += '?branch=%s' % c.branch
608 except (CommitDoesNotExistError, VCSError):
607 except (CommitDoesNotExistError, VCSError):
609 c.url_next = '#'
608 c.url_next = '#'
610 c.next_commit = EmptyCommit()
609 c.next_commit = EmptyCommit()
611
610
612 # files or dirs
611 # files or dirs
613 try:
612 try:
614 c.file = c.commit.get_node(f_path)
613 c.file = c.commit.get_node(f_path)
615 c.file_author = True
614 c.file_author = True
616 c.file_tree = ''
615 c.file_tree = ''
617
616
618 # load file content
617 # load file content
619 if c.file.is_file():
618 if c.file.is_file():
620 c.lf_node = c.file.get_largefile_node()
619 c.lf_node = c.file.get_largefile_node()
621
620
622 c.file_source_page = 'true'
621 c.file_source_page = 'true'
623 c.file_last_commit = c.file.last_commit
622 c.file_last_commit = c.file.last_commit
624 if c.file.size < c.visual.cut_off_limit_diff:
623 if c.file.size < c.visual.cut_off_limit_diff:
625 if c.annotate: # annotation has precedence over renderer
624 if c.annotate: # annotation has precedence over renderer
626 c.annotated_lines = filenode_as_annotated_lines_tokens(
625 c.annotated_lines = filenode_as_annotated_lines_tokens(
627 c.file
626 c.file
628 )
627 )
629 else:
628 else:
630 c.renderer = (
629 c.renderer = (
631 c.renderer and h.renderer_from_filename(c.file.path)
630 c.renderer and h.renderer_from_filename(c.file.path)
632 )
631 )
633 if not c.renderer:
632 if not c.renderer:
634 c.lines = filenode_as_lines_tokens(c.file)
633 c.lines = filenode_as_lines_tokens(c.file)
635
634
636 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
635 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
637 commit_id, self.rhodecode_vcs_repo)
636 commit_id, self.rhodecode_vcs_repo)
638 c.on_branch_head = is_head
637 c.on_branch_head = is_head
639
638
640 branch = c.commit.branch if (
639 branch = c.commit.branch if (
641 c.commit.branch and '/' not in c.commit.branch) else None
640 c.commit.branch and '/' not in c.commit.branch) else None
642 c.branch_or_raw_id = branch or c.commit.raw_id
641 c.branch_or_raw_id = branch or c.commit.raw_id
643 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
642 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
644
643
645 author = c.file_last_commit.author
644 author = c.file_last_commit.author
646 c.authors = [[
645 c.authors = [[
647 h.email(author),
646 h.email(author),
648 h.person(author, 'username_or_name_or_email'),
647 h.person(author, 'username_or_name_or_email'),
649 1
648 1
650 ]]
649 ]]
651
650
652 else: # load tree content at path
651 else: # load tree content at path
653 c.file_source_page = 'false'
652 c.file_source_page = 'false'
654 c.authors = []
653 c.authors = []
655 # this loads a simple tree without metadata to speed things up
654 # this loads a simple tree without metadata to speed things up
656 # later via ajax we call repo_nodetree_full and fetch whole
655 # later via ajax we call repo_nodetree_full and fetch whole
657 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
656 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
658
657
659 except RepositoryError as e:
658 except RepositoryError as e:
660 h.flash(safe_str(h.escape(e)), category='error')
659 h.flash(safe_str(h.escape(e)), category='error')
661 raise HTTPNotFound()
660 raise HTTPNotFound()
662
661
663 if self.request.environ.get('HTTP_X_PJAX'):
662 if self.request.environ.get('HTTP_X_PJAX'):
664 html = render('rhodecode:templates/files/files_pjax.mako',
663 html = render('rhodecode:templates/files/files_pjax.mako',
665 self._get_template_context(c), self.request)
664 self._get_template_context(c), self.request)
666 else:
665 else:
667 html = render('rhodecode:templates/files/files.mako',
666 html = render('rhodecode:templates/files/files.mako',
668 self._get_template_context(c), self.request)
667 self._get_template_context(c), self.request)
669 return Response(html)
668 return Response(html)
670
669
671 @HasRepoPermissionAnyDecorator(
670 @HasRepoPermissionAnyDecorator(
672 'repository.read', 'repository.write', 'repository.admin')
671 'repository.read', 'repository.write', 'repository.admin')
673 @view_config(
672 @view_config(
674 route_name='repo_files:annotated_previous', request_method='GET',
673 route_name='repo_files:annotated_previous', request_method='GET',
675 renderer=None)
674 renderer=None)
676 def repo_files_annotated_previous(self):
675 def repo_files_annotated_previous(self):
677 self.load_default_context()
676 self.load_default_context()
678
677
679 commit_id, f_path = self._get_commit_and_path()
678 commit_id, f_path = self._get_commit_and_path()
680 commit = self._get_commit_or_redirect(commit_id)
679 commit = self._get_commit_or_redirect(commit_id)
681 prev_commit_id = commit.raw_id
680 prev_commit_id = commit.raw_id
682 line_anchor = self.request.GET.get('line_anchor')
681 line_anchor = self.request.GET.get('line_anchor')
683 is_file = False
682 is_file = False
684 try:
683 try:
685 _file = commit.get_node(f_path)
684 _file = commit.get_node(f_path)
686 is_file = _file.is_file()
685 is_file = _file.is_file()
687 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
686 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
688 pass
687 pass
689
688
690 if is_file:
689 if is_file:
691 history = commit.get_path_history(f_path)
690 history = commit.get_path_history(f_path)
692 prev_commit_id = history[1].raw_id \
691 prev_commit_id = history[1].raw_id \
693 if len(history) > 1 else prev_commit_id
692 if len(history) > 1 else prev_commit_id
694 prev_url = h.route_path(
693 prev_url = h.route_path(
695 'repo_files:annotated', repo_name=self.db_repo_name,
694 'repo_files:annotated', repo_name=self.db_repo_name,
696 commit_id=prev_commit_id, f_path=f_path,
695 commit_id=prev_commit_id, f_path=f_path,
697 _anchor='L{}'.format(line_anchor))
696 _anchor='L{}'.format(line_anchor))
698
697
699 raise HTTPFound(prev_url)
698 raise HTTPFound(prev_url)
700
699
701 @LoginRequired()
700 @LoginRequired()
702 @HasRepoPermissionAnyDecorator(
701 @HasRepoPermissionAnyDecorator(
703 'repository.read', 'repository.write', 'repository.admin')
702 'repository.read', 'repository.write', 'repository.admin')
704 @view_config(
703 @view_config(
705 route_name='repo_nodetree_full', request_method='GET',
704 route_name='repo_nodetree_full', request_method='GET',
706 renderer=None, xhr=True)
705 renderer=None, xhr=True)
707 @view_config(
706 @view_config(
708 route_name='repo_nodetree_full:default_path', request_method='GET',
707 route_name='repo_nodetree_full:default_path', request_method='GET',
709 renderer=None, xhr=True)
708 renderer=None, xhr=True)
710 def repo_nodetree_full(self):
709 def repo_nodetree_full(self):
711 """
710 """
712 Returns rendered html of file tree that contains commit date,
711 Returns rendered html of file tree that contains commit date,
713 author, commit_id for the specified combination of
712 author, commit_id for the specified combination of
714 repo, commit_id and file path
713 repo, commit_id and file path
715 """
714 """
716 c = self.load_default_context()
715 c = self.load_default_context()
717
716
718 commit_id, f_path = self._get_commit_and_path()
717 commit_id, f_path = self._get_commit_and_path()
719 commit = self._get_commit_or_redirect(commit_id)
718 commit = self._get_commit_or_redirect(commit_id)
720 try:
719 try:
721 dir_node = commit.get_node(f_path)
720 dir_node = commit.get_node(f_path)
722 except RepositoryError as e:
721 except RepositoryError as e:
723 return Response('error: {}'.format(h.escape(safe_str(e))))
722 return Response('error: {}'.format(h.escape(safe_str(e))))
724
723
725 if dir_node.is_file():
724 if dir_node.is_file():
726 return Response('')
725 return Response('')
727
726
728 c.file = dir_node
727 c.file = dir_node
729 c.commit = commit
728 c.commit = commit
730
729
731 html = self._get_tree_at_commit(
730 html = self._get_tree_at_commit(
732 c, commit.raw_id, dir_node.path, full_load=True)
731 c, commit.raw_id, dir_node.path, full_load=True)
733
732
734 return Response(html)
733 return Response(html)
735
734
736 def _get_attachement_headers(self, f_path):
735 def _get_attachement_headers(self, f_path):
737 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
736 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
738 safe_path = f_name.replace('"', '\\"')
737 safe_path = f_name.replace('"', '\\"')
739 encoded_path = urllib.quote(f_name)
738 encoded_path = urllib.quote(f_name)
740
739
741 return "attachment; " \
740 return "attachment; " \
742 "filename=\"{}\"; " \
741 "filename=\"{}\"; " \
743 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
742 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
744
743
745 @LoginRequired()
744 @LoginRequired()
746 @HasRepoPermissionAnyDecorator(
745 @HasRepoPermissionAnyDecorator(
747 'repository.read', 'repository.write', 'repository.admin')
746 'repository.read', 'repository.write', 'repository.admin')
748 @view_config(
747 @view_config(
749 route_name='repo_file_raw', request_method='GET',
748 route_name='repo_file_raw', request_method='GET',
750 renderer=None)
749 renderer=None)
751 def repo_file_raw(self):
750 def repo_file_raw(self):
752 """
751 """
753 Action for show as raw, some mimetypes are "rendered",
752 Action for show as raw, some mimetypes are "rendered",
754 those include images, icons.
753 those include images, icons.
755 """
754 """
756 c = self.load_default_context()
755 c = self.load_default_context()
757
756
758 commit_id, f_path = self._get_commit_and_path()
757 commit_id, f_path = self._get_commit_and_path()
759 commit = self._get_commit_or_redirect(commit_id)
758 commit = self._get_commit_or_redirect(commit_id)
760 file_node = self._get_filenode_or_redirect(commit, f_path)
759 file_node = self._get_filenode_or_redirect(commit, f_path)
761
760
762 raw_mimetype_mapping = {
761 raw_mimetype_mapping = {
763 # map original mimetype to a mimetype used for "show as raw"
762 # map original mimetype to a mimetype used for "show as raw"
764 # you can also provide a content-disposition to override the
763 # you can also provide a content-disposition to override the
765 # default "attachment" disposition.
764 # default "attachment" disposition.
766 # orig_type: (new_type, new_dispo)
765 # orig_type: (new_type, new_dispo)
767
766
768 # show images inline:
767 # show images inline:
769 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
768 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
770 # for example render an SVG with javascript inside or even render
769 # for example render an SVG with javascript inside or even render
771 # HTML.
770 # HTML.
772 'image/x-icon': ('image/x-icon', 'inline'),
771 'image/x-icon': ('image/x-icon', 'inline'),
773 'image/png': ('image/png', 'inline'),
772 'image/png': ('image/png', 'inline'),
774 'image/gif': ('image/gif', 'inline'),
773 'image/gif': ('image/gif', 'inline'),
775 'image/jpeg': ('image/jpeg', 'inline'),
774 'image/jpeg': ('image/jpeg', 'inline'),
776 'application/pdf': ('application/pdf', 'inline'),
775 'application/pdf': ('application/pdf', 'inline'),
777 }
776 }
778
777
779 mimetype = file_node.mimetype
778 mimetype = file_node.mimetype
780 try:
779 try:
781 mimetype, disposition = raw_mimetype_mapping[mimetype]
780 mimetype, disposition = raw_mimetype_mapping[mimetype]
782 except KeyError:
781 except KeyError:
783 # we don't know anything special about this, handle it safely
782 # we don't know anything special about this, handle it safely
784 if file_node.is_binary:
783 if file_node.is_binary:
785 # do same as download raw for binary files
784 # do same as download raw for binary files
786 mimetype, disposition = 'application/octet-stream', 'attachment'
785 mimetype, disposition = 'application/octet-stream', 'attachment'
787 else:
786 else:
788 # do not just use the original mimetype, but force text/plain,
787 # do not just use the original mimetype, but force text/plain,
789 # otherwise it would serve text/html and that might be unsafe.
788 # otherwise it would serve text/html and that might be unsafe.
790 # Note: underlying vcs library fakes text/plain mimetype if the
789 # Note: underlying vcs library fakes text/plain mimetype if the
791 # mimetype can not be determined and it thinks it is not
790 # mimetype can not be determined and it thinks it is not
792 # binary.This might lead to erroneous text display in some
791 # binary.This might lead to erroneous text display in some
793 # cases, but helps in other cases, like with text files
792 # cases, but helps in other cases, like with text files
794 # without extension.
793 # without extension.
795 mimetype, disposition = 'text/plain', 'inline'
794 mimetype, disposition = 'text/plain', 'inline'
796
795
797 if disposition == 'attachment':
796 if disposition == 'attachment':
798 disposition = self._get_attachement_headers(f_path)
797 disposition = self._get_attachement_headers(f_path)
799
798
800 def stream_node():
799 def stream_node():
801 yield file_node.raw_bytes
800 yield file_node.raw_bytes
802
801
803 response = Response(app_iter=stream_node())
802 response = Response(app_iter=stream_node())
804 response.content_disposition = disposition
803 response.content_disposition = disposition
805 response.content_type = mimetype
804 response.content_type = mimetype
806
805
807 charset = self._get_default_encoding(c)
806 charset = self._get_default_encoding(c)
808 if charset:
807 if charset:
809 response.charset = charset
808 response.charset = charset
810
809
811 return response
810 return response
812
811
813 @LoginRequired()
812 @LoginRequired()
814 @HasRepoPermissionAnyDecorator(
813 @HasRepoPermissionAnyDecorator(
815 'repository.read', 'repository.write', 'repository.admin')
814 'repository.read', 'repository.write', 'repository.admin')
816 @view_config(
815 @view_config(
817 route_name='repo_file_download', request_method='GET',
816 route_name='repo_file_download', request_method='GET',
818 renderer=None)
817 renderer=None)
819 @view_config(
818 @view_config(
820 route_name='repo_file_download:legacy', request_method='GET',
819 route_name='repo_file_download:legacy', request_method='GET',
821 renderer=None)
820 renderer=None)
822 def repo_file_download(self):
821 def repo_file_download(self):
823 c = self.load_default_context()
822 c = self.load_default_context()
824
823
825 commit_id, f_path = self._get_commit_and_path()
824 commit_id, f_path = self._get_commit_and_path()
826 commit = self._get_commit_or_redirect(commit_id)
825 commit = self._get_commit_or_redirect(commit_id)
827 file_node = self._get_filenode_or_redirect(commit, f_path)
826 file_node = self._get_filenode_or_redirect(commit, f_path)
828
827
829 if self.request.GET.get('lf'):
828 if self.request.GET.get('lf'):
830 # only if lf get flag is passed, we download this file
829 # only if lf get flag is passed, we download this file
831 # as LFS/Largefile
830 # as LFS/Largefile
832 lf_node = file_node.get_largefile_node()
831 lf_node = file_node.get_largefile_node()
833 if lf_node:
832 if lf_node:
834 # overwrite our pointer with the REAL large-file
833 # overwrite our pointer with the REAL large-file
835 file_node = lf_node
834 file_node = lf_node
836
835
837 disposition = self._get_attachement_headers(f_path)
836 disposition = self._get_attachement_headers(f_path)
838
837
839 def stream_node():
838 def stream_node():
840 yield file_node.raw_bytes
839 yield file_node.raw_bytes
841
840
842 response = Response(app_iter=stream_node())
841 response = Response(app_iter=stream_node())
843 response.content_disposition = disposition
842 response.content_disposition = disposition
844 response.content_type = file_node.mimetype
843 response.content_type = file_node.mimetype
845
844
846 charset = self._get_default_encoding(c)
845 charset = self._get_default_encoding(c)
847 if charset:
846 if charset:
848 response.charset = charset
847 response.charset = charset
849
848
850 return response
849 return response
851
850
852 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
851 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
853
852
854 cache_seconds = safe_int(
853 cache_seconds = safe_int(
855 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
854 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
856 cache_on = cache_seconds > 0
855 cache_on = cache_seconds > 0
857 log.debug(
856 log.debug(
858 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
857 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
859 'with caching: %s[TTL: %ss]' % (
858 'with caching: %s[TTL: %ss]' % (
860 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
859 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
861
860
862 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
861 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
863 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
862 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
864
863
865 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
864 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
866 condition=cache_on)
865 condition=cache_on)
867 def compute_file_search(repo_id, commit_id, f_path):
866 def compute_file_search(repo_id, commit_id, f_path):
868 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
867 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
869 repo_id, commit_id, f_path)
868 repo_id, commit_id, f_path)
870 try:
869 try:
871 _d, _f = ScmModel().get_nodes(
870 _d, _f = ScmModel().get_nodes(
872 repo_name, commit_id, f_path, flat=False)
871 repo_name, commit_id, f_path, flat=False)
873 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
872 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
874 log.exception(safe_str(e))
873 log.exception(safe_str(e))
875 h.flash(safe_str(h.escape(e)), category='error')
874 h.flash(safe_str(h.escape(e)), category='error')
876 raise HTTPFound(h.route_path(
875 raise HTTPFound(h.route_path(
877 'repo_files', repo_name=self.db_repo_name,
876 'repo_files', repo_name=self.db_repo_name,
878 commit_id='tip', f_path='/'))
877 commit_id='tip', f_path='/'))
879 return _d + _f
878 return _d + _f
880
879
881 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
880 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
882
881
883 @LoginRequired()
882 @LoginRequired()
884 @HasRepoPermissionAnyDecorator(
883 @HasRepoPermissionAnyDecorator(
885 'repository.read', 'repository.write', 'repository.admin')
884 'repository.read', 'repository.write', 'repository.admin')
886 @view_config(
885 @view_config(
887 route_name='repo_files_nodelist', request_method='GET',
886 route_name='repo_files_nodelist', request_method='GET',
888 renderer='json_ext', xhr=True)
887 renderer='json_ext', xhr=True)
889 def repo_nodelist(self):
888 def repo_nodelist(self):
890 self.load_default_context()
889 self.load_default_context()
891
890
892 commit_id, f_path = self._get_commit_and_path()
891 commit_id, f_path = self._get_commit_and_path()
893 commit = self._get_commit_or_redirect(commit_id)
892 commit = self._get_commit_or_redirect(commit_id)
894
893
895 metadata = self._get_nodelist_at_commit(
894 metadata = self._get_nodelist_at_commit(
896 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
895 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
897 return {'nodes': metadata}
896 return {'nodes': metadata}
898
897
899 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
898 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
900 items = []
899 items = []
901 for name, commit_id in branches_or_tags.items():
900 for name, commit_id in branches_or_tags.items():
902 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
901 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
903 items.append((sym_ref, name, ref_type))
902 items.append((sym_ref, name, ref_type))
904 return items
903 return items
905
904
906 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
905 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
907 return commit_id
906 return commit_id
908
907
909 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
908 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
910 new_f_path = vcspath.join(name, f_path)
909 new_f_path = vcspath.join(name, f_path)
911 return u'%s@%s' % (new_f_path, commit_id)
910 return u'%s@%s' % (new_f_path, commit_id)
912
911
913 def _get_node_history(self, commit_obj, f_path, commits=None):
912 def _get_node_history(self, commit_obj, f_path, commits=None):
914 """
913 """
915 get commit history for given node
914 get commit history for given node
916
915
917 :param commit_obj: commit to calculate history
916 :param commit_obj: commit to calculate history
918 :param f_path: path for node to calculate history for
917 :param f_path: path for node to calculate history for
919 :param commits: if passed don't calculate history and take
918 :param commits: if passed don't calculate history and take
920 commits defined in this list
919 commits defined in this list
921 """
920 """
922 _ = self.request.translate
921 _ = self.request.translate
923
922
924 # calculate history based on tip
923 # calculate history based on tip
925 tip = self.rhodecode_vcs_repo.get_commit()
924 tip = self.rhodecode_vcs_repo.get_commit()
926 if commits is None:
925 if commits is None:
927 pre_load = ["author", "branch"]
926 pre_load = ["author", "branch"]
928 try:
927 try:
929 commits = tip.get_path_history(f_path, pre_load=pre_load)
928 commits = tip.get_path_history(f_path, pre_load=pre_load)
930 except (NodeDoesNotExistError, CommitError):
929 except (NodeDoesNotExistError, CommitError):
931 # this node is not present at tip!
930 # this node is not present at tip!
932 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
931 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
933
932
934 history = []
933 history = []
935 commits_group = ([], _("Changesets"))
934 commits_group = ([], _("Changesets"))
936 for commit in commits:
935 for commit in commits:
937 branch = ' (%s)' % commit.branch if commit.branch else ''
936 branch = ' (%s)' % commit.branch if commit.branch else ''
938 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
937 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
939 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
938 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
940 history.append(commits_group)
939 history.append(commits_group)
941
940
942 symbolic_reference = self._symbolic_reference
941 symbolic_reference = self._symbolic_reference
943
942
944 if self.rhodecode_vcs_repo.alias == 'svn':
943 if self.rhodecode_vcs_repo.alias == 'svn':
945 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
944 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
946 f_path, self.rhodecode_vcs_repo)
945 f_path, self.rhodecode_vcs_repo)
947 if adjusted_f_path != f_path:
946 if adjusted_f_path != f_path:
948 log.debug(
947 log.debug(
949 'Recognized svn tag or branch in file "%s", using svn '
948 'Recognized svn tag or branch in file "%s", using svn '
950 'specific symbolic references', f_path)
949 'specific symbolic references', f_path)
951 f_path = adjusted_f_path
950 f_path = adjusted_f_path
952 symbolic_reference = self._symbolic_reference_svn
951 symbolic_reference = self._symbolic_reference_svn
953
952
954 branches = self._create_references(
953 branches = self._create_references(
955 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
954 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
956 branches_group = (branches, _("Branches"))
955 branches_group = (branches, _("Branches"))
957
956
958 tags = self._create_references(
957 tags = self._create_references(
959 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
958 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
960 tags_group = (tags, _("Tags"))
959 tags_group = (tags, _("Tags"))
961
960
962 history.append(branches_group)
961 history.append(branches_group)
963 history.append(tags_group)
962 history.append(tags_group)
964
963
965 return history, commits
964 return history, commits
966
965
967 @LoginRequired()
966 @LoginRequired()
968 @HasRepoPermissionAnyDecorator(
967 @HasRepoPermissionAnyDecorator(
969 'repository.read', 'repository.write', 'repository.admin')
968 'repository.read', 'repository.write', 'repository.admin')
970 @view_config(
969 @view_config(
971 route_name='repo_file_history', request_method='GET',
970 route_name='repo_file_history', request_method='GET',
972 renderer='json_ext')
971 renderer='json_ext')
973 def repo_file_history(self):
972 def repo_file_history(self):
974 self.load_default_context()
973 self.load_default_context()
975
974
976 commit_id, f_path = self._get_commit_and_path()
975 commit_id, f_path = self._get_commit_and_path()
977 commit = self._get_commit_or_redirect(commit_id)
976 commit = self._get_commit_or_redirect(commit_id)
978 file_node = self._get_filenode_or_redirect(commit, f_path)
977 file_node = self._get_filenode_or_redirect(commit, f_path)
979
978
980 if file_node.is_file():
979 if file_node.is_file():
981 file_history, _hist = self._get_node_history(commit, f_path)
980 file_history, _hist = self._get_node_history(commit, f_path)
982
981
983 res = []
982 res = []
984 for obj in file_history:
983 for obj in file_history:
985 res.append({
984 res.append({
986 'text': obj[1],
985 'text': obj[1],
987 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
986 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
988 })
987 })
989
988
990 data = {
989 data = {
991 'more': False,
990 'more': False,
992 'results': res
991 'results': res
993 }
992 }
994 return data
993 return data
995
994
996 log.warning('Cannot fetch history for directory')
995 log.warning('Cannot fetch history for directory')
997 raise HTTPBadRequest()
996 raise HTTPBadRequest()
998
997
999 @LoginRequired()
998 @LoginRequired()
1000 @HasRepoPermissionAnyDecorator(
999 @HasRepoPermissionAnyDecorator(
1001 'repository.read', 'repository.write', 'repository.admin')
1000 'repository.read', 'repository.write', 'repository.admin')
1002 @view_config(
1001 @view_config(
1003 route_name='repo_file_authors', request_method='GET',
1002 route_name='repo_file_authors', request_method='GET',
1004 renderer='rhodecode:templates/files/file_authors_box.mako')
1003 renderer='rhodecode:templates/files/file_authors_box.mako')
1005 def repo_file_authors(self):
1004 def repo_file_authors(self):
1006 c = self.load_default_context()
1005 c = self.load_default_context()
1007
1006
1008 commit_id, f_path = self._get_commit_and_path()
1007 commit_id, f_path = self._get_commit_and_path()
1009 commit = self._get_commit_or_redirect(commit_id)
1008 commit = self._get_commit_or_redirect(commit_id)
1010 file_node = self._get_filenode_or_redirect(commit, f_path)
1009 file_node = self._get_filenode_or_redirect(commit, f_path)
1011
1010
1012 if not file_node.is_file():
1011 if not file_node.is_file():
1013 raise HTTPBadRequest()
1012 raise HTTPBadRequest()
1014
1013
1015 c.file_last_commit = file_node.last_commit
1014 c.file_last_commit = file_node.last_commit
1016 if self.request.GET.get('annotate') == '1':
1015 if self.request.GET.get('annotate') == '1':
1017 # use _hist from annotation if annotation mode is on
1016 # use _hist from annotation if annotation mode is on
1018 commit_ids = set(x[1] for x in file_node.annotate)
1017 commit_ids = set(x[1] for x in file_node.annotate)
1019 _hist = (
1018 _hist = (
1020 self.rhodecode_vcs_repo.get_commit(commit_id)
1019 self.rhodecode_vcs_repo.get_commit(commit_id)
1021 for commit_id in commit_ids)
1020 for commit_id in commit_ids)
1022 else:
1021 else:
1023 _f_history, _hist = self._get_node_history(commit, f_path)
1022 _f_history, _hist = self._get_node_history(commit, f_path)
1024 c.file_author = False
1023 c.file_author = False
1025
1024
1026 unique = collections.OrderedDict()
1025 unique = collections.OrderedDict()
1027 for commit in _hist:
1026 for commit in _hist:
1028 author = commit.author
1027 author = commit.author
1029 if author not in unique:
1028 if author not in unique:
1030 unique[commit.author] = [
1029 unique[commit.author] = [
1031 h.email(author),
1030 h.email(author),
1032 h.person(author, 'username_or_name_or_email'),
1031 h.person(author, 'username_or_name_or_email'),
1033 1 # counter
1032 1 # counter
1034 ]
1033 ]
1035
1034
1036 else:
1035 else:
1037 # increase counter
1036 # increase counter
1038 unique[commit.author][2] += 1
1037 unique[commit.author][2] += 1
1039
1038
1040 c.authors = [val for val in unique.values()]
1039 c.authors = [val for val in unique.values()]
1041
1040
1042 return self._get_template_context(c)
1041 return self._get_template_context(c)
1043
1042
1044 @LoginRequired()
1043 @LoginRequired()
1045 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1044 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1046 @view_config(
1045 @view_config(
1047 route_name='repo_files_remove_file', request_method='GET',
1046 route_name='repo_files_remove_file', request_method='GET',
1048 renderer='rhodecode:templates/files/files_delete.mako')
1047 renderer='rhodecode:templates/files/files_delete.mako')
1049 def repo_files_remove_file(self):
1048 def repo_files_remove_file(self):
1050 _ = self.request.translate
1049 _ = self.request.translate
1051 c = self.load_default_context()
1050 c = self.load_default_context()
1052 commit_id, f_path = self._get_commit_and_path()
1051 commit_id, f_path = self._get_commit_and_path()
1053
1052
1054 self._ensure_not_locked()
1053 self._ensure_not_locked()
1055 _branch_name, _sha_commit_id, is_head = \
1054 _branch_name, _sha_commit_id, is_head = \
1056 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1055 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1057
1056
1058 if not is_head:
1057 if not is_head:
1059 h.flash(_('You can only delete files with commit '
1058 h.flash(_('You can only delete files with commit '
1060 'being a valid branch head.'), category='warning')
1059 'being a valid branch head.'), category='warning')
1061 raise HTTPFound(
1060 raise HTTPFound(
1062 h.route_path('repo_files',
1061 h.route_path('repo_files',
1063 repo_name=self.db_repo_name, commit_id='tip',
1062 repo_name=self.db_repo_name, commit_id='tip',
1064 f_path=f_path))
1063 f_path=f_path))
1065
1064
1066 self.check_branch_permission(_branch_name)
1065 self.check_branch_permission(_branch_name)
1067 c.commit = self._get_commit_or_redirect(commit_id)
1066 c.commit = self._get_commit_or_redirect(commit_id)
1068 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1067 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1069
1068
1070 c.default_message = _(
1069 c.default_message = _(
1071 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1070 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1072 c.f_path = f_path
1071 c.f_path = f_path
1073
1072
1074 return self._get_template_context(c)
1073 return self._get_template_context(c)
1075
1074
1076 @LoginRequired()
1075 @LoginRequired()
1077 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1076 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1078 @CSRFRequired()
1077 @CSRFRequired()
1079 @view_config(
1078 @view_config(
1080 route_name='repo_files_delete_file', request_method='POST',
1079 route_name='repo_files_delete_file', request_method='POST',
1081 renderer=None)
1080 renderer=None)
1082 def repo_files_delete_file(self):
1081 def repo_files_delete_file(self):
1083 _ = self.request.translate
1082 _ = self.request.translate
1084
1083
1085 c = self.load_default_context()
1084 c = self.load_default_context()
1086 commit_id, f_path = self._get_commit_and_path()
1085 commit_id, f_path = self._get_commit_and_path()
1087
1086
1088 self._ensure_not_locked()
1087 self._ensure_not_locked()
1089 _branch_name, _sha_commit_id, is_head = \
1088 _branch_name, _sha_commit_id, is_head = \
1090 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1089 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1091
1090
1092 if not is_head:
1091 if not is_head:
1093 h.flash(_('You can only delete files with commit '
1092 h.flash(_('You can only delete files with commit '
1094 'being a valid branch head.'), category='warning')
1093 'being a valid branch head.'), category='warning')
1095 raise HTTPFound(
1094 raise HTTPFound(
1096 h.route_path('repo_files',
1095 h.route_path('repo_files',
1097 repo_name=self.db_repo_name, commit_id='tip',
1096 repo_name=self.db_repo_name, commit_id='tip',
1098 f_path=f_path))
1097 f_path=f_path))
1099 self.check_branch_permission(_branch_name)
1098 self.check_branch_permission(_branch_name)
1100
1099
1101 c.commit = self._get_commit_or_redirect(commit_id)
1100 c.commit = self._get_commit_or_redirect(commit_id)
1102 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1101 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1103
1102
1104 c.default_message = _(
1103 c.default_message = _(
1105 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1104 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1106 c.f_path = f_path
1105 c.f_path = f_path
1107 node_path = f_path
1106 node_path = f_path
1108 author = self._rhodecode_db_user.full_contact
1107 author = self._rhodecode_db_user.full_contact
1109 message = self.request.POST.get('message') or c.default_message
1108 message = self.request.POST.get('message') or c.default_message
1110 try:
1109 try:
1111 nodes = {
1110 nodes = {
1112 node_path: {
1111 node_path: {
1113 'content': ''
1112 'content': ''
1114 }
1113 }
1115 }
1114 }
1116 ScmModel().delete_nodes(
1115 ScmModel().delete_nodes(
1117 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1116 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1118 message=message,
1117 message=message,
1119 nodes=nodes,
1118 nodes=nodes,
1120 parent_commit=c.commit,
1119 parent_commit=c.commit,
1121 author=author,
1120 author=author,
1122 )
1121 )
1123
1122
1124 h.flash(
1123 h.flash(
1125 _('Successfully deleted file `{}`').format(
1124 _('Successfully deleted file `{}`').format(
1126 h.escape(f_path)), category='success')
1125 h.escape(f_path)), category='success')
1127 except Exception:
1126 except Exception:
1128 log.exception('Error during commit operation')
1127 log.exception('Error during commit operation')
1129 h.flash(_('Error occurred during commit'), category='error')
1128 h.flash(_('Error occurred during commit'), category='error')
1130 raise HTTPFound(
1129 raise HTTPFound(
1131 h.route_path('repo_commit', repo_name=self.db_repo_name,
1130 h.route_path('repo_commit', repo_name=self.db_repo_name,
1132 commit_id='tip'))
1131 commit_id='tip'))
1133
1132
1134 @LoginRequired()
1133 @LoginRequired()
1135 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1134 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1136 @view_config(
1135 @view_config(
1137 route_name='repo_files_edit_file', request_method='GET',
1136 route_name='repo_files_edit_file', request_method='GET',
1138 renderer='rhodecode:templates/files/files_edit.mako')
1137 renderer='rhodecode:templates/files/files_edit.mako')
1139 def repo_files_edit_file(self):
1138 def repo_files_edit_file(self):
1140 _ = self.request.translate
1139 _ = self.request.translate
1141 c = self.load_default_context()
1140 c = self.load_default_context()
1142 commit_id, f_path = self._get_commit_and_path()
1141 commit_id, f_path = self._get_commit_and_path()
1143
1142
1144 self._ensure_not_locked()
1143 self._ensure_not_locked()
1145 _branch_name, _sha_commit_id, is_head = \
1144 _branch_name, _sha_commit_id, is_head = \
1146 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1145 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1147
1146
1148 if not is_head:
1147 if not is_head:
1149 h.flash(_('You can only edit files with commit '
1148 h.flash(_('You can only edit files with commit '
1150 'being a valid branch head.'), category='warning')
1149 'being a valid branch head.'), category='warning')
1151 raise HTTPFound(
1150 raise HTTPFound(
1152 h.route_path('repo_files',
1151 h.route_path('repo_files',
1153 repo_name=self.db_repo_name, commit_id='tip',
1152 repo_name=self.db_repo_name, commit_id='tip',
1154 f_path=f_path))
1153 f_path=f_path))
1155 self.check_branch_permission(_branch_name)
1154 self.check_branch_permission(_branch_name)
1156
1155
1157 c.commit = self._get_commit_or_redirect(commit_id)
1156 c.commit = self._get_commit_or_redirect(commit_id)
1158 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1157 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1159
1158
1160 if c.file.is_binary:
1159 if c.file.is_binary:
1161 files_url = h.route_path(
1160 files_url = h.route_path(
1162 'repo_files',
1161 'repo_files',
1163 repo_name=self.db_repo_name,
1162 repo_name=self.db_repo_name,
1164 commit_id=c.commit.raw_id, f_path=f_path)
1163 commit_id=c.commit.raw_id, f_path=f_path)
1165 raise HTTPFound(files_url)
1164 raise HTTPFound(files_url)
1166
1165
1167 c.default_message = _(
1166 c.default_message = _(
1168 'Edited file {} via RhodeCode Enterprise').format(f_path)
1167 'Edited file {} via RhodeCode Enterprise').format(f_path)
1169 c.f_path = f_path
1168 c.f_path = f_path
1170
1169
1171 return self._get_template_context(c)
1170 return self._get_template_context(c)
1172
1171
1173 @LoginRequired()
1172 @LoginRequired()
1174 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1173 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1175 @CSRFRequired()
1174 @CSRFRequired()
1176 @view_config(
1175 @view_config(
1177 route_name='repo_files_update_file', request_method='POST',
1176 route_name='repo_files_update_file', request_method='POST',
1178 renderer=None)
1177 renderer=None)
1179 def repo_files_update_file(self):
1178 def repo_files_update_file(self):
1180 _ = self.request.translate
1179 _ = self.request.translate
1181 c = self.load_default_context()
1180 c = self.load_default_context()
1182 commit_id, f_path = self._get_commit_and_path()
1181 commit_id, f_path = self._get_commit_and_path()
1183
1182
1184 self._ensure_not_locked()
1183 self._ensure_not_locked()
1185 _branch_name, _sha_commit_id, is_head = \
1184 _branch_name, _sha_commit_id, is_head = \
1186 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1185 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1187
1186
1188 if not is_head:
1187 if not is_head:
1189 h.flash(_('You can only edit files with commit '
1188 h.flash(_('You can only edit files with commit '
1190 'being a valid branch head.'), category='warning')
1189 'being a valid branch head.'), category='warning')
1191 raise HTTPFound(
1190 raise HTTPFound(
1192 h.route_path('repo_files',
1191 h.route_path('repo_files',
1193 repo_name=self.db_repo_name, commit_id='tip',
1192 repo_name=self.db_repo_name, commit_id='tip',
1194 f_path=f_path))
1193 f_path=f_path))
1195
1194
1196 self.check_branch_permission(_branch_name)
1195 self.check_branch_permission(_branch_name)
1197
1196
1198 c.commit = self._get_commit_or_redirect(commit_id)
1197 c.commit = self._get_commit_or_redirect(commit_id)
1199 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1198 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1200
1199
1201 if c.file.is_binary:
1200 if c.file.is_binary:
1202 raise HTTPFound(
1201 raise HTTPFound(
1203 h.route_path('repo_files',
1202 h.route_path('repo_files',
1204 repo_name=self.db_repo_name,
1203 repo_name=self.db_repo_name,
1205 commit_id=c.commit.raw_id,
1204 commit_id=c.commit.raw_id,
1206 f_path=f_path))
1205 f_path=f_path))
1207
1206
1208 c.default_message = _(
1207 c.default_message = _(
1209 'Edited file {} via RhodeCode Enterprise').format(f_path)
1208 'Edited file {} via RhodeCode Enterprise').format(f_path)
1210 c.f_path = f_path
1209 c.f_path = f_path
1211 old_content = c.file.content
1210 old_content = c.file.content
1212 sl = old_content.splitlines(1)
1211 sl = old_content.splitlines(1)
1213 first_line = sl[0] if sl else ''
1212 first_line = sl[0] if sl else ''
1214
1213
1215 r_post = self.request.POST
1214 r_post = self.request.POST
1216 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1215 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1217 line_ending_mode = detect_mode(first_line, 0)
1216 line_ending_mode = detect_mode(first_line, 0)
1218 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1217 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1219
1218
1220 message = r_post.get('message') or c.default_message
1219 message = r_post.get('message') or c.default_message
1221 org_f_path = c.file.unicode_path
1220 org_f_path = c.file.unicode_path
1222 filename = r_post['filename']
1221 filename = r_post['filename']
1223 org_filename = c.file.name
1222 org_filename = c.file.name
1224
1223
1225 if content == old_content and filename == org_filename:
1224 if content == old_content and filename == org_filename:
1226 h.flash(_('No changes'), category='warning')
1225 h.flash(_('No changes'), category='warning')
1227 raise HTTPFound(
1226 raise HTTPFound(
1228 h.route_path('repo_commit', repo_name=self.db_repo_name,
1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1229 commit_id='tip'))
1228 commit_id='tip'))
1230 try:
1229 try:
1231 mapping = {
1230 mapping = {
1232 org_f_path: {
1231 org_f_path: {
1233 'org_filename': org_f_path,
1232 'org_filename': org_f_path,
1234 'filename': os.path.join(c.file.dir_path, filename),
1233 'filename': os.path.join(c.file.dir_path, filename),
1235 'content': content,
1234 'content': content,
1236 'lexer': '',
1235 'lexer': '',
1237 'op': 'mod',
1236 'op': 'mod',
1238 'mode': c.file.mode
1237 'mode': c.file.mode
1239 }
1238 }
1240 }
1239 }
1241
1240
1242 ScmModel().update_nodes(
1241 ScmModel().update_nodes(
1243 user=self._rhodecode_db_user.user_id,
1242 user=self._rhodecode_db_user.user_id,
1244 repo=self.db_repo,
1243 repo=self.db_repo,
1245 message=message,
1244 message=message,
1246 nodes=mapping,
1245 nodes=mapping,
1247 parent_commit=c.commit,
1246 parent_commit=c.commit,
1248 )
1247 )
1249
1248
1250 h.flash(
1249 h.flash(
1251 _('Successfully committed changes to file `{}`').format(
1250 _('Successfully committed changes to file `{}`').format(
1252 h.escape(f_path)), category='success')
1251 h.escape(f_path)), category='success')
1253 except Exception:
1252 except Exception:
1254 log.exception('Error occurred during commit')
1253 log.exception('Error occurred during commit')
1255 h.flash(_('Error occurred during commit'), category='error')
1254 h.flash(_('Error occurred during commit'), category='error')
1256 raise HTTPFound(
1255 raise HTTPFound(
1257 h.route_path('repo_commit', repo_name=self.db_repo_name,
1256 h.route_path('repo_commit', repo_name=self.db_repo_name,
1258 commit_id='tip'))
1257 commit_id='tip'))
1259
1258
1260 @LoginRequired()
1259 @LoginRequired()
1261 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1260 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1262 @view_config(
1261 @view_config(
1263 route_name='repo_files_add_file', request_method='GET',
1262 route_name='repo_files_add_file', request_method='GET',
1264 renderer='rhodecode:templates/files/files_add.mako')
1263 renderer='rhodecode:templates/files/files_add.mako')
1265 @view_config(
1264 @view_config(
1266 route_name='repo_files_upload_file', request_method='GET',
1265 route_name='repo_files_upload_file', request_method='GET',
1267 renderer='rhodecode:templates/files/files_upload.mako')
1266 renderer='rhodecode:templates/files/files_upload.mako')
1268 def repo_files_add_file(self):
1267 def repo_files_add_file(self):
1269 _ = self.request.translate
1268 _ = self.request.translate
1270 c = self.load_default_context()
1269 c = self.load_default_context()
1271 commit_id, f_path = self._get_commit_and_path()
1270 commit_id, f_path = self._get_commit_and_path()
1272
1271
1273 self._ensure_not_locked()
1272 self._ensure_not_locked()
1274
1273
1275 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1274 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1276 if c.commit is None:
1275 if c.commit is None:
1277 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1276 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1278 c.default_message = (_('Added file via RhodeCode Enterprise'))
1277 c.default_message = (_('Added file via RhodeCode Enterprise'))
1279 c.f_path = f_path.lstrip('/') # ensure not relative path
1278 c.f_path = f_path.lstrip('/') # ensure not relative path
1280
1279
1281 if self.rhodecode_vcs_repo.is_empty:
1280 if self.rhodecode_vcs_repo.is_empty:
1282 # for empty repository we cannot check for current branch, we rely on
1281 # for empty repository we cannot check for current branch, we rely on
1283 # c.commit.branch instead
1282 # c.commit.branch instead
1284 _branch_name = c.commit.branch
1283 _branch_name = c.commit.branch
1285 is_head = True
1284 is_head = True
1286 else:
1285 else:
1287 _branch_name, _sha_commit_id, is_head = \
1286 _branch_name, _sha_commit_id, is_head = \
1288 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1287 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1289
1288
1290 if not is_head:
1289 if not is_head:
1291 h.flash(_('You can only add files with commit '
1290 h.flash(_('You can only add files with commit '
1292 'being a valid branch head.'), category='warning')
1291 'being a valid branch head.'), category='warning')
1293 raise HTTPFound(
1292 raise HTTPFound(
1294 h.route_path('repo_files',
1293 h.route_path('repo_files',
1295 repo_name=self.db_repo_name, commit_id='tip',
1294 repo_name=self.db_repo_name, commit_id='tip',
1296 f_path=f_path))
1295 f_path=f_path))
1297
1296
1298 self.check_branch_permission(_branch_name)
1297 self.check_branch_permission(_branch_name)
1299
1298
1300 return self._get_template_context(c)
1299 return self._get_template_context(c)
1301
1300
1302 @LoginRequired()
1301 @LoginRequired()
1303 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1302 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1304 @CSRFRequired()
1303 @CSRFRequired()
1305 @view_config(
1304 @view_config(
1306 route_name='repo_files_create_file', request_method='POST',
1305 route_name='repo_files_create_file', request_method='POST',
1307 renderer=None)
1306 renderer=None)
1308 def repo_files_create_file(self):
1307 def repo_files_create_file(self):
1309 _ = self.request.translate
1308 _ = self.request.translate
1310 c = self.load_default_context()
1309 c = self.load_default_context()
1311 commit_id, f_path = self._get_commit_and_path()
1310 commit_id, f_path = self._get_commit_and_path()
1312
1311
1313 self._ensure_not_locked()
1312 self._ensure_not_locked()
1314
1313
1315 r_post = self.request.POST
1314 r_post = self.request.POST
1316
1315
1317 c.commit = self._get_commit_or_redirect(
1316 c.commit = self._get_commit_or_redirect(
1318 commit_id, redirect_after=False)
1317 commit_id, redirect_after=False)
1319 if c.commit is None:
1318 if c.commit is None:
1320 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1319 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1321
1320
1322 if self.rhodecode_vcs_repo.is_empty:
1321 if self.rhodecode_vcs_repo.is_empty:
1323 # for empty repository we cannot check for current branch, we rely on
1322 # for empty repository we cannot check for current branch, we rely on
1324 # c.commit.branch instead
1323 # c.commit.branch instead
1325 _branch_name = c.commit.branch
1324 _branch_name = c.commit.branch
1326 is_head = True
1325 is_head = True
1327 else:
1326 else:
1328 _branch_name, _sha_commit_id, is_head = \
1327 _branch_name, _sha_commit_id, is_head = \
1329 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1328 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1330
1329
1331 if not is_head:
1330 if not is_head:
1332 h.flash(_('You can only add files with commit '
1331 h.flash(_('You can only add files with commit '
1333 'being a valid branch head.'), category='warning')
1332 'being a valid branch head.'), category='warning')
1334 raise HTTPFound(
1333 raise HTTPFound(
1335 h.route_path('repo_files',
1334 h.route_path('repo_files',
1336 repo_name=self.db_repo_name, commit_id='tip',
1335 repo_name=self.db_repo_name, commit_id='tip',
1337 f_path=f_path))
1336 f_path=f_path))
1338
1337
1339 self.check_branch_permission(_branch_name)
1338 self.check_branch_permission(_branch_name)
1340
1339
1341 c.default_message = (_('Added file via RhodeCode Enterprise'))
1340 c.default_message = (_('Added file via RhodeCode Enterprise'))
1342 c.f_path = f_path
1341 c.f_path = f_path
1343 unix_mode = 0
1342 unix_mode = 0
1344 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1343 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1345
1344
1346 message = r_post.get('message') or c.default_message
1345 message = r_post.get('message') or c.default_message
1347 filename = r_post.get('filename')
1346 filename = r_post.get('filename')
1348 location = r_post.get('location', '') # dir location
1347 location = r_post.get('location', '') # dir location
1349 file_obj = r_post.get('upload_file', None)
1348 file_obj = r_post.get('upload_file', None)
1350
1349
1351 if file_obj is not None and hasattr(file_obj, 'filename'):
1350 if file_obj is not None and hasattr(file_obj, 'filename'):
1352 filename = r_post.get('filename_upload')
1351 filename = r_post.get('filename_upload')
1353 content = file_obj.file
1352 content = file_obj.file
1354
1353
1355 if hasattr(content, 'file'):
1354 if hasattr(content, 'file'):
1356 # non posix systems store real file under file attr
1355 # non posix systems store real file under file attr
1357 content = content.file
1356 content = content.file
1358
1357
1359 if self.rhodecode_vcs_repo.is_empty:
1358 if self.rhodecode_vcs_repo.is_empty:
1360 default_redirect_url = h.route_path(
1359 default_redirect_url = h.route_path(
1361 'repo_summary', repo_name=self.db_repo_name)
1360 'repo_summary', repo_name=self.db_repo_name)
1362 else:
1361 else:
1363 default_redirect_url = h.route_path(
1362 default_redirect_url = h.route_path(
1364 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1363 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1365
1364
1366 # If there's no commit, redirect to repo summary
1365 # If there's no commit, redirect to repo summary
1367 if type(c.commit) is EmptyCommit:
1366 if type(c.commit) is EmptyCommit:
1368 redirect_url = h.route_path(
1367 redirect_url = h.route_path(
1369 'repo_summary', repo_name=self.db_repo_name)
1368 'repo_summary', repo_name=self.db_repo_name)
1370 else:
1369 else:
1371 redirect_url = default_redirect_url
1370 redirect_url = default_redirect_url
1372
1371
1373 if not filename:
1372 if not filename:
1374 h.flash(_('No filename'), category='warning')
1373 h.flash(_('No filename'), category='warning')
1375 raise HTTPFound(redirect_url)
1374 raise HTTPFound(redirect_url)
1376
1375
1377 # extract the location from filename,
1376 # extract the location from filename,
1378 # allows using foo/bar.txt syntax to create subdirectories
1377 # allows using foo/bar.txt syntax to create subdirectories
1379 subdir_loc = filename.rsplit('/', 1)
1378 subdir_loc = filename.rsplit('/', 1)
1380 if len(subdir_loc) == 2:
1379 if len(subdir_loc) == 2:
1381 location = os.path.join(location, subdir_loc[0])
1380 location = os.path.join(location, subdir_loc[0])
1382
1381
1383 # strip all crap out of file, just leave the basename
1382 # strip all crap out of file, just leave the basename
1384 filename = os.path.basename(filename)
1383 filename = os.path.basename(filename)
1385 node_path = os.path.join(location, filename)
1384 node_path = os.path.join(location, filename)
1386 author = self._rhodecode_db_user.full_contact
1385 author = self._rhodecode_db_user.full_contact
1387
1386
1388 try:
1387 try:
1389 nodes = {
1388 nodes = {
1390 node_path: {
1389 node_path: {
1391 'content': content
1390 'content': content
1392 }
1391 }
1393 }
1392 }
1394 ScmModel().create_nodes(
1393 ScmModel().create_nodes(
1395 user=self._rhodecode_db_user.user_id,
1394 user=self._rhodecode_db_user.user_id,
1396 repo=self.db_repo,
1395 repo=self.db_repo,
1397 message=message,
1396 message=message,
1398 nodes=nodes,
1397 nodes=nodes,
1399 parent_commit=c.commit,
1398 parent_commit=c.commit,
1400 author=author,
1399 author=author,
1401 )
1400 )
1402
1401
1403 h.flash(
1402 h.flash(
1404 _('Successfully committed new file `{}`').format(
1403 _('Successfully committed new file `{}`').format(
1405 h.escape(node_path)), category='success')
1404 h.escape(node_path)), category='success')
1406 except NonRelativePathError:
1405 except NonRelativePathError:
1407 log.exception('Non Relative path found')
1406 log.exception('Non Relative path found')
1408 h.flash(_(
1407 h.flash(_(
1409 'The location specified must be a relative path and must not '
1408 'The location specified must be a relative path and must not '
1410 'contain .. in the path'), category='warning')
1409 'contain .. in the path'), category='warning')
1411 raise HTTPFound(default_redirect_url)
1410 raise HTTPFound(default_redirect_url)
1412 except (NodeError, NodeAlreadyExistsError) as e:
1411 except (NodeError, NodeAlreadyExistsError) as e:
1413 h.flash(_(h.escape(e)), category='error')
1412 h.flash(_(h.escape(e)), category='error')
1414 except Exception:
1413 except Exception:
1415 log.exception('Error occurred during commit')
1414 log.exception('Error occurred during commit')
1416 h.flash(_('Error occurred during commit'), category='error')
1415 h.flash(_('Error occurred during commit'), category='error')
1417
1416
1418 raise HTTPFound(default_redirect_url)
1417 raise HTTPFound(default_redirect_url)
@@ -1,1850 +1,1850 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-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 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24 import os
24 import os
25 import re
25 import re
26 import time
26 import time
27 import shutil
27 import shutil
28 import datetime
28 import datetime
29 import fnmatch
29 import fnmatch
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import collections
32 import collections
33 import warnings
33 import warnings
34
34
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 from pyramid import compat
36 from pyramid import compat
37
37
38 from rhodecode.translation import lazy_ugettext
38 from rhodecode.translation import lazy_ugettext
39 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 from rhodecode.lib.utils2 import safe_str, safe_unicode
40 from rhodecode.lib.vcs import connection
40 from rhodecode.lib.vcs import connection
41 from rhodecode.lib.vcs.utils import author_name, author_email
41 from rhodecode.lib.vcs.utils import author_name, author_email
42 from rhodecode.lib.vcs.conf import settings
42 from rhodecode.lib.vcs.conf import settings
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
44 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
45 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
45 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
46 NodeDoesNotExistError, NodeNotChangedError, VCSError,
46 NodeDoesNotExistError, NodeNotChangedError, VCSError,
47 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
47 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
48 RepositoryError)
48 RepositoryError)
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 FILEMODE_DEFAULT = 0o100644
54 FILEMODE_DEFAULT = 0o100644
55 FILEMODE_EXECUTABLE = 0o100755
55 FILEMODE_EXECUTABLE = 0o100755
56
56
57 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
57 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
58
58
59
59
60 class MergeFailureReason(object):
60 class MergeFailureReason(object):
61 """
61 """
62 Enumeration with all the reasons why the server side merge could fail.
62 Enumeration with all the reasons why the server side merge could fail.
63
63
64 DO NOT change the number of the reasons, as they may be stored in the
64 DO NOT change the number of the reasons, as they may be stored in the
65 database.
65 database.
66
66
67 Changing the name of a reason is acceptable and encouraged to deprecate old
67 Changing the name of a reason is acceptable and encouraged to deprecate old
68 reasons.
68 reasons.
69 """
69 """
70
70
71 # Everything went well.
71 # Everything went well.
72 NONE = 0
72 NONE = 0
73
73
74 # An unexpected exception was raised. Check the logs for more details.
74 # An unexpected exception was raised. Check the logs for more details.
75 UNKNOWN = 1
75 UNKNOWN = 1
76
76
77 # The merge was not successful, there are conflicts.
77 # The merge was not successful, there are conflicts.
78 MERGE_FAILED = 2
78 MERGE_FAILED = 2
79
79
80 # The merge succeeded but we could not push it to the target repository.
80 # The merge succeeded but we could not push it to the target repository.
81 PUSH_FAILED = 3
81 PUSH_FAILED = 3
82
82
83 # The specified target is not a head in the target repository.
83 # The specified target is not a head in the target repository.
84 TARGET_IS_NOT_HEAD = 4
84 TARGET_IS_NOT_HEAD = 4
85
85
86 # The source repository contains more branches than the target. Pushing
86 # The source repository contains more branches than the target. Pushing
87 # the merge will create additional branches in the target.
87 # the merge will create additional branches in the target.
88 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 HG_SOURCE_HAS_MORE_BRANCHES = 5
89
89
90 # The target reference has multiple heads. That does not allow to correctly
90 # The target reference has multiple heads. That does not allow to correctly
91 # identify the target location. This could only happen for mercurial
91 # identify the target location. This could only happen for mercurial
92 # branches.
92 # branches.
93 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 HG_TARGET_HAS_MULTIPLE_HEADS = 6
94
94
95 # The target repository is locked
95 # The target repository is locked
96 TARGET_IS_LOCKED = 7
96 TARGET_IS_LOCKED = 7
97
97
98 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
99 # A involved commit could not be found.
99 # A involved commit could not be found.
100 _DEPRECATED_MISSING_COMMIT = 8
100 _DEPRECATED_MISSING_COMMIT = 8
101
101
102 # The target repo reference is missing.
102 # The target repo reference is missing.
103 MISSING_TARGET_REF = 9
103 MISSING_TARGET_REF = 9
104
104
105 # The source repo reference is missing.
105 # The source repo reference is missing.
106 MISSING_SOURCE_REF = 10
106 MISSING_SOURCE_REF = 10
107
107
108 # The merge was not successful, there are conflicts related to sub
108 # The merge was not successful, there are conflicts related to sub
109 # repositories.
109 # repositories.
110 SUBREPO_MERGE_FAILED = 11
110 SUBREPO_MERGE_FAILED = 11
111
111
112
112
113 class UpdateFailureReason(object):
113 class UpdateFailureReason(object):
114 """
114 """
115 Enumeration with all the reasons why the pull request update could fail.
115 Enumeration with all the reasons why the pull request update could fail.
116
116
117 DO NOT change the number of the reasons, as they may be stored in the
117 DO NOT change the number of the reasons, as they may be stored in the
118 database.
118 database.
119
119
120 Changing the name of a reason is acceptable and encouraged to deprecate old
120 Changing the name of a reason is acceptable and encouraged to deprecate old
121 reasons.
121 reasons.
122 """
122 """
123
123
124 # Everything went well.
124 # Everything went well.
125 NONE = 0
125 NONE = 0
126
126
127 # An unexpected exception was raised. Check the logs for more details.
127 # An unexpected exception was raised. Check the logs for more details.
128 UNKNOWN = 1
128 UNKNOWN = 1
129
129
130 # The pull request is up to date.
130 # The pull request is up to date.
131 NO_CHANGE = 2
131 NO_CHANGE = 2
132
132
133 # The pull request has a reference type that is not supported for update.
133 # The pull request has a reference type that is not supported for update.
134 WRONG_REF_TYPE = 3
134 WRONG_REF_TYPE = 3
135
135
136 # Update failed because the target reference is missing.
136 # Update failed because the target reference is missing.
137 MISSING_TARGET_REF = 4
137 MISSING_TARGET_REF = 4
138
138
139 # Update failed because the source reference is missing.
139 # Update failed because the source reference is missing.
140 MISSING_SOURCE_REF = 5
140 MISSING_SOURCE_REF = 5
141
141
142
142
143 class MergeResponse(object):
143 class MergeResponse(object):
144
144
145 # uses .format(**metadata) for variables
145 # uses .format(**metadata) for variables
146 MERGE_STATUS_MESSAGES = {
146 MERGE_STATUS_MESSAGES = {
147 MergeFailureReason.NONE: lazy_ugettext(
147 MergeFailureReason.NONE: lazy_ugettext(
148 u'This pull request can be automatically merged.'),
148 u'This pull request can be automatically merged.'),
149 MergeFailureReason.UNKNOWN: lazy_ugettext(
149 MergeFailureReason.UNKNOWN: lazy_ugettext(
150 u'This pull request cannot be merged because of an unhandled exception. '
150 u'This pull request cannot be merged because of an unhandled exception. '
151 u'{exception}'),
151 u'{exception}'),
152 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
152 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
153 u'This pull request cannot be merged because of merge conflicts.'),
153 u'This pull request cannot be merged because of merge conflicts.'),
154 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
154 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
155 u'This pull request could not be merged because push to '
155 u'This pull request could not be merged because push to '
156 u'target:`{target}@{merge_commit}` failed.'),
156 u'target:`{target}@{merge_commit}` failed.'),
157 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
157 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
158 u'This pull request cannot be merged because the target '
158 u'This pull request cannot be merged because the target '
159 u'`{target_ref.name}` is not a head.'),
159 u'`{target_ref.name}` is not a head.'),
160 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
160 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
161 u'This pull request cannot be merged because the source contains '
161 u'This pull request cannot be merged because the source contains '
162 u'more branches than the target.'),
162 u'more branches than the target.'),
163 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
163 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
164 u'This pull request cannot be merged because the target `{target_ref.name}` '
164 u'This pull request cannot be merged because the target `{target_ref.name}` '
165 u'has multiple heads: `{heads}`.'),
165 u'has multiple heads: `{heads}`.'),
166 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
166 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
167 u'This pull request cannot be merged because the target repository is '
167 u'This pull request cannot be merged because the target repository is '
168 u'locked by {locked_by}.'),
168 u'locked by {locked_by}.'),
169
169
170 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
170 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
171 u'This pull request cannot be merged because the target '
171 u'This pull request cannot be merged because the target '
172 u'reference `{target_ref.name}` is missing.'),
172 u'reference `{target_ref.name}` is missing.'),
173 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
173 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
174 u'This pull request cannot be merged because the source '
174 u'This pull request cannot be merged because the source '
175 u'reference `{source_ref.name}` is missing.'),
175 u'reference `{source_ref.name}` is missing.'),
176 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
176 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
177 u'This pull request cannot be merged because of conflicts related '
177 u'This pull request cannot be merged because of conflicts related '
178 u'to sub repositories.'),
178 u'to sub repositories.'),
179
179
180 # Deprecations
180 # Deprecations
181 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
181 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
182 u'This pull request cannot be merged because the target or the '
182 u'This pull request cannot be merged because the target or the '
183 u'source reference is missing.'),
183 u'source reference is missing.'),
184
184
185 }
185 }
186
186
187 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
187 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
188 self.possible = possible
188 self.possible = possible
189 self.executed = executed
189 self.executed = executed
190 self.merge_ref = merge_ref
190 self.merge_ref = merge_ref
191 self.failure_reason = failure_reason
191 self.failure_reason = failure_reason
192 self.metadata = metadata or {}
192 self.metadata = metadata or {}
193
193
194 def __repr__(self):
194 def __repr__(self):
195 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
195 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
196
196
197 def __eq__(self, other):
197 def __eq__(self, other):
198 same_instance = isinstance(other, self.__class__)
198 same_instance = isinstance(other, self.__class__)
199 return same_instance \
199 return same_instance \
200 and self.possible == other.possible \
200 and self.possible == other.possible \
201 and self.executed == other.executed \
201 and self.executed == other.executed \
202 and self.failure_reason == other.failure_reason
202 and self.failure_reason == other.failure_reason
203
203
204 @property
204 @property
205 def label(self):
205 def label(self):
206 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
206 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
207 not k.startswith('_'))
207 not k.startswith('_'))
208 return label_dict.get(self.failure_reason)
208 return label_dict.get(self.failure_reason)
209
209
210 @property
210 @property
211 def merge_status_message(self):
211 def merge_status_message(self):
212 """
212 """
213 Return a human friendly error message for the given merge status code.
213 Return a human friendly error message for the given merge status code.
214 """
214 """
215 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
215 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
216 try:
216 try:
217 return msg.format(**self.metadata)
217 return msg.format(**self.metadata)
218 except Exception:
218 except Exception:
219 log.exception('Failed to format %s message', self)
219 log.exception('Failed to format %s message', self)
220 return msg
220 return msg
221
221
222 def asdict(self):
222 def asdict(self):
223 data = {}
223 data = {}
224 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
224 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
225 'merge_status_message']:
225 'merge_status_message']:
226 data[k] = getattr(self, k)
226 data[k] = getattr(self, k)
227 return data
227 return data
228
228
229
229
230 class BaseRepository(object):
230 class BaseRepository(object):
231 """
231 """
232 Base Repository for final backends
232 Base Repository for final backends
233
233
234 .. attribute:: DEFAULT_BRANCH_NAME
234 .. attribute:: DEFAULT_BRANCH_NAME
235
235
236 name of default branch (i.e. "trunk" for svn, "master" for git etc.
236 name of default branch (i.e. "trunk" for svn, "master" for git etc.
237
237
238 .. attribute:: commit_ids
238 .. attribute:: commit_ids
239
239
240 list of all available commit ids, in ascending order
240 list of all available commit ids, in ascending order
241
241
242 .. attribute:: path
242 .. attribute:: path
243
243
244 absolute path to the repository
244 absolute path to the repository
245
245
246 .. attribute:: bookmarks
246 .. attribute:: bookmarks
247
247
248 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
248 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
249 there are no bookmarks or the backend implementation does not support
249 there are no bookmarks or the backend implementation does not support
250 bookmarks.
250 bookmarks.
251
251
252 .. attribute:: tags
252 .. attribute:: tags
253
253
254 Mapping from name to :term:`Commit ID` of the tag.
254 Mapping from name to :term:`Commit ID` of the tag.
255
255
256 """
256 """
257
257
258 DEFAULT_BRANCH_NAME = None
258 DEFAULT_BRANCH_NAME = None
259 DEFAULT_CONTACT = u"Unknown"
259 DEFAULT_CONTACT = u"Unknown"
260 DEFAULT_DESCRIPTION = u"unknown"
260 DEFAULT_DESCRIPTION = u"unknown"
261 EMPTY_COMMIT_ID = '0' * 40
261 EMPTY_COMMIT_ID = '0' * 40
262
262
263 path = None
263 path = None
264
264
265 def __init__(self, repo_path, config=None, create=False, **kwargs):
265 def __init__(self, repo_path, config=None, create=False, **kwargs):
266 """
266 """
267 Initializes repository. Raises RepositoryError if repository could
267 Initializes repository. Raises RepositoryError if repository could
268 not be find at the given ``repo_path`` or directory at ``repo_path``
268 not be find at the given ``repo_path`` or directory at ``repo_path``
269 exists and ``create`` is set to True.
269 exists and ``create`` is set to True.
270
270
271 :param repo_path: local path of the repository
271 :param repo_path: local path of the repository
272 :param config: repository configuration
272 :param config: repository configuration
273 :param create=False: if set to True, would try to create repository.
273 :param create=False: if set to True, would try to create repository.
274 :param src_url=None: if set, should be proper url from which repository
274 :param src_url=None: if set, should be proper url from which repository
275 would be cloned; requires ``create`` parameter to be set to True -
275 would be cloned; requires ``create`` parameter to be set to True -
276 raises RepositoryError if src_url is set and create evaluates to
276 raises RepositoryError if src_url is set and create evaluates to
277 False
277 False
278 """
278 """
279 raise NotImplementedError
279 raise NotImplementedError
280
280
281 def __repr__(self):
281 def __repr__(self):
282 return '<%s at %s>' % (self.__class__.__name__, self.path)
282 return '<%s at %s>' % (self.__class__.__name__, self.path)
283
283
284 def __len__(self):
284 def __len__(self):
285 return self.count()
285 return self.count()
286
286
287 def __eq__(self, other):
287 def __eq__(self, other):
288 same_instance = isinstance(other, self.__class__)
288 same_instance = isinstance(other, self.__class__)
289 return same_instance and other.path == self.path
289 return same_instance and other.path == self.path
290
290
291 def __ne__(self, other):
291 def __ne__(self, other):
292 return not self.__eq__(other)
292 return not self.__eq__(other)
293
293
294 def get_create_shadow_cache_pr_path(self, db_repo):
294 def get_create_shadow_cache_pr_path(self, db_repo):
295 path = db_repo.cached_diffs_dir
295 path = db_repo.cached_diffs_dir
296 if not os.path.exists(path):
296 if not os.path.exists(path):
297 os.makedirs(path, 0o755)
297 os.makedirs(path, 0o755)
298 return path
298 return path
299
299
300 @classmethod
300 @classmethod
301 def get_default_config(cls, default=None):
301 def get_default_config(cls, default=None):
302 config = Config()
302 config = Config()
303 if default and isinstance(default, list):
303 if default and isinstance(default, list):
304 for section, key, val in default:
304 for section, key, val in default:
305 config.set(section, key, val)
305 config.set(section, key, val)
306 return config
306 return config
307
307
308 @LazyProperty
308 @LazyProperty
309 def _remote(self):
309 def _remote(self):
310 raise NotImplementedError
310 raise NotImplementedError
311
311
312 def _heads(self, branch=None):
312 def _heads(self, branch=None):
313 return []
313 return []
314
314
315 @LazyProperty
315 @LazyProperty
316 def EMPTY_COMMIT(self):
316 def EMPTY_COMMIT(self):
317 return EmptyCommit(self.EMPTY_COMMIT_ID)
317 return EmptyCommit(self.EMPTY_COMMIT_ID)
318
318
319 @LazyProperty
319 @LazyProperty
320 def alias(self):
320 def alias(self):
321 for k, v in settings.BACKENDS.items():
321 for k, v in settings.BACKENDS.items():
322 if v.split('.')[-1] == str(self.__class__.__name__):
322 if v.split('.')[-1] == str(self.__class__.__name__):
323 return k
323 return k
324
324
325 @LazyProperty
325 @LazyProperty
326 def name(self):
326 def name(self):
327 return safe_unicode(os.path.basename(self.path))
327 return safe_unicode(os.path.basename(self.path))
328
328
329 @LazyProperty
329 @LazyProperty
330 def description(self):
330 def description(self):
331 raise NotImplementedError
331 raise NotImplementedError
332
332
333 def refs(self):
333 def refs(self):
334 """
334 """
335 returns a `dict` with branches, bookmarks, tags, and closed_branches
335 returns a `dict` with branches, bookmarks, tags, and closed_branches
336 for this repository
336 for this repository
337 """
337 """
338 return dict(
338 return dict(
339 branches=self.branches,
339 branches=self.branches,
340 branches_closed=self.branches_closed,
340 branches_closed=self.branches_closed,
341 tags=self.tags,
341 tags=self.tags,
342 bookmarks=self.bookmarks
342 bookmarks=self.bookmarks
343 )
343 )
344
344
345 @LazyProperty
345 @LazyProperty
346 def branches(self):
346 def branches(self):
347 """
347 """
348 A `dict` which maps branch names to commit ids.
348 A `dict` which maps branch names to commit ids.
349 """
349 """
350 raise NotImplementedError
350 raise NotImplementedError
351
351
352 @LazyProperty
352 @LazyProperty
353 def branches_closed(self):
353 def branches_closed(self):
354 """
354 """
355 A `dict` which maps tags names to commit ids.
355 A `dict` which maps tags names to commit ids.
356 """
356 """
357 raise NotImplementedError
357 raise NotImplementedError
358
358
359 @LazyProperty
359 @LazyProperty
360 def bookmarks(self):
360 def bookmarks(self):
361 """
361 """
362 A `dict` which maps tags names to commit ids.
362 A `dict` which maps tags names to commit ids.
363 """
363 """
364 raise NotImplementedError
364 raise NotImplementedError
365
365
366 @LazyProperty
366 @LazyProperty
367 def tags(self):
367 def tags(self):
368 """
368 """
369 A `dict` which maps tags names to commit ids.
369 A `dict` which maps tags names to commit ids.
370 """
370 """
371 raise NotImplementedError
371 raise NotImplementedError
372
372
373 @LazyProperty
373 @LazyProperty
374 def size(self):
374 def size(self):
375 """
375 """
376 Returns combined size in bytes for all repository files
376 Returns combined size in bytes for all repository files
377 """
377 """
378 tip = self.get_commit()
378 tip = self.get_commit()
379 return tip.size
379 return tip.size
380
380
381 def size_at_commit(self, commit_id):
381 def size_at_commit(self, commit_id):
382 commit = self.get_commit(commit_id)
382 commit = self.get_commit(commit_id)
383 return commit.size
383 return commit.size
384
384
385 def is_empty(self):
385 def is_empty(self):
386 return self._remote.is_empty()
386 return self._remote.is_empty()
387
387
388 @staticmethod
388 @staticmethod
389 def check_url(url, config):
389 def check_url(url, config):
390 """
390 """
391 Function will check given url and try to verify if it's a valid
391 Function will check given url and try to verify if it's a valid
392 link.
392 link.
393 """
393 """
394 raise NotImplementedError
394 raise NotImplementedError
395
395
396 @staticmethod
396 @staticmethod
397 def is_valid_repository(path):
397 def is_valid_repository(path):
398 """
398 """
399 Check if given `path` contains a valid repository of this backend
399 Check if given `path` contains a valid repository of this backend
400 """
400 """
401 raise NotImplementedError
401 raise NotImplementedError
402
402
403 # ==========================================================================
403 # ==========================================================================
404 # COMMITS
404 # COMMITS
405 # ==========================================================================
405 # ==========================================================================
406
406
407 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
407 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
408 """
408 """
409 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
409 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
410 are both None, most recent commit is returned.
410 are both None, most recent commit is returned.
411
411
412 :param pre_load: Optional. List of commit attributes to load.
412 :param pre_load: Optional. List of commit attributes to load.
413
413
414 :raises ``EmptyRepositoryError``: if there are no commits
414 :raises ``EmptyRepositoryError``: if there are no commits
415 """
415 """
416 raise NotImplementedError
416 raise NotImplementedError
417
417
418 def __iter__(self):
418 def __iter__(self):
419 for commit_id in self.commit_ids:
419 for commit_id in self.commit_ids:
420 yield self.get_commit(commit_id=commit_id)
420 yield self.get_commit(commit_id=commit_id)
421
421
422 def get_commits(
422 def get_commits(
423 self, start_id=None, end_id=None, start_date=None, end_date=None,
423 self, start_id=None, end_id=None, start_date=None, end_date=None,
424 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
424 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
425 """
425 """
426 Returns iterator of `BaseCommit` objects from start to end
426 Returns iterator of `BaseCommit` objects from start to end
427 not inclusive. This should behave just like a list, ie. end is not
427 not inclusive. This should behave just like a list, ie. end is not
428 inclusive.
428 inclusive.
429
429
430 :param start_id: None or str, must be a valid commit id
430 :param start_id: None or str, must be a valid commit id
431 :param end_id: None or str, must be a valid commit id
431 :param end_id: None or str, must be a valid commit id
432 :param start_date:
432 :param start_date:
433 :param end_date:
433 :param end_date:
434 :param branch_name:
434 :param branch_name:
435 :param show_hidden:
435 :param show_hidden:
436 :param pre_load:
436 :param pre_load:
437 :param translate_tags:
437 :param translate_tags:
438 """
438 """
439 raise NotImplementedError
439 raise NotImplementedError
440
440
441 def __getitem__(self, key):
441 def __getitem__(self, key):
442 """
442 """
443 Allows index based access to the commit objects of this repository.
443 Allows index based access to the commit objects of this repository.
444 """
444 """
445 pre_load = ["author", "branch", "date", "message", "parents"]
445 pre_load = ["author", "branch", "date", "message", "parents"]
446 if isinstance(key, slice):
446 if isinstance(key, slice):
447 return self._get_range(key, pre_load)
447 return self._get_range(key, pre_load)
448 return self.get_commit(commit_idx=key, pre_load=pre_load)
448 return self.get_commit(commit_idx=key, pre_load=pre_load)
449
449
450 def _get_range(self, slice_obj, pre_load):
450 def _get_range(self, slice_obj, pre_load):
451 for commit_id in self.commit_ids.__getitem__(slice_obj):
451 for commit_id in self.commit_ids.__getitem__(slice_obj):
452 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
452 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
453
453
454 def count(self):
454 def count(self):
455 return len(self.commit_ids)
455 return len(self.commit_ids)
456
456
457 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
457 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
458 """
458 """
459 Creates and returns a tag for the given ``commit_id``.
459 Creates and returns a tag for the given ``commit_id``.
460
460
461 :param name: name for new tag
461 :param name: name for new tag
462 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
462 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
463 :param commit_id: commit id for which new tag would be created
463 :param commit_id: commit id for which new tag would be created
464 :param message: message of the tag's commit
464 :param message: message of the tag's commit
465 :param date: date of tag's commit
465 :param date: date of tag's commit
466
466
467 :raises TagAlreadyExistError: if tag with same name already exists
467 :raises TagAlreadyExistError: if tag with same name already exists
468 """
468 """
469 raise NotImplementedError
469 raise NotImplementedError
470
470
471 def remove_tag(self, name, user, message=None, date=None):
471 def remove_tag(self, name, user, message=None, date=None):
472 """
472 """
473 Removes tag with the given ``name``.
473 Removes tag with the given ``name``.
474
474
475 :param name: name of the tag to be removed
475 :param name: name of the tag to be removed
476 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
476 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
477 :param message: message of the tag's removal commit
477 :param message: message of the tag's removal commit
478 :param date: date of tag's removal commit
478 :param date: date of tag's removal commit
479
479
480 :raises TagDoesNotExistError: if tag with given name does not exists
480 :raises TagDoesNotExistError: if tag with given name does not exists
481 """
481 """
482 raise NotImplementedError
482 raise NotImplementedError
483
483
484 def get_diff(
484 def get_diff(
485 self, commit1, commit2, path=None, ignore_whitespace=False,
485 self, commit1, commit2, path=None, ignore_whitespace=False,
486 context=3, path1=None):
486 context=3, path1=None):
487 """
487 """
488 Returns (git like) *diff*, as plain text. Shows changes introduced by
488 Returns (git like) *diff*, as plain text. Shows changes introduced by
489 `commit2` since `commit1`.
489 `commit2` since `commit1`.
490
490
491 :param commit1: Entry point from which diff is shown. Can be
491 :param commit1: Entry point from which diff is shown. Can be
492 ``self.EMPTY_COMMIT`` - in this case, patch showing all
492 ``self.EMPTY_COMMIT`` - in this case, patch showing all
493 the changes since empty state of the repository until `commit2`
493 the changes since empty state of the repository until `commit2`
494 :param commit2: Until which commit changes should be shown.
494 :param commit2: Until which commit changes should be shown.
495 :param path: Can be set to a path of a file to create a diff of that
495 :param path: Can be set to a path of a file to create a diff of that
496 file. If `path1` is also set, this value is only associated to
496 file. If `path1` is also set, this value is only associated to
497 `commit2`.
497 `commit2`.
498 :param ignore_whitespace: If set to ``True``, would not show whitespace
498 :param ignore_whitespace: If set to ``True``, would not show whitespace
499 changes. Defaults to ``False``.
499 changes. Defaults to ``False``.
500 :param context: How many lines before/after changed lines should be
500 :param context: How many lines before/after changed lines should be
501 shown. Defaults to ``3``.
501 shown. Defaults to ``3``.
502 :param path1: Can be set to a path to associate with `commit1`. This
502 :param path1: Can be set to a path to associate with `commit1`. This
503 parameter works only for backends which support diff generation for
503 parameter works only for backends which support diff generation for
504 different paths. Other backends will raise a `ValueError` if `path1`
504 different paths. Other backends will raise a `ValueError` if `path1`
505 is set and has a different value than `path`.
505 is set and has a different value than `path`.
506 :param file_path: filter this diff by given path pattern
506 :param file_path: filter this diff by given path pattern
507 """
507 """
508 raise NotImplementedError
508 raise NotImplementedError
509
509
510 def strip(self, commit_id, branch=None):
510 def strip(self, commit_id, branch=None):
511 """
511 """
512 Strip given commit_id from the repository
512 Strip given commit_id from the repository
513 """
513 """
514 raise NotImplementedError
514 raise NotImplementedError
515
515
516 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
516 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
517 """
517 """
518 Return a latest common ancestor commit if one exists for this repo
518 Return a latest common ancestor commit if one exists for this repo
519 `commit_id1` vs `commit_id2` from `repo2`.
519 `commit_id1` vs `commit_id2` from `repo2`.
520
520
521 :param commit_id1: Commit it from this repository to use as a
521 :param commit_id1: Commit it from this repository to use as a
522 target for the comparison.
522 target for the comparison.
523 :param commit_id2: Source commit id to use for comparison.
523 :param commit_id2: Source commit id to use for comparison.
524 :param repo2: Source repository to use for comparison.
524 :param repo2: Source repository to use for comparison.
525 """
525 """
526 raise NotImplementedError
526 raise NotImplementedError
527
527
528 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
528 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
529 """
529 """
530 Compare this repository's revision `commit_id1` with `commit_id2`.
530 Compare this repository's revision `commit_id1` with `commit_id2`.
531
531
532 Returns a tuple(commits, ancestor) that would be merged from
532 Returns a tuple(commits, ancestor) that would be merged from
533 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
533 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
534 will be returned as ancestor.
534 will be returned as ancestor.
535
535
536 :param commit_id1: Commit it from this repository to use as a
536 :param commit_id1: Commit it from this repository to use as a
537 target for the comparison.
537 target for the comparison.
538 :param commit_id2: Source commit id to use for comparison.
538 :param commit_id2: Source commit id to use for comparison.
539 :param repo2: Source repository to use for comparison.
539 :param repo2: Source repository to use for comparison.
540 :param merge: If set to ``True`` will do a merge compare which also
540 :param merge: If set to ``True`` will do a merge compare which also
541 returns the common ancestor.
541 returns the common ancestor.
542 :param pre_load: Optional. List of commit attributes to load.
542 :param pre_load: Optional. List of commit attributes to load.
543 """
543 """
544 raise NotImplementedError
544 raise NotImplementedError
545
545
546 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
546 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
547 user_name='', user_email='', message='', dry_run=False,
547 user_name='', user_email='', message='', dry_run=False,
548 use_rebase=False, close_branch=False):
548 use_rebase=False, close_branch=False):
549 """
549 """
550 Merge the revisions specified in `source_ref` from `source_repo`
550 Merge the revisions specified in `source_ref` from `source_repo`
551 onto the `target_ref` of this repository.
551 onto the `target_ref` of this repository.
552
552
553 `source_ref` and `target_ref` are named tupls with the following
553 `source_ref` and `target_ref` are named tupls with the following
554 fields `type`, `name` and `commit_id`.
554 fields `type`, `name` and `commit_id`.
555
555
556 Returns a MergeResponse named tuple with the following fields
556 Returns a MergeResponse named tuple with the following fields
557 'possible', 'executed', 'source_commit', 'target_commit',
557 'possible', 'executed', 'source_commit', 'target_commit',
558 'merge_commit'.
558 'merge_commit'.
559
559
560 :param repo_id: `repo_id` target repo id.
560 :param repo_id: `repo_id` target repo id.
561 :param workspace_id: `workspace_id` unique identifier.
561 :param workspace_id: `workspace_id` unique identifier.
562 :param target_ref: `target_ref` points to the commit on top of which
562 :param target_ref: `target_ref` points to the commit on top of which
563 the `source_ref` should be merged.
563 the `source_ref` should be merged.
564 :param source_repo: The repository that contains the commits to be
564 :param source_repo: The repository that contains the commits to be
565 merged.
565 merged.
566 :param source_ref: `source_ref` points to the topmost commit from
566 :param source_ref: `source_ref` points to the topmost commit from
567 the `source_repo` which should be merged.
567 the `source_repo` which should be merged.
568 :param user_name: Merge commit `user_name`.
568 :param user_name: Merge commit `user_name`.
569 :param user_email: Merge commit `user_email`.
569 :param user_email: Merge commit `user_email`.
570 :param message: Merge commit `message`.
570 :param message: Merge commit `message`.
571 :param dry_run: If `True` the merge will not take place.
571 :param dry_run: If `True` the merge will not take place.
572 :param use_rebase: If `True` commits from the source will be rebased
572 :param use_rebase: If `True` commits from the source will be rebased
573 on top of the target instead of being merged.
573 on top of the target instead of being merged.
574 :param close_branch: If `True` branch will be close before merging it
574 :param close_branch: If `True` branch will be close before merging it
575 """
575 """
576 if dry_run:
576 if dry_run:
577 message = message or settings.MERGE_DRY_RUN_MESSAGE
577 message = message or settings.MERGE_DRY_RUN_MESSAGE
578 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
578 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
579 user_name = user_name or settings.MERGE_DRY_RUN_USER
579 user_name = user_name or settings.MERGE_DRY_RUN_USER
580 else:
580 else:
581 if not user_name:
581 if not user_name:
582 raise ValueError('user_name cannot be empty')
582 raise ValueError('user_name cannot be empty')
583 if not user_email:
583 if not user_email:
584 raise ValueError('user_email cannot be empty')
584 raise ValueError('user_email cannot be empty')
585 if not message:
585 if not message:
586 raise ValueError('message cannot be empty')
586 raise ValueError('message cannot be empty')
587
587
588 try:
588 try:
589 return self._merge_repo(
589 return self._merge_repo(
590 repo_id, workspace_id, target_ref, source_repo,
590 repo_id, workspace_id, target_ref, source_repo,
591 source_ref, message, user_name, user_email, dry_run=dry_run,
591 source_ref, message, user_name, user_email, dry_run=dry_run,
592 use_rebase=use_rebase, close_branch=close_branch)
592 use_rebase=use_rebase, close_branch=close_branch)
593 except RepositoryError as exc:
593 except RepositoryError as exc:
594 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
594 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
595 return MergeResponse(
595 return MergeResponse(
596 False, False, None, MergeFailureReason.UNKNOWN,
596 False, False, None, MergeFailureReason.UNKNOWN,
597 metadata={'exception': str(exc)})
597 metadata={'exception': str(exc)})
598
598
599 def _merge_repo(self, repo_id, workspace_id, target_ref,
599 def _merge_repo(self, repo_id, workspace_id, target_ref,
600 source_repo, source_ref, merge_message,
600 source_repo, source_ref, merge_message,
601 merger_name, merger_email, dry_run=False,
601 merger_name, merger_email, dry_run=False,
602 use_rebase=False, close_branch=False):
602 use_rebase=False, close_branch=False):
603 """Internal implementation of merge."""
603 """Internal implementation of merge."""
604 raise NotImplementedError
604 raise NotImplementedError
605
605
606 def _maybe_prepare_merge_workspace(
606 def _maybe_prepare_merge_workspace(
607 self, repo_id, workspace_id, target_ref, source_ref):
607 self, repo_id, workspace_id, target_ref, source_ref):
608 """
608 """
609 Create the merge workspace.
609 Create the merge workspace.
610
610
611 :param workspace_id: `workspace_id` unique identifier.
611 :param workspace_id: `workspace_id` unique identifier.
612 """
612 """
613 raise NotImplementedError
613 raise NotImplementedError
614
614
615 def _get_legacy_shadow_repository_path(self, workspace_id):
615 def _get_legacy_shadow_repository_path(self, workspace_id):
616 """
616 """
617 Legacy version that was used before. We still need it for
617 Legacy version that was used before. We still need it for
618 backward compat
618 backward compat
619 """
619 """
620 return os.path.join(
620 return os.path.join(
621 os.path.dirname(self.path),
621 os.path.dirname(self.path),
622 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
622 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
623
623
624 def _get_shadow_repository_path(self, repo_id, workspace_id):
624 def _get_shadow_repository_path(self, repo_id, workspace_id):
625 # The name of the shadow repository must start with '.', so it is
625 # The name of the shadow repository must start with '.', so it is
626 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
626 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
627 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
627 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
628 if os.path.exists(legacy_repository_path):
628 if os.path.exists(legacy_repository_path):
629 return legacy_repository_path
629 return legacy_repository_path
630 else:
630 else:
631 return os.path.join(
631 return os.path.join(
632 os.path.dirname(self.path),
632 os.path.dirname(self.path),
633 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
633 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
634
634
635 def cleanup_merge_workspace(self, repo_id, workspace_id):
635 def cleanup_merge_workspace(self, repo_id, workspace_id):
636 """
636 """
637 Remove merge workspace.
637 Remove merge workspace.
638
638
639 This function MUST not fail in case there is no workspace associated to
639 This function MUST not fail in case there is no workspace associated to
640 the given `workspace_id`.
640 the given `workspace_id`.
641
641
642 :param workspace_id: `workspace_id` unique identifier.
642 :param workspace_id: `workspace_id` unique identifier.
643 """
643 """
644 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
644 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
645 shadow_repository_path_del = '{}.{}.delete'.format(
645 shadow_repository_path_del = '{}.{}.delete'.format(
646 shadow_repository_path, time.time())
646 shadow_repository_path, time.time())
647
647
648 # move the shadow repo, so it never conflicts with the one used.
648 # move the shadow repo, so it never conflicts with the one used.
649 # we use this method because shutil.rmtree had some edge case problems
649 # we use this method because shutil.rmtree had some edge case problems
650 # removing symlinked repositories
650 # removing symlinked repositories
651 if not os.path.isdir(shadow_repository_path):
651 if not os.path.isdir(shadow_repository_path):
652 return
652 return
653
653
654 shutil.move(shadow_repository_path, shadow_repository_path_del)
654 shutil.move(shadow_repository_path, shadow_repository_path_del)
655 try:
655 try:
656 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
656 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
657 except Exception:
657 except Exception:
658 log.exception('Failed to gracefully remove shadow repo under %s',
658 log.exception('Failed to gracefully remove shadow repo under %s',
659 shadow_repository_path_del)
659 shadow_repository_path_del)
660 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
660 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
661
661
662 # ========== #
662 # ========== #
663 # COMMIT API #
663 # COMMIT API #
664 # ========== #
664 # ========== #
665
665
666 @LazyProperty
666 @LazyProperty
667 def in_memory_commit(self):
667 def in_memory_commit(self):
668 """
668 """
669 Returns :class:`InMemoryCommit` object for this repository.
669 Returns :class:`InMemoryCommit` object for this repository.
670 """
670 """
671 raise NotImplementedError
671 raise NotImplementedError
672
672
673 # ======================== #
673 # ======================== #
674 # UTILITIES FOR SUBCLASSES #
674 # UTILITIES FOR SUBCLASSES #
675 # ======================== #
675 # ======================== #
676
676
677 def _validate_diff_commits(self, commit1, commit2):
677 def _validate_diff_commits(self, commit1, commit2):
678 """
678 """
679 Validates that the given commits are related to this repository.
679 Validates that the given commits are related to this repository.
680
680
681 Intended as a utility for sub classes to have a consistent validation
681 Intended as a utility for sub classes to have a consistent validation
682 of input parameters in methods like :meth:`get_diff`.
682 of input parameters in methods like :meth:`get_diff`.
683 """
683 """
684 self._validate_commit(commit1)
684 self._validate_commit(commit1)
685 self._validate_commit(commit2)
685 self._validate_commit(commit2)
686 if (isinstance(commit1, EmptyCommit) and
686 if (isinstance(commit1, EmptyCommit) and
687 isinstance(commit2, EmptyCommit)):
687 isinstance(commit2, EmptyCommit)):
688 raise ValueError("Cannot compare two empty commits")
688 raise ValueError("Cannot compare two empty commits")
689
689
690 def _validate_commit(self, commit):
690 def _validate_commit(self, commit):
691 if not isinstance(commit, BaseCommit):
691 if not isinstance(commit, BaseCommit):
692 raise TypeError(
692 raise TypeError(
693 "%s is not of type BaseCommit" % repr(commit))
693 "%s is not of type BaseCommit" % repr(commit))
694 if commit.repository != self and not isinstance(commit, EmptyCommit):
694 if commit.repository != self and not isinstance(commit, EmptyCommit):
695 raise ValueError(
695 raise ValueError(
696 "Commit %s must be a valid commit from this repository %s, "
696 "Commit %s must be a valid commit from this repository %s, "
697 "related to this repository instead %s." %
697 "related to this repository instead %s." %
698 (commit, self, commit.repository))
698 (commit, self, commit.repository))
699
699
700 def _validate_commit_id(self, commit_id):
700 def _validate_commit_id(self, commit_id):
701 if not isinstance(commit_id, compat.string_types):
701 if not isinstance(commit_id, compat.string_types):
702 raise TypeError("commit_id must be a string value")
702 raise TypeError("commit_id must be a string value")
703
703
704 def _validate_commit_idx(self, commit_idx):
704 def _validate_commit_idx(self, commit_idx):
705 if not isinstance(commit_idx, (int, long)):
705 if not isinstance(commit_idx, (int, long)):
706 raise TypeError("commit_idx must be a numeric value")
706 raise TypeError("commit_idx must be a numeric value")
707
707
708 def _validate_branch_name(self, branch_name):
708 def _validate_branch_name(self, branch_name):
709 if branch_name and branch_name not in self.branches_all:
709 if branch_name and branch_name not in self.branches_all:
710 msg = ("Branch %s not found in %s" % (branch_name, self))
710 msg = ("Branch %s not found in %s" % (branch_name, self))
711 raise BranchDoesNotExistError(msg)
711 raise BranchDoesNotExistError(msg)
712
712
713 #
713 #
714 # Supporting deprecated API parts
714 # Supporting deprecated API parts
715 # TODO: johbo: consider to move this into a mixin
715 # TODO: johbo: consider to move this into a mixin
716 #
716 #
717
717
718 @property
718 @property
719 def EMPTY_CHANGESET(self):
719 def EMPTY_CHANGESET(self):
720 warnings.warn(
720 warnings.warn(
721 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
721 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
722 return self.EMPTY_COMMIT_ID
722 return self.EMPTY_COMMIT_ID
723
723
724 @property
724 @property
725 def revisions(self):
725 def revisions(self):
726 warnings.warn("Use commits attribute instead", DeprecationWarning)
726 warnings.warn("Use commits attribute instead", DeprecationWarning)
727 return self.commit_ids
727 return self.commit_ids
728
728
729 @revisions.setter
729 @revisions.setter
730 def revisions(self, value):
730 def revisions(self, value):
731 warnings.warn("Use commits attribute instead", DeprecationWarning)
731 warnings.warn("Use commits attribute instead", DeprecationWarning)
732 self.commit_ids = value
732 self.commit_ids = value
733
733
734 def get_changeset(self, revision=None, pre_load=None):
734 def get_changeset(self, revision=None, pre_load=None):
735 warnings.warn("Use get_commit instead", DeprecationWarning)
735 warnings.warn("Use get_commit instead", DeprecationWarning)
736 commit_id = None
736 commit_id = None
737 commit_idx = None
737 commit_idx = None
738 if isinstance(revision, compat.string_types):
738 if isinstance(revision, compat.string_types):
739 commit_id = revision
739 commit_id = revision
740 else:
740 else:
741 commit_idx = revision
741 commit_idx = revision
742 return self.get_commit(
742 return self.get_commit(
743 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
743 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
744
744
745 def get_changesets(
745 def get_changesets(
746 self, start=None, end=None, start_date=None, end_date=None,
746 self, start=None, end=None, start_date=None, end_date=None,
747 branch_name=None, pre_load=None):
747 branch_name=None, pre_load=None):
748 warnings.warn("Use get_commits instead", DeprecationWarning)
748 warnings.warn("Use get_commits instead", DeprecationWarning)
749 start_id = self._revision_to_commit(start)
749 start_id = self._revision_to_commit(start)
750 end_id = self._revision_to_commit(end)
750 end_id = self._revision_to_commit(end)
751 return self.get_commits(
751 return self.get_commits(
752 start_id=start_id, end_id=end_id, start_date=start_date,
752 start_id=start_id, end_id=end_id, start_date=start_date,
753 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
753 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
754
754
755 def _revision_to_commit(self, revision):
755 def _revision_to_commit(self, revision):
756 """
756 """
757 Translates a revision to a commit_id
757 Translates a revision to a commit_id
758
758
759 Helps to support the old changeset based API which allows to use
759 Helps to support the old changeset based API which allows to use
760 commit ids and commit indices interchangeable.
760 commit ids and commit indices interchangeable.
761 """
761 """
762 if revision is None:
762 if revision is None:
763 return revision
763 return revision
764
764
765 if isinstance(revision, compat.string_types):
765 if isinstance(revision, compat.string_types):
766 commit_id = revision
766 commit_id = revision
767 else:
767 else:
768 commit_id = self.commit_ids[revision]
768 commit_id = self.commit_ids[revision]
769 return commit_id
769 return commit_id
770
770
771 @property
771 @property
772 def in_memory_changeset(self):
772 def in_memory_changeset(self):
773 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
773 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
774 return self.in_memory_commit
774 return self.in_memory_commit
775
775
776 def get_path_permissions(self, username):
776 def get_path_permissions(self, username):
777 """
777 """
778 Returns a path permission checker or None if not supported
778 Returns a path permission checker or None if not supported
779
779
780 :param username: session user name
780 :param username: session user name
781 :return: an instance of BasePathPermissionChecker or None
781 :return: an instance of BasePathPermissionChecker or None
782 """
782 """
783 return None
783 return None
784
784
785 def install_hooks(self, force=False):
785 def install_hooks(self, force=False):
786 return self._remote.install_hooks(force)
786 return self._remote.install_hooks(force)
787
787
788 def get_hooks_info(self):
788 def get_hooks_info(self):
789 return self._remote.get_hooks_info()
789 return self._remote.get_hooks_info()
790
790
791
791
792 class BaseCommit(object):
792 class BaseCommit(object):
793 """
793 """
794 Each backend should implement it's commit representation.
794 Each backend should implement it's commit representation.
795
795
796 **Attributes**
796 **Attributes**
797
797
798 ``repository``
798 ``repository``
799 repository object within which commit exists
799 repository object within which commit exists
800
800
801 ``id``
801 ``id``
802 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
802 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
803 just ``tip``.
803 just ``tip``.
804
804
805 ``raw_id``
805 ``raw_id``
806 raw commit representation (i.e. full 40 length sha for git
806 raw commit representation (i.e. full 40 length sha for git
807 backend)
807 backend)
808
808
809 ``short_id``
809 ``short_id``
810 shortened (if apply) version of ``raw_id``; it would be simple
810 shortened (if apply) version of ``raw_id``; it would be simple
811 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
811 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
812 as ``raw_id`` for subversion
812 as ``raw_id`` for subversion
813
813
814 ``idx``
814 ``idx``
815 commit index
815 commit index
816
816
817 ``files``
817 ``files``
818 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
818 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
819
819
820 ``dirs``
820 ``dirs``
821 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
821 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
822
822
823 ``nodes``
823 ``nodes``
824 combined list of ``Node`` objects
824 combined list of ``Node`` objects
825
825
826 ``author``
826 ``author``
827 author of the commit, as unicode
827 author of the commit, as unicode
828
828
829 ``message``
829 ``message``
830 message of the commit, as unicode
830 message of the commit, as unicode
831
831
832 ``parents``
832 ``parents``
833 list of parent commits
833 list of parent commits
834
834
835 """
835 """
836
836
837 branch = None
837 branch = None
838 """
838 """
839 Depending on the backend this should be set to the branch name of the
839 Depending on the backend this should be set to the branch name of the
840 commit. Backends not supporting branches on commits should leave this
840 commit. Backends not supporting branches on commits should leave this
841 value as ``None``.
841 value as ``None``.
842 """
842 """
843
843
844 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
844 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
845 """
845 """
846 This template is used to generate a default prefix for repository archives
846 This template is used to generate a default prefix for repository archives
847 if no prefix has been specified.
847 if no prefix has been specified.
848 """
848 """
849
849
850 def __str__(self):
850 def __str__(self):
851 return '<%s at %s:%s>' % (
851 return '<%s at %s:%s>' % (
852 self.__class__.__name__, self.idx, self.short_id)
852 self.__class__.__name__, self.idx, self.short_id)
853
853
854 def __repr__(self):
854 def __repr__(self):
855 return self.__str__()
855 return self.__str__()
856
856
857 def __unicode__(self):
857 def __unicode__(self):
858 return u'%s:%s' % (self.idx, self.short_id)
858 return u'%s:%s' % (self.idx, self.short_id)
859
859
860 def __eq__(self, other):
860 def __eq__(self, other):
861 same_instance = isinstance(other, self.__class__)
861 same_instance = isinstance(other, self.__class__)
862 return same_instance and self.raw_id == other.raw_id
862 return same_instance and self.raw_id == other.raw_id
863
863
864 def __json__(self):
864 def __json__(self):
865 parents = []
865 parents = []
866 try:
866 try:
867 for parent in self.parents:
867 for parent in self.parents:
868 parents.append({'raw_id': parent.raw_id})
868 parents.append({'raw_id': parent.raw_id})
869 except NotImplementedError:
869 except NotImplementedError:
870 # empty commit doesn't have parents implemented
870 # empty commit doesn't have parents implemented
871 pass
871 pass
872
872
873 return {
873 return {
874 'short_id': self.short_id,
874 'short_id': self.short_id,
875 'raw_id': self.raw_id,
875 'raw_id': self.raw_id,
876 'revision': self.idx,
876 'revision': self.idx,
877 'message': self.message,
877 'message': self.message,
878 'date': self.date,
878 'date': self.date,
879 'author': self.author,
879 'author': self.author,
880 'parents': parents,
880 'parents': parents,
881 'branch': self.branch
881 'branch': self.branch
882 }
882 }
883
883
884 def __getstate__(self):
884 def __getstate__(self):
885 d = self.__dict__.copy()
885 d = self.__dict__.copy()
886 d.pop('_remote', None)
886 d.pop('_remote', None)
887 d.pop('repository', None)
887 d.pop('repository', None)
888 return d
888 return d
889
889
890 def _get_refs(self):
890 def _get_refs(self):
891 return {
891 return {
892 'branches': [self.branch] if self.branch else [],
892 'branches': [self.branch] if self.branch else [],
893 'bookmarks': getattr(self, 'bookmarks', []),
893 'bookmarks': getattr(self, 'bookmarks', []),
894 'tags': self.tags
894 'tags': self.tags
895 }
895 }
896
896
897 @LazyProperty
897 @LazyProperty
898 def last(self):
898 def last(self):
899 """
899 """
900 ``True`` if this is last commit in repository, ``False``
900 ``True`` if this is last commit in repository, ``False``
901 otherwise; trying to access this attribute while there is no
901 otherwise; trying to access this attribute while there is no
902 commits would raise `EmptyRepositoryError`
902 commits would raise `EmptyRepositoryError`
903 """
903 """
904 if self.repository is None:
904 if self.repository is None:
905 raise CommitError("Cannot check if it's most recent commit")
905 raise CommitError("Cannot check if it's most recent commit")
906 return self.raw_id == self.repository.commit_ids[-1]
906 return self.raw_id == self.repository.commit_ids[-1]
907
907
908 @LazyProperty
908 @LazyProperty
909 def parents(self):
909 def parents(self):
910 """
910 """
911 Returns list of parent commits.
911 Returns list of parent commits.
912 """
912 """
913 raise NotImplementedError
913 raise NotImplementedError
914
914
915 @LazyProperty
915 @LazyProperty
916 def first_parent(self):
916 def first_parent(self):
917 """
917 """
918 Returns list of parent commits.
918 Returns list of parent commits.
919 """
919 """
920 return self.parents[0] if self.parents else EmptyCommit()
920 return self.parents[0] if self.parents else EmptyCommit()
921
921
922 @property
922 @property
923 def merge(self):
923 def merge(self):
924 """
924 """
925 Returns boolean if commit is a merge.
925 Returns boolean if commit is a merge.
926 """
926 """
927 return len(self.parents) > 1
927 return len(self.parents) > 1
928
928
929 @LazyProperty
929 @LazyProperty
930 def children(self):
930 def children(self):
931 """
931 """
932 Returns list of child commits.
932 Returns list of child commits.
933 """
933 """
934 raise NotImplementedError
934 raise NotImplementedError
935
935
936 @LazyProperty
936 @LazyProperty
937 def id(self):
937 def id(self):
938 """
938 """
939 Returns string identifying this commit.
939 Returns string identifying this commit.
940 """
940 """
941 raise NotImplementedError
941 raise NotImplementedError
942
942
943 @LazyProperty
943 @LazyProperty
944 def raw_id(self):
944 def raw_id(self):
945 """
945 """
946 Returns raw string identifying this commit.
946 Returns raw string identifying this commit.
947 """
947 """
948 raise NotImplementedError
948 raise NotImplementedError
949
949
950 @LazyProperty
950 @LazyProperty
951 def short_id(self):
951 def short_id(self):
952 """
952 """
953 Returns shortened version of ``raw_id`` attribute, as string,
953 Returns shortened version of ``raw_id`` attribute, as string,
954 identifying this commit, useful for presentation to users.
954 identifying this commit, useful for presentation to users.
955 """
955 """
956 raise NotImplementedError
956 raise NotImplementedError
957
957
958 @LazyProperty
958 @LazyProperty
959 def idx(self):
959 def idx(self):
960 """
960 """
961 Returns integer identifying this commit.
961 Returns integer identifying this commit.
962 """
962 """
963 raise NotImplementedError
963 raise NotImplementedError
964
964
965 @LazyProperty
965 @LazyProperty
966 def committer(self):
966 def committer(self):
967 """
967 """
968 Returns committer for this commit
968 Returns committer for this commit
969 """
969 """
970 raise NotImplementedError
970 raise NotImplementedError
971
971
972 @LazyProperty
972 @LazyProperty
973 def committer_name(self):
973 def committer_name(self):
974 """
974 """
975 Returns committer name for this commit
975 Returns committer name for this commit
976 """
976 """
977
977
978 return author_name(self.committer)
978 return author_name(self.committer)
979
979
980 @LazyProperty
980 @LazyProperty
981 def committer_email(self):
981 def committer_email(self):
982 """
982 """
983 Returns committer email address for this commit
983 Returns committer email address for this commit
984 """
984 """
985
985
986 return author_email(self.committer)
986 return author_email(self.committer)
987
987
988 @LazyProperty
988 @LazyProperty
989 def author(self):
989 def author(self):
990 """
990 """
991 Returns author for this commit
991 Returns author for this commit
992 """
992 """
993
993
994 raise NotImplementedError
994 raise NotImplementedError
995
995
996 @LazyProperty
996 @LazyProperty
997 def author_name(self):
997 def author_name(self):
998 """
998 """
999 Returns author name for this commit
999 Returns author name for this commit
1000 """
1000 """
1001
1001
1002 return author_name(self.author)
1002 return author_name(self.author)
1003
1003
1004 @LazyProperty
1004 @LazyProperty
1005 def author_email(self):
1005 def author_email(self):
1006 """
1006 """
1007 Returns author email address for this commit
1007 Returns author email address for this commit
1008 """
1008 """
1009
1009
1010 return author_email(self.author)
1010 return author_email(self.author)
1011
1011
1012 def get_file_mode(self, path):
1012 def get_file_mode(self, path):
1013 """
1013 """
1014 Returns stat mode of the file at `path`.
1014 Returns stat mode of the file at `path`.
1015 """
1015 """
1016 raise NotImplementedError
1016 raise NotImplementedError
1017
1017
1018 def is_link(self, path):
1018 def is_link(self, path):
1019 """
1019 """
1020 Returns ``True`` if given `path` is a symlink
1020 Returns ``True`` if given `path` is a symlink
1021 """
1021 """
1022 raise NotImplementedError
1022 raise NotImplementedError
1023
1023
1024 def get_file_content(self, path):
1024 def get_file_content(self, path):
1025 """
1025 """
1026 Returns content of the file at the given `path`.
1026 Returns content of the file at the given `path`.
1027 """
1027 """
1028 raise NotImplementedError
1028 raise NotImplementedError
1029
1029
1030 def get_file_size(self, path):
1030 def get_file_size(self, path):
1031 """
1031 """
1032 Returns size of the file at the given `path`.
1032 Returns size of the file at the given `path`.
1033 """
1033 """
1034 raise NotImplementedError
1034 raise NotImplementedError
1035
1035
1036 def get_path_commit(self, path, pre_load=None):
1036 def get_path_commit(self, path, pre_load=None):
1037 """
1037 """
1038 Returns last commit of the file at the given `path`.
1038 Returns last commit of the file at the given `path`.
1039
1039
1040 :param pre_load: Optional. List of commit attributes to load.
1040 :param pre_load: Optional. List of commit attributes to load.
1041 """
1041 """
1042 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1042 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1043 if not commits:
1043 if not commits:
1044 raise RepositoryError(
1044 raise RepositoryError(
1045 'Failed to fetch history for path {}. '
1045 'Failed to fetch history for path {}. '
1046 'Please check if such path exists in your repository'.format(
1046 'Please check if such path exists in your repository'.format(
1047 path))
1047 path))
1048 return commits[0]
1048 return commits[0]
1049
1049
1050 def get_path_history(self, path, limit=None, pre_load=None):
1050 def get_path_history(self, path, limit=None, pre_load=None):
1051 """
1051 """
1052 Returns history of file as reversed list of :class:`BaseCommit`
1052 Returns history of file as reversed list of :class:`BaseCommit`
1053 objects for which file at given `path` has been modified.
1053 objects for which file at given `path` has been modified.
1054
1054
1055 :param limit: Optional. Allows to limit the size of the returned
1055 :param limit: Optional. Allows to limit the size of the returned
1056 history. This is intended as a hint to the underlying backend, so
1056 history. This is intended as a hint to the underlying backend, so
1057 that it can apply optimizations depending on the limit.
1057 that it can apply optimizations depending on the limit.
1058 :param pre_load: Optional. List of commit attributes to load.
1058 :param pre_load: Optional. List of commit attributes to load.
1059 """
1059 """
1060 raise NotImplementedError
1060 raise NotImplementedError
1061
1061
1062 def get_file_annotate(self, path, pre_load=None):
1062 def get_file_annotate(self, path, pre_load=None):
1063 """
1063 """
1064 Returns a generator of four element tuples with
1064 Returns a generator of four element tuples with
1065 lineno, sha, commit lazy loader and line
1065 lineno, sha, commit lazy loader and line
1066
1066
1067 :param pre_load: Optional. List of commit attributes to load.
1067 :param pre_load: Optional. List of commit attributes to load.
1068 """
1068 """
1069 raise NotImplementedError
1069 raise NotImplementedError
1070
1070
1071 def get_nodes(self, path):
1071 def get_nodes(self, path):
1072 """
1072 """
1073 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1073 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1074 state of commit at the given ``path``.
1074 state of commit at the given ``path``.
1075
1075
1076 :raises ``CommitError``: if node at the given ``path`` is not
1076 :raises ``CommitError``: if node at the given ``path`` is not
1077 instance of ``DirNode``
1077 instance of ``DirNode``
1078 """
1078 """
1079 raise NotImplementedError
1079 raise NotImplementedError
1080
1080
1081 def get_node(self, path):
1081 def get_node(self, path):
1082 """
1082 """
1083 Returns ``Node`` object from the given ``path``.
1083 Returns ``Node`` object from the given ``path``.
1084
1084
1085 :raises ``NodeDoesNotExistError``: if there is no node at the given
1085 :raises ``NodeDoesNotExistError``: if there is no node at the given
1086 ``path``
1086 ``path``
1087 """
1087 """
1088 raise NotImplementedError
1088 raise NotImplementedError
1089
1089
1090 def get_largefile_node(self, path):
1090 def get_largefile_node(self, path):
1091 """
1091 """
1092 Returns the path to largefile from Mercurial/Git-lfs storage.
1092 Returns the path to largefile from Mercurial/Git-lfs storage.
1093 or None if it's not a largefile node
1093 or None if it's not a largefile node
1094 """
1094 """
1095 return None
1095 return None
1096
1096
1097 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1097 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1098 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1098 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1099 """
1099 """
1100 Creates an archive containing the contents of the repository.
1100 Creates an archive containing the contents of the repository.
1101
1101
1102 :param archive_dest_path: path to the file which to create the archive.
1102 :param archive_dest_path: path to the file which to create the archive.
1103 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1103 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1104 :param prefix: name of root directory in archive.
1104 :param prefix: name of root directory in archive.
1105 Default is repository name and commit's short_id joined with dash:
1105 Default is repository name and commit's short_id joined with dash:
1106 ``"{repo_name}-{short_id}"``.
1106 ``"{repo_name}-{short_id}"``.
1107 :param write_metadata: write a metadata file into archive.
1107 :param write_metadata: write a metadata file into archive.
1108 :param mtime: custom modification time for archive creation, defaults
1108 :param mtime: custom modification time for archive creation, defaults
1109 to time.time() if not given.
1109 to time.time() if not given.
1110 :param archive_at_path: pack files at this path (default '/')
1110 :param archive_at_path: pack files at this path (default '/')
1111
1111
1112 :raise VCSError: If prefix has a problem.
1112 :raise VCSError: If prefix has a problem.
1113 """
1113 """
1114 allowed_kinds = settings.ARCHIVE_SPECS.keys()
1114 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1115 if kind not in allowed_kinds:
1115 if kind not in allowed_kinds:
1116 raise ImproperArchiveTypeError(
1116 raise ImproperArchiveTypeError(
1117 'Archive kind (%s) not supported use one of %s' %
1117 'Archive kind (%s) not supported use one of %s' %
1118 (kind, allowed_kinds))
1118 (kind, allowed_kinds))
1119
1119
1120 prefix = self._validate_archive_prefix(prefix)
1120 prefix = self._validate_archive_prefix(prefix)
1121
1121
1122 mtime = mtime is not None or time.mktime(self.date.timetuple())
1122 mtime = mtime is not None or time.mktime(self.date.timetuple())
1123
1123
1124 file_info = []
1124 file_info = []
1125 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1125 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1126 for _r, _d, files in cur_rev.walk(archive_at_path):
1126 for _r, _d, files in cur_rev.walk(archive_at_path):
1127 for f in files:
1127 for f in files:
1128 f_path = os.path.join(prefix, f.path)
1128 f_path = os.path.join(prefix, f.path)
1129 file_info.append(
1129 file_info.append(
1130 (f_path, f.mode, f.is_link(), f.raw_bytes))
1130 (f_path, f.mode, f.is_link(), f.raw_bytes))
1131
1131
1132 if write_metadata:
1132 if write_metadata:
1133 metadata = [
1133 metadata = [
1134 ('repo_name', self.repository.name),
1134 ('repo_name', self.repository.name),
1135 ('commit_id', self.raw_id),
1135 ('commit_id', self.raw_id),
1136 ('mtime', mtime),
1136 ('mtime', mtime),
1137 ('branch', self.branch),
1137 ('branch', self.branch),
1138 ('tags', ','.join(self.tags)),
1138 ('tags', ','.join(self.tags)),
1139 ]
1139 ]
1140 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1140 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1141 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1141 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1142
1142
1143 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1143 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1144
1144
1145 def _validate_archive_prefix(self, prefix):
1145 def _validate_archive_prefix(self, prefix):
1146 if prefix is None:
1146 if prefix is None:
1147 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1147 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1148 repo_name=safe_str(self.repository.name),
1148 repo_name=safe_str(self.repository.name),
1149 short_id=self.short_id)
1149 short_id=self.short_id)
1150 elif not isinstance(prefix, str):
1150 elif not isinstance(prefix, str):
1151 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1151 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1152 elif prefix.startswith('/'):
1152 elif prefix.startswith('/'):
1153 raise VCSError("Prefix cannot start with leading slash")
1153 raise VCSError("Prefix cannot start with leading slash")
1154 elif prefix.strip() == '':
1154 elif prefix.strip() == '':
1155 raise VCSError("Prefix cannot be empty")
1155 raise VCSError("Prefix cannot be empty")
1156 return prefix
1156 return prefix
1157
1157
1158 @LazyProperty
1158 @LazyProperty
1159 def root(self):
1159 def root(self):
1160 """
1160 """
1161 Returns ``RootNode`` object for this commit.
1161 Returns ``RootNode`` object for this commit.
1162 """
1162 """
1163 return self.get_node('')
1163 return self.get_node('')
1164
1164
1165 def next(self, branch=None):
1165 def next(self, branch=None):
1166 """
1166 """
1167 Returns next commit from current, if branch is gives it will return
1167 Returns next commit from current, if branch is gives it will return
1168 next commit belonging to this branch
1168 next commit belonging to this branch
1169
1169
1170 :param branch: show commits within the given named branch
1170 :param branch: show commits within the given named branch
1171 """
1171 """
1172 indexes = xrange(self.idx + 1, self.repository.count())
1172 indexes = xrange(self.idx + 1, self.repository.count())
1173 return self._find_next(indexes, branch)
1173 return self._find_next(indexes, branch)
1174
1174
1175 def prev(self, branch=None):
1175 def prev(self, branch=None):
1176 """
1176 """
1177 Returns previous commit from current, if branch is gives it will
1177 Returns previous commit from current, if branch is gives it will
1178 return previous commit belonging to this branch
1178 return previous commit belonging to this branch
1179
1179
1180 :param branch: show commit within the given named branch
1180 :param branch: show commit within the given named branch
1181 """
1181 """
1182 indexes = xrange(self.idx - 1, -1, -1)
1182 indexes = xrange(self.idx - 1, -1, -1)
1183 return self._find_next(indexes, branch)
1183 return self._find_next(indexes, branch)
1184
1184
1185 def _find_next(self, indexes, branch=None):
1185 def _find_next(self, indexes, branch=None):
1186 if branch and self.branch != branch:
1186 if branch and self.branch != branch:
1187 raise VCSError('Branch option used on commit not belonging '
1187 raise VCSError('Branch option used on commit not belonging '
1188 'to that branch')
1188 'to that branch')
1189
1189
1190 for next_idx in indexes:
1190 for next_idx in indexes:
1191 commit = self.repository.get_commit(commit_idx=next_idx)
1191 commit = self.repository.get_commit(commit_idx=next_idx)
1192 if branch and branch != commit.branch:
1192 if branch and branch != commit.branch:
1193 continue
1193 continue
1194 return commit
1194 return commit
1195 raise CommitDoesNotExistError
1195 raise CommitDoesNotExistError
1196
1196
1197 def diff(self, ignore_whitespace=True, context=3):
1197 def diff(self, ignore_whitespace=True, context=3):
1198 """
1198 """
1199 Returns a `Diff` object representing the change made by this commit.
1199 Returns a `Diff` object representing the change made by this commit.
1200 """
1200 """
1201 parent = self.first_parent
1201 parent = self.first_parent
1202 diff = self.repository.get_diff(
1202 diff = self.repository.get_diff(
1203 parent, self,
1203 parent, self,
1204 ignore_whitespace=ignore_whitespace,
1204 ignore_whitespace=ignore_whitespace,
1205 context=context)
1205 context=context)
1206 return diff
1206 return diff
1207
1207
1208 @LazyProperty
1208 @LazyProperty
1209 def added(self):
1209 def added(self):
1210 """
1210 """
1211 Returns list of added ``FileNode`` objects.
1211 Returns list of added ``FileNode`` objects.
1212 """
1212 """
1213 raise NotImplementedError
1213 raise NotImplementedError
1214
1214
1215 @LazyProperty
1215 @LazyProperty
1216 def changed(self):
1216 def changed(self):
1217 """
1217 """
1218 Returns list of modified ``FileNode`` objects.
1218 Returns list of modified ``FileNode`` objects.
1219 """
1219 """
1220 raise NotImplementedError
1220 raise NotImplementedError
1221
1221
1222 @LazyProperty
1222 @LazyProperty
1223 def removed(self):
1223 def removed(self):
1224 """
1224 """
1225 Returns list of removed ``FileNode`` objects.
1225 Returns list of removed ``FileNode`` objects.
1226 """
1226 """
1227 raise NotImplementedError
1227 raise NotImplementedError
1228
1228
1229 @LazyProperty
1229 @LazyProperty
1230 def size(self):
1230 def size(self):
1231 """
1231 """
1232 Returns total number of bytes from contents of all filenodes.
1232 Returns total number of bytes from contents of all filenodes.
1233 """
1233 """
1234 return sum((node.size for node in self.get_filenodes_generator()))
1234 return sum((node.size for node in self.get_filenodes_generator()))
1235
1235
1236 def walk(self, topurl=''):
1236 def walk(self, topurl=''):
1237 """
1237 """
1238 Similar to os.walk method. Insted of filesystem it walks through
1238 Similar to os.walk method. Insted of filesystem it walks through
1239 commit starting at given ``topurl``. Returns generator of tuples
1239 commit starting at given ``topurl``. Returns generator of tuples
1240 (topnode, dirnodes, filenodes).
1240 (topnode, dirnodes, filenodes).
1241 """
1241 """
1242 topnode = self.get_node(topurl)
1242 topnode = self.get_node(topurl)
1243 if not topnode.is_dir():
1243 if not topnode.is_dir():
1244 return
1244 return
1245 yield (topnode, topnode.dirs, topnode.files)
1245 yield (topnode, topnode.dirs, topnode.files)
1246 for dirnode in topnode.dirs:
1246 for dirnode in topnode.dirs:
1247 for tup in self.walk(dirnode.path):
1247 for tup in self.walk(dirnode.path):
1248 yield tup
1248 yield tup
1249
1249
1250 def get_filenodes_generator(self):
1250 def get_filenodes_generator(self):
1251 """
1251 """
1252 Returns generator that yields *all* file nodes.
1252 Returns generator that yields *all* file nodes.
1253 """
1253 """
1254 for topnode, dirs, files in self.walk():
1254 for topnode, dirs, files in self.walk():
1255 for node in files:
1255 for node in files:
1256 yield node
1256 yield node
1257
1257
1258 #
1258 #
1259 # Utilities for sub classes to support consistent behavior
1259 # Utilities for sub classes to support consistent behavior
1260 #
1260 #
1261
1261
1262 def no_node_at_path(self, path):
1262 def no_node_at_path(self, path):
1263 return NodeDoesNotExistError(
1263 return NodeDoesNotExistError(
1264 u"There is no file nor directory at the given path: "
1264 u"There is no file nor directory at the given path: "
1265 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1265 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1266
1266
1267 def _fix_path(self, path):
1267 def _fix_path(self, path):
1268 """
1268 """
1269 Paths are stored without trailing slash so we need to get rid off it if
1269 Paths are stored without trailing slash so we need to get rid off it if
1270 needed.
1270 needed.
1271 """
1271 """
1272 return path.rstrip('/')
1272 return path.rstrip('/')
1273
1273
1274 #
1274 #
1275 # Deprecated API based on changesets
1275 # Deprecated API based on changesets
1276 #
1276 #
1277
1277
1278 @property
1278 @property
1279 def revision(self):
1279 def revision(self):
1280 warnings.warn("Use idx instead", DeprecationWarning)
1280 warnings.warn("Use idx instead", DeprecationWarning)
1281 return self.idx
1281 return self.idx
1282
1282
1283 @revision.setter
1283 @revision.setter
1284 def revision(self, value):
1284 def revision(self, value):
1285 warnings.warn("Use idx instead", DeprecationWarning)
1285 warnings.warn("Use idx instead", DeprecationWarning)
1286 self.idx = value
1286 self.idx = value
1287
1287
1288 def get_file_changeset(self, path):
1288 def get_file_changeset(self, path):
1289 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1289 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1290 return self.get_path_commit(path)
1290 return self.get_path_commit(path)
1291
1291
1292
1292
1293 class BaseChangesetClass(type):
1293 class BaseChangesetClass(type):
1294
1294
1295 def __instancecheck__(self, instance):
1295 def __instancecheck__(self, instance):
1296 return isinstance(instance, BaseCommit)
1296 return isinstance(instance, BaseCommit)
1297
1297
1298
1298
1299 class BaseChangeset(BaseCommit):
1299 class BaseChangeset(BaseCommit):
1300
1300
1301 __metaclass__ = BaseChangesetClass
1301 __metaclass__ = BaseChangesetClass
1302
1302
1303 def __new__(cls, *args, **kwargs):
1303 def __new__(cls, *args, **kwargs):
1304 warnings.warn(
1304 warnings.warn(
1305 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1305 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1306 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1306 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1307
1307
1308
1308
1309 class BaseInMemoryCommit(object):
1309 class BaseInMemoryCommit(object):
1310 """
1310 """
1311 Represents differences between repository's state (most recent head) and
1311 Represents differences between repository's state (most recent head) and
1312 changes made *in place*.
1312 changes made *in place*.
1313
1313
1314 **Attributes**
1314 **Attributes**
1315
1315
1316 ``repository``
1316 ``repository``
1317 repository object for this in-memory-commit
1317 repository object for this in-memory-commit
1318
1318
1319 ``added``
1319 ``added``
1320 list of ``FileNode`` objects marked as *added*
1320 list of ``FileNode`` objects marked as *added*
1321
1321
1322 ``changed``
1322 ``changed``
1323 list of ``FileNode`` objects marked as *changed*
1323 list of ``FileNode`` objects marked as *changed*
1324
1324
1325 ``removed``
1325 ``removed``
1326 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1326 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1327 *removed*
1327 *removed*
1328
1328
1329 ``parents``
1329 ``parents``
1330 list of :class:`BaseCommit` instances representing parents of
1330 list of :class:`BaseCommit` instances representing parents of
1331 in-memory commit. Should always be 2-element sequence.
1331 in-memory commit. Should always be 2-element sequence.
1332
1332
1333 """
1333 """
1334
1334
1335 def __init__(self, repository):
1335 def __init__(self, repository):
1336 self.repository = repository
1336 self.repository = repository
1337 self.added = []
1337 self.added = []
1338 self.changed = []
1338 self.changed = []
1339 self.removed = []
1339 self.removed = []
1340 self.parents = []
1340 self.parents = []
1341
1341
1342 def add(self, *filenodes):
1342 def add(self, *filenodes):
1343 """
1343 """
1344 Marks given ``FileNode`` objects as *to be committed*.
1344 Marks given ``FileNode`` objects as *to be committed*.
1345
1345
1346 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1346 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1347 latest commit
1347 latest commit
1348 :raises ``NodeAlreadyAddedError``: if node with same path is already
1348 :raises ``NodeAlreadyAddedError``: if node with same path is already
1349 marked as *added*
1349 marked as *added*
1350 """
1350 """
1351 # Check if not already marked as *added* first
1351 # Check if not already marked as *added* first
1352 for node in filenodes:
1352 for node in filenodes:
1353 if node.path in (n.path for n in self.added):
1353 if node.path in (n.path for n in self.added):
1354 raise NodeAlreadyAddedError(
1354 raise NodeAlreadyAddedError(
1355 "Such FileNode %s is already marked for addition"
1355 "Such FileNode %s is already marked for addition"
1356 % node.path)
1356 % node.path)
1357 for node in filenodes:
1357 for node in filenodes:
1358 self.added.append(node)
1358 self.added.append(node)
1359
1359
1360 def change(self, *filenodes):
1360 def change(self, *filenodes):
1361 """
1361 """
1362 Marks given ``FileNode`` objects to be *changed* in next commit.
1362 Marks given ``FileNode`` objects to be *changed* in next commit.
1363
1363
1364 :raises ``EmptyRepositoryError``: if there are no commits yet
1364 :raises ``EmptyRepositoryError``: if there are no commits yet
1365 :raises ``NodeAlreadyExistsError``: if node with same path is already
1365 :raises ``NodeAlreadyExistsError``: if node with same path is already
1366 marked to be *changed*
1366 marked to be *changed*
1367 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1367 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1368 marked to be *removed*
1368 marked to be *removed*
1369 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1369 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1370 commit
1370 commit
1371 :raises ``NodeNotChangedError``: if node hasn't really be changed
1371 :raises ``NodeNotChangedError``: if node hasn't really be changed
1372 """
1372 """
1373 for node in filenodes:
1373 for node in filenodes:
1374 if node.path in (n.path for n in self.removed):
1374 if node.path in (n.path for n in self.removed):
1375 raise NodeAlreadyRemovedError(
1375 raise NodeAlreadyRemovedError(
1376 "Node at %s is already marked as removed" % node.path)
1376 "Node at %s is already marked as removed" % node.path)
1377 try:
1377 try:
1378 self.repository.get_commit()
1378 self.repository.get_commit()
1379 except EmptyRepositoryError:
1379 except EmptyRepositoryError:
1380 raise EmptyRepositoryError(
1380 raise EmptyRepositoryError(
1381 "Nothing to change - try to *add* new nodes rather than "
1381 "Nothing to change - try to *add* new nodes rather than "
1382 "changing them")
1382 "changing them")
1383 for node in filenodes:
1383 for node in filenodes:
1384 if node.path in (n.path for n in self.changed):
1384 if node.path in (n.path for n in self.changed):
1385 raise NodeAlreadyChangedError(
1385 raise NodeAlreadyChangedError(
1386 "Node at '%s' is already marked as changed" % node.path)
1386 "Node at '%s' is already marked as changed" % node.path)
1387 self.changed.append(node)
1387 self.changed.append(node)
1388
1388
1389 def remove(self, *filenodes):
1389 def remove(self, *filenodes):
1390 """
1390 """
1391 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1391 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1392 *removed* in next commit.
1392 *removed* in next commit.
1393
1393
1394 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1394 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1395 be *removed*
1395 be *removed*
1396 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1396 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1397 be *changed*
1397 be *changed*
1398 """
1398 """
1399 for node in filenodes:
1399 for node in filenodes:
1400 if node.path in (n.path for n in self.removed):
1400 if node.path in (n.path for n in self.removed):
1401 raise NodeAlreadyRemovedError(
1401 raise NodeAlreadyRemovedError(
1402 "Node is already marked to for removal at %s" % node.path)
1402 "Node is already marked to for removal at %s" % node.path)
1403 if node.path in (n.path for n in self.changed):
1403 if node.path in (n.path for n in self.changed):
1404 raise NodeAlreadyChangedError(
1404 raise NodeAlreadyChangedError(
1405 "Node is already marked to be changed at %s" % node.path)
1405 "Node is already marked to be changed at %s" % node.path)
1406 # We only mark node as *removed* - real removal is done by
1406 # We only mark node as *removed* - real removal is done by
1407 # commit method
1407 # commit method
1408 self.removed.append(node)
1408 self.removed.append(node)
1409
1409
1410 def reset(self):
1410 def reset(self):
1411 """
1411 """
1412 Resets this instance to initial state (cleans ``added``, ``changed``
1412 Resets this instance to initial state (cleans ``added``, ``changed``
1413 and ``removed`` lists).
1413 and ``removed`` lists).
1414 """
1414 """
1415 self.added = []
1415 self.added = []
1416 self.changed = []
1416 self.changed = []
1417 self.removed = []
1417 self.removed = []
1418 self.parents = []
1418 self.parents = []
1419
1419
1420 def get_ipaths(self):
1420 def get_ipaths(self):
1421 """
1421 """
1422 Returns generator of paths from nodes marked as added, changed or
1422 Returns generator of paths from nodes marked as added, changed or
1423 removed.
1423 removed.
1424 """
1424 """
1425 for node in itertools.chain(self.added, self.changed, self.removed):
1425 for node in itertools.chain(self.added, self.changed, self.removed):
1426 yield node.path
1426 yield node.path
1427
1427
1428 def get_paths(self):
1428 def get_paths(self):
1429 """
1429 """
1430 Returns list of paths from nodes marked as added, changed or removed.
1430 Returns list of paths from nodes marked as added, changed or removed.
1431 """
1431 """
1432 return list(self.get_ipaths())
1432 return list(self.get_ipaths())
1433
1433
1434 def check_integrity(self, parents=None):
1434 def check_integrity(self, parents=None):
1435 """
1435 """
1436 Checks in-memory commit's integrity. Also, sets parents if not
1436 Checks in-memory commit's integrity. Also, sets parents if not
1437 already set.
1437 already set.
1438
1438
1439 :raises CommitError: if any error occurs (i.e.
1439 :raises CommitError: if any error occurs (i.e.
1440 ``NodeDoesNotExistError``).
1440 ``NodeDoesNotExistError``).
1441 """
1441 """
1442 if not self.parents:
1442 if not self.parents:
1443 parents = parents or []
1443 parents = parents or []
1444 if len(parents) == 0:
1444 if len(parents) == 0:
1445 try:
1445 try:
1446 parents = [self.repository.get_commit(), None]
1446 parents = [self.repository.get_commit(), None]
1447 except EmptyRepositoryError:
1447 except EmptyRepositoryError:
1448 parents = [None, None]
1448 parents = [None, None]
1449 elif len(parents) == 1:
1449 elif len(parents) == 1:
1450 parents += [None]
1450 parents += [None]
1451 self.parents = parents
1451 self.parents = parents
1452
1452
1453 # Local parents, only if not None
1453 # Local parents, only if not None
1454 parents = [p for p in self.parents if p]
1454 parents = [p for p in self.parents if p]
1455
1455
1456 # Check nodes marked as added
1456 # Check nodes marked as added
1457 for p in parents:
1457 for p in parents:
1458 for node in self.added:
1458 for node in self.added:
1459 try:
1459 try:
1460 p.get_node(node.path)
1460 p.get_node(node.path)
1461 except NodeDoesNotExistError:
1461 except NodeDoesNotExistError:
1462 pass
1462 pass
1463 else:
1463 else:
1464 raise NodeAlreadyExistsError(
1464 raise NodeAlreadyExistsError(
1465 "Node `%s` already exists at %s" % (node.path, p))
1465 "Node `%s` already exists at %s" % (node.path, p))
1466
1466
1467 # Check nodes marked as changed
1467 # Check nodes marked as changed
1468 missing = set(self.changed)
1468 missing = set(self.changed)
1469 not_changed = set(self.changed)
1469 not_changed = set(self.changed)
1470 if self.changed and not parents:
1470 if self.changed and not parents:
1471 raise NodeDoesNotExistError(str(self.changed[0].path))
1471 raise NodeDoesNotExistError(str(self.changed[0].path))
1472 for p in parents:
1472 for p in parents:
1473 for node in self.changed:
1473 for node in self.changed:
1474 try:
1474 try:
1475 old = p.get_node(node.path)
1475 old = p.get_node(node.path)
1476 missing.remove(node)
1476 missing.remove(node)
1477 # if content actually changed, remove node from not_changed
1477 # if content actually changed, remove node from not_changed
1478 if old.content != node.content:
1478 if old.content != node.content:
1479 not_changed.remove(node)
1479 not_changed.remove(node)
1480 except NodeDoesNotExistError:
1480 except NodeDoesNotExistError:
1481 pass
1481 pass
1482 if self.changed and missing:
1482 if self.changed and missing:
1483 raise NodeDoesNotExistError(
1483 raise NodeDoesNotExistError(
1484 "Node `%s` marked as modified but missing in parents: %s"
1484 "Node `%s` marked as modified but missing in parents: %s"
1485 % (node.path, parents))
1485 % (node.path, parents))
1486
1486
1487 if self.changed and not_changed:
1487 if self.changed and not_changed:
1488 raise NodeNotChangedError(
1488 raise NodeNotChangedError(
1489 "Node `%s` wasn't actually changed (parents: %s)"
1489 "Node `%s` wasn't actually changed (parents: %s)"
1490 % (not_changed.pop().path, parents))
1490 % (not_changed.pop().path, parents))
1491
1491
1492 # Check nodes marked as removed
1492 # Check nodes marked as removed
1493 if self.removed and not parents:
1493 if self.removed and not parents:
1494 raise NodeDoesNotExistError(
1494 raise NodeDoesNotExistError(
1495 "Cannot remove node at %s as there "
1495 "Cannot remove node at %s as there "
1496 "were no parents specified" % self.removed[0].path)
1496 "were no parents specified" % self.removed[0].path)
1497 really_removed = set()
1497 really_removed = set()
1498 for p in parents:
1498 for p in parents:
1499 for node in self.removed:
1499 for node in self.removed:
1500 try:
1500 try:
1501 p.get_node(node.path)
1501 p.get_node(node.path)
1502 really_removed.add(node)
1502 really_removed.add(node)
1503 except CommitError:
1503 except CommitError:
1504 pass
1504 pass
1505 not_removed = set(self.removed) - really_removed
1505 not_removed = set(self.removed) - really_removed
1506 if not_removed:
1506 if not_removed:
1507 # TODO: johbo: This code branch does not seem to be covered
1507 # TODO: johbo: This code branch does not seem to be covered
1508 raise NodeDoesNotExistError(
1508 raise NodeDoesNotExistError(
1509 "Cannot remove node at %s from "
1509 "Cannot remove node at %s from "
1510 "following parents: %s" % (not_removed, parents))
1510 "following parents: %s" % (not_removed, parents))
1511
1511
1512 def commit(
1512 def commit(
1513 self, message, author, parents=None, branch=None, date=None,
1513 self, message, author, parents=None, branch=None, date=None,
1514 **kwargs):
1514 **kwargs):
1515 """
1515 """
1516 Performs in-memory commit (doesn't check workdir in any way) and
1516 Performs in-memory commit (doesn't check workdir in any way) and
1517 returns newly created :class:`BaseCommit`. Updates repository's
1517 returns newly created :class:`BaseCommit`. Updates repository's
1518 attribute `commits`.
1518 attribute `commits`.
1519
1519
1520 .. note::
1520 .. note::
1521
1521
1522 While overriding this method each backend's should call
1522 While overriding this method each backend's should call
1523 ``self.check_integrity(parents)`` in the first place.
1523 ``self.check_integrity(parents)`` in the first place.
1524
1524
1525 :param message: message of the commit
1525 :param message: message of the commit
1526 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1526 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1527 :param parents: single parent or sequence of parents from which commit
1527 :param parents: single parent or sequence of parents from which commit
1528 would be derived
1528 would be derived
1529 :param date: ``datetime.datetime`` instance. Defaults to
1529 :param date: ``datetime.datetime`` instance. Defaults to
1530 ``datetime.datetime.now()``.
1530 ``datetime.datetime.now()``.
1531 :param branch: branch name, as string. If none given, default backend's
1531 :param branch: branch name, as string. If none given, default backend's
1532 branch would be used.
1532 branch would be used.
1533
1533
1534 :raises ``CommitError``: if any error occurs while committing
1534 :raises ``CommitError``: if any error occurs while committing
1535 """
1535 """
1536 raise NotImplementedError
1536 raise NotImplementedError
1537
1537
1538
1538
1539 class BaseInMemoryChangesetClass(type):
1539 class BaseInMemoryChangesetClass(type):
1540
1540
1541 def __instancecheck__(self, instance):
1541 def __instancecheck__(self, instance):
1542 return isinstance(instance, BaseInMemoryCommit)
1542 return isinstance(instance, BaseInMemoryCommit)
1543
1543
1544
1544
1545 class BaseInMemoryChangeset(BaseInMemoryCommit):
1545 class BaseInMemoryChangeset(BaseInMemoryCommit):
1546
1546
1547 __metaclass__ = BaseInMemoryChangesetClass
1547 __metaclass__ = BaseInMemoryChangesetClass
1548
1548
1549 def __new__(cls, *args, **kwargs):
1549 def __new__(cls, *args, **kwargs):
1550 warnings.warn(
1550 warnings.warn(
1551 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1551 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1552 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1552 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1553
1553
1554
1554
1555 class EmptyCommit(BaseCommit):
1555 class EmptyCommit(BaseCommit):
1556 """
1556 """
1557 An dummy empty commit. It's possible to pass hash when creating
1557 An dummy empty commit. It's possible to pass hash when creating
1558 an EmptyCommit
1558 an EmptyCommit
1559 """
1559 """
1560
1560
1561 def __init__(
1561 def __init__(
1562 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1562 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1563 message='', author='', date=None):
1563 message='', author='', date=None):
1564 self._empty_commit_id = commit_id
1564 self._empty_commit_id = commit_id
1565 # TODO: johbo: Solve idx parameter, default value does not make
1565 # TODO: johbo: Solve idx parameter, default value does not make
1566 # too much sense
1566 # too much sense
1567 self.idx = idx
1567 self.idx = idx
1568 self.message = message
1568 self.message = message
1569 self.author = author
1569 self.author = author
1570 self.date = date or datetime.datetime.fromtimestamp(0)
1570 self.date = date or datetime.datetime.fromtimestamp(0)
1571 self.repository = repo
1571 self.repository = repo
1572 self.alias = alias
1572 self.alias = alias
1573
1573
1574 @LazyProperty
1574 @LazyProperty
1575 def raw_id(self):
1575 def raw_id(self):
1576 """
1576 """
1577 Returns raw string identifying this commit, useful for web
1577 Returns raw string identifying this commit, useful for web
1578 representation.
1578 representation.
1579 """
1579 """
1580
1580
1581 return self._empty_commit_id
1581 return self._empty_commit_id
1582
1582
1583 @LazyProperty
1583 @LazyProperty
1584 def branch(self):
1584 def branch(self):
1585 if self.alias:
1585 if self.alias:
1586 from rhodecode.lib.vcs.backends import get_backend
1586 from rhodecode.lib.vcs.backends import get_backend
1587 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1587 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1588
1588
1589 @LazyProperty
1589 @LazyProperty
1590 def short_id(self):
1590 def short_id(self):
1591 return self.raw_id[:12]
1591 return self.raw_id[:12]
1592
1592
1593 @LazyProperty
1593 @LazyProperty
1594 def id(self):
1594 def id(self):
1595 return self.raw_id
1595 return self.raw_id
1596
1596
1597 def get_path_commit(self, path):
1597 def get_path_commit(self, path):
1598 return self
1598 return self
1599
1599
1600 def get_file_content(self, path):
1600 def get_file_content(self, path):
1601 return u''
1601 return u''
1602
1602
1603 def get_file_size(self, path):
1603 def get_file_size(self, path):
1604 return 0
1604 return 0
1605
1605
1606
1606
1607 class EmptyChangesetClass(type):
1607 class EmptyChangesetClass(type):
1608
1608
1609 def __instancecheck__(self, instance):
1609 def __instancecheck__(self, instance):
1610 return isinstance(instance, EmptyCommit)
1610 return isinstance(instance, EmptyCommit)
1611
1611
1612
1612
1613 class EmptyChangeset(EmptyCommit):
1613 class EmptyChangeset(EmptyCommit):
1614
1614
1615 __metaclass__ = EmptyChangesetClass
1615 __metaclass__ = EmptyChangesetClass
1616
1616
1617 def __new__(cls, *args, **kwargs):
1617 def __new__(cls, *args, **kwargs):
1618 warnings.warn(
1618 warnings.warn(
1619 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1619 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1620 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1620 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1621
1621
1622 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1622 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1623 alias=None, revision=-1, message='', author='', date=None):
1623 alias=None, revision=-1, message='', author='', date=None):
1624 if requested_revision is not None:
1624 if requested_revision is not None:
1625 warnings.warn(
1625 warnings.warn(
1626 "Parameter requested_revision not supported anymore",
1626 "Parameter requested_revision not supported anymore",
1627 DeprecationWarning)
1627 DeprecationWarning)
1628 super(EmptyChangeset, self).__init__(
1628 super(EmptyChangeset, self).__init__(
1629 commit_id=cs, repo=repo, alias=alias, idx=revision,
1629 commit_id=cs, repo=repo, alias=alias, idx=revision,
1630 message=message, author=author, date=date)
1630 message=message, author=author, date=date)
1631
1631
1632 @property
1632 @property
1633 def revision(self):
1633 def revision(self):
1634 warnings.warn("Use idx instead", DeprecationWarning)
1634 warnings.warn("Use idx instead", DeprecationWarning)
1635 return self.idx
1635 return self.idx
1636
1636
1637 @revision.setter
1637 @revision.setter
1638 def revision(self, value):
1638 def revision(self, value):
1639 warnings.warn("Use idx instead", DeprecationWarning)
1639 warnings.warn("Use idx instead", DeprecationWarning)
1640 self.idx = value
1640 self.idx = value
1641
1641
1642
1642
1643 class EmptyRepository(BaseRepository):
1643 class EmptyRepository(BaseRepository):
1644 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1644 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1645 pass
1645 pass
1646
1646
1647 def get_diff(self, *args, **kwargs):
1647 def get_diff(self, *args, **kwargs):
1648 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1648 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1649 return GitDiff('')
1649 return GitDiff('')
1650
1650
1651
1651
1652 class CollectionGenerator(object):
1652 class CollectionGenerator(object):
1653
1653
1654 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1654 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1655 self.repo = repo
1655 self.repo = repo
1656 self.commit_ids = commit_ids
1656 self.commit_ids = commit_ids
1657 # TODO: (oliver) this isn't currently hooked up
1657 # TODO: (oliver) this isn't currently hooked up
1658 self.collection_size = None
1658 self.collection_size = None
1659 self.pre_load = pre_load
1659 self.pre_load = pre_load
1660 self.translate_tag = translate_tag
1660 self.translate_tag = translate_tag
1661
1661
1662 def __len__(self):
1662 def __len__(self):
1663 if self.collection_size is not None:
1663 if self.collection_size is not None:
1664 return self.collection_size
1664 return self.collection_size
1665 return self.commit_ids.__len__()
1665 return self.commit_ids.__len__()
1666
1666
1667 def __iter__(self):
1667 def __iter__(self):
1668 for commit_id in self.commit_ids:
1668 for commit_id in self.commit_ids:
1669 # TODO: johbo: Mercurial passes in commit indices or commit ids
1669 # TODO: johbo: Mercurial passes in commit indices or commit ids
1670 yield self._commit_factory(commit_id)
1670 yield self._commit_factory(commit_id)
1671
1671
1672 def _commit_factory(self, commit_id):
1672 def _commit_factory(self, commit_id):
1673 """
1673 """
1674 Allows backends to override the way commits are generated.
1674 Allows backends to override the way commits are generated.
1675 """
1675 """
1676 return self.repo.get_commit(
1676 return self.repo.get_commit(
1677 commit_id=commit_id, pre_load=self.pre_load,
1677 commit_id=commit_id, pre_load=self.pre_load,
1678 translate_tag=self.translate_tag)
1678 translate_tag=self.translate_tag)
1679
1679
1680 def __getslice__(self, i, j):
1680 def __getslice__(self, i, j):
1681 """
1681 """
1682 Returns an iterator of sliced repository
1682 Returns an iterator of sliced repository
1683 """
1683 """
1684 commit_ids = self.commit_ids[i:j]
1684 commit_ids = self.commit_ids[i:j]
1685 return self.__class__(
1685 return self.__class__(
1686 self.repo, commit_ids, pre_load=self.pre_load,
1686 self.repo, commit_ids, pre_load=self.pre_load,
1687 translate_tag=self.translate_tag)
1687 translate_tag=self.translate_tag)
1688
1688
1689 def __repr__(self):
1689 def __repr__(self):
1690 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1690 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1691
1691
1692
1692
1693 class Config(object):
1693 class Config(object):
1694 """
1694 """
1695 Represents the configuration for a repository.
1695 Represents the configuration for a repository.
1696
1696
1697 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1697 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1698 standard library. It implements only the needed subset.
1698 standard library. It implements only the needed subset.
1699 """
1699 """
1700
1700
1701 def __init__(self):
1701 def __init__(self):
1702 self._values = {}
1702 self._values = {}
1703
1703
1704 def copy(self):
1704 def copy(self):
1705 clone = Config()
1705 clone = Config()
1706 for section, values in self._values.items():
1706 for section, values in self._values.items():
1707 clone._values[section] = values.copy()
1707 clone._values[section] = values.copy()
1708 return clone
1708 return clone
1709
1709
1710 def __repr__(self):
1710 def __repr__(self):
1711 return '<Config(%s sections) at %s>' % (
1711 return '<Config(%s sections) at %s>' % (
1712 len(self._values), hex(id(self)))
1712 len(self._values), hex(id(self)))
1713
1713
1714 def items(self, section):
1714 def items(self, section):
1715 return self._values.get(section, {}).iteritems()
1715 return self._values.get(section, {}).iteritems()
1716
1716
1717 def get(self, section, option):
1717 def get(self, section, option):
1718 return self._values.get(section, {}).get(option)
1718 return self._values.get(section, {}).get(option)
1719
1719
1720 def set(self, section, option, value):
1720 def set(self, section, option, value):
1721 section_values = self._values.setdefault(section, {})
1721 section_values = self._values.setdefault(section, {})
1722 section_values[option] = value
1722 section_values[option] = value
1723
1723
1724 def clear_section(self, section):
1724 def clear_section(self, section):
1725 self._values[section] = {}
1725 self._values[section] = {}
1726
1726
1727 def serialize(self):
1727 def serialize(self):
1728 """
1728 """
1729 Creates a list of three tuples (section, key, value) representing
1729 Creates a list of three tuples (section, key, value) representing
1730 this config object.
1730 this config object.
1731 """
1731 """
1732 items = []
1732 items = []
1733 for section in self._values:
1733 for section in self._values:
1734 for option, value in self._values[section].items():
1734 for option, value in self._values[section].items():
1735 items.append(
1735 items.append(
1736 (safe_str(section), safe_str(option), safe_str(value)))
1736 (safe_str(section), safe_str(option), safe_str(value)))
1737 return items
1737 return items
1738
1738
1739
1739
1740 class Diff(object):
1740 class Diff(object):
1741 """
1741 """
1742 Represents a diff result from a repository backend.
1742 Represents a diff result from a repository backend.
1743
1743
1744 Subclasses have to provide a backend specific value for
1744 Subclasses have to provide a backend specific value for
1745 :attr:`_header_re` and :attr:`_meta_re`.
1745 :attr:`_header_re` and :attr:`_meta_re`.
1746 """
1746 """
1747 _meta_re = None
1747 _meta_re = None
1748 _header_re = None
1748 _header_re = None
1749
1749
1750 def __init__(self, raw_diff):
1750 def __init__(self, raw_diff):
1751 self.raw = raw_diff
1751 self.raw = raw_diff
1752
1752
1753 def chunks(self):
1753 def chunks(self):
1754 """
1754 """
1755 split the diff in chunks of separate --git a/file b/file chunks
1755 split the diff in chunks of separate --git a/file b/file chunks
1756 to make diffs consistent we must prepend with \n, and make sure
1756 to make diffs consistent we must prepend with \n, and make sure
1757 we can detect last chunk as this was also has special rule
1757 we can detect last chunk as this was also has special rule
1758 """
1758 """
1759
1759
1760 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1760 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1761 header = diff_parts[0]
1761 header = diff_parts[0]
1762
1762
1763 if self._meta_re:
1763 if self._meta_re:
1764 match = self._meta_re.match(header)
1764 match = self._meta_re.match(header)
1765
1765
1766 chunks = diff_parts[1:]
1766 chunks = diff_parts[1:]
1767 total_chunks = len(chunks)
1767 total_chunks = len(chunks)
1768
1768
1769 return (
1769 return (
1770 DiffChunk(chunk, self, cur_chunk == total_chunks)
1770 DiffChunk(chunk, self, cur_chunk == total_chunks)
1771 for cur_chunk, chunk in enumerate(chunks, start=1))
1771 for cur_chunk, chunk in enumerate(chunks, start=1))
1772
1772
1773
1773
1774 class DiffChunk(object):
1774 class DiffChunk(object):
1775
1775
1776 def __init__(self, chunk, diff, last_chunk):
1776 def __init__(self, chunk, diff, last_chunk):
1777 self._diff = diff
1777 self._diff = diff
1778
1778
1779 # since we split by \ndiff --git that part is lost from original diff
1779 # since we split by \ndiff --git that part is lost from original diff
1780 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1780 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1781 if not last_chunk:
1781 if not last_chunk:
1782 chunk += '\n'
1782 chunk += '\n'
1783
1783
1784 match = self._diff._header_re.match(chunk)
1784 match = self._diff._header_re.match(chunk)
1785 self.header = match.groupdict()
1785 self.header = match.groupdict()
1786 self.diff = chunk[match.end():]
1786 self.diff = chunk[match.end():]
1787 self.raw = chunk
1787 self.raw = chunk
1788
1788
1789
1789
1790 class BasePathPermissionChecker(object):
1790 class BasePathPermissionChecker(object):
1791
1791
1792 @staticmethod
1792 @staticmethod
1793 def create_from_patterns(includes, excludes):
1793 def create_from_patterns(includes, excludes):
1794 if includes and '*' in includes and not excludes:
1794 if includes and '*' in includes and not excludes:
1795 return AllPathPermissionChecker()
1795 return AllPathPermissionChecker()
1796 elif excludes and '*' in excludes:
1796 elif excludes and '*' in excludes:
1797 return NonePathPermissionChecker()
1797 return NonePathPermissionChecker()
1798 else:
1798 else:
1799 return PatternPathPermissionChecker(includes, excludes)
1799 return PatternPathPermissionChecker(includes, excludes)
1800
1800
1801 @property
1801 @property
1802 def has_full_access(self):
1802 def has_full_access(self):
1803 raise NotImplemented()
1803 raise NotImplemented()
1804
1804
1805 def has_access(self, path):
1805 def has_access(self, path):
1806 raise NotImplemented()
1806 raise NotImplemented()
1807
1807
1808
1808
1809 class AllPathPermissionChecker(BasePathPermissionChecker):
1809 class AllPathPermissionChecker(BasePathPermissionChecker):
1810
1810
1811 @property
1811 @property
1812 def has_full_access(self):
1812 def has_full_access(self):
1813 return True
1813 return True
1814
1814
1815 def has_access(self, path):
1815 def has_access(self, path):
1816 return True
1816 return True
1817
1817
1818
1818
1819 class NonePathPermissionChecker(BasePathPermissionChecker):
1819 class NonePathPermissionChecker(BasePathPermissionChecker):
1820
1820
1821 @property
1821 @property
1822 def has_full_access(self):
1822 def has_full_access(self):
1823 return False
1823 return False
1824
1824
1825 def has_access(self, path):
1825 def has_access(self, path):
1826 return False
1826 return False
1827
1827
1828
1828
1829 class PatternPathPermissionChecker(BasePathPermissionChecker):
1829 class PatternPathPermissionChecker(BasePathPermissionChecker):
1830
1830
1831 def __init__(self, includes, excludes):
1831 def __init__(self, includes, excludes):
1832 self.includes = includes
1832 self.includes = includes
1833 self.excludes = excludes
1833 self.excludes = excludes
1834 self.includes_re = [] if not includes else [
1834 self.includes_re = [] if not includes else [
1835 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1835 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1836 self.excludes_re = [] if not excludes else [
1836 self.excludes_re = [] if not excludes else [
1837 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1837 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1838
1838
1839 @property
1839 @property
1840 def has_full_access(self):
1840 def has_full_access(self):
1841 return '*' in self.includes and not self.excludes
1841 return '*' in self.includes and not self.excludes
1842
1842
1843 def has_access(self, path):
1843 def has_access(self, path):
1844 for regex in self.excludes_re:
1844 for regex in self.excludes_re:
1845 if regex.match(path):
1845 if regex.match(path):
1846 return False
1846 return False
1847 for regex in self.includes_re:
1847 for regex in self.includes_re:
1848 if regex.match(path):
1848 if regex.match(path):
1849 return True
1849 return True
1850 return False
1850 return False
@@ -1,75 +1,79 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-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 Internal settings for vcs-lib
22 Internal settings for vcs-lib
23 """
23 """
24
24
25 # list of default encoding used in safe_unicode/safe_str methods
25 # list of default encoding used in safe_unicode/safe_str methods
26 DEFAULT_ENCODINGS = ['utf8']
26 DEFAULT_ENCODINGS = ['utf8']
27
27
28 # Optional arguments to rev-filter, it has to be a list
28 # Optional arguments to rev-filter, it has to be a list
29 # It can also be ['--branches', '--tags']
29 # It can also be ['--branches', '--tags']
30 GIT_REV_FILTER = ['--all']
30 GIT_REV_FILTER = ['--all']
31
31
32 # Compatibility version when creating SVN repositories. None means newest.
32 # Compatibility version when creating SVN repositories. None means newest.
33 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
33 # Other available options are: pre-1.4-compatible, pre-1.5-compatible,
34 # pre-1.6-compatible, pre-1.8-compatible
34 # pre-1.6-compatible, pre-1.8-compatible
35 SVN_COMPATIBLE_VERSION = None
35 SVN_COMPATIBLE_VERSION = None
36
36
37 ALIASES = ['hg', 'git', 'svn']
37 ALIASES = ['hg', 'git', 'svn']
38
38
39 BACKENDS = {
39 BACKENDS = {
40 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
40 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository',
41 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
41 'git': 'rhodecode.lib.vcs.backends.git.GitRepository',
42 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
42 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository',
43 }
43 }
44
44
45 # TODO: Remove once controllers/files.py is adjusted
45
46 ARCHIVE_SPECS = {
46 ARCHIVE_SPECS = [
47 'tbz2': ('application/x-bzip2', '.tar.bz2'),
47 ('tbz2', 'application/x-bzip2', 'tbz2'),
48 'tgz': ('application/x-gzip', '.tar.gz'),
48 ('tbz2', 'application/x-bzip2', '.tar.bz2'),
49 'zip': ('application/zip', '.zip'),
49
50 }
50 ('tgz', 'application/x-gzip', '.tgz'),
51 ('tgz', 'application/x-gzip', '.tar.gz'),
52
53 ('zip', 'application/zip', '.zip'),
54 ]
51
55
52 HOOKS_PROTOCOL = None
56 HOOKS_PROTOCOL = None
53 HOOKS_DIRECT_CALLS = False
57 HOOKS_DIRECT_CALLS = False
54 HOOKS_HOST = '127.0.0.1'
58 HOOKS_HOST = '127.0.0.1'
55
59
56
60
57 MERGE_MESSAGE_TMPL = (
61 MERGE_MESSAGE_TMPL = (
58 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}\n\n '
62 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}\n\n '
59 u'{pr_title}')
63 u'{pr_title}')
60 MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode'
64 MERGE_DRY_RUN_MESSAGE = 'dry_run_merge_message_from_rhodecode'
61 MERGE_DRY_RUN_USER = 'Dry-Run User'
65 MERGE_DRY_RUN_USER = 'Dry-Run User'
62 MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com'
66 MERGE_DRY_RUN_EMAIL = 'dry-run-merge@rhodecode.com'
63
67
64
68
65 def available_aliases():
69 def available_aliases():
66 """
70 """
67 Mercurial is required for the system to work, so in case vcs.backends does
71 Mercurial is required for the system to work, so in case vcs.backends does
68 not include it, we make sure it will be available internally
72 not include it, we make sure it will be available internally
69 TODO: anderson: refactor vcs.backends so it won't be necessary, VCS server
73 TODO: anderson: refactor vcs.backends so it won't be necessary, VCS server
70 should be responsible to dictate available backends.
74 should be responsible to dictate available backends.
71 """
75 """
72 aliases = ALIASES[:]
76 aliases = ALIASES[:]
73 if 'hg' not in aliases:
77 if 'hg' not in aliases:
74 aliases += ['hg']
78 aliases += ['hg']
75 return aliases
79 return aliases
General Comments 0
You need to be logged in to leave comments. Login now