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