##// END OF EJS Templates
Files: preserve filemode on web edits.
dan -
r3373:5f4a0f91 default
parent child Browse files
Show More
@@ -1,1392 +1,1393 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)
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 return c
88 88
89 89 def _ensure_not_locked(self):
90 90 _ = self.request.translate
91 91
92 92 repo = self.db_repo
93 93 if repo.enable_locking and repo.locked[0]:
94 94 h.flash(_('This repository has been locked by %s on %s')
95 95 % (h.person_by_id(repo.locked[0]),
96 96 h.format_date(h.time_to_datetime(repo.locked[1]))),
97 97 'warning')
98 98 files_url = h.route_path(
99 99 'repo_files:default_path',
100 100 repo_name=self.db_repo_name, commit_id='tip')
101 101 raise HTTPFound(files_url)
102 102
103 103 def check_branch_permission(self, branch_name):
104 104 _ = self.request.translate
105 105
106 106 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
107 107 self.db_repo_name, branch_name)
108 108 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
109 109 h.flash(
110 110 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
111 111 'warning')
112 112 files_url = h.route_path(
113 113 'repo_files:default_path',
114 114 repo_name=self.db_repo_name, commit_id='tip')
115 115 raise HTTPFound(files_url)
116 116
117 117 def _get_commit_and_path(self):
118 118 default_commit_id = self.db_repo.landing_rev[1]
119 119 default_f_path = '/'
120 120
121 121 commit_id = self.request.matchdict.get(
122 122 'commit_id', default_commit_id)
123 123 f_path = self._get_f_path(self.request.matchdict, default_f_path)
124 124 return commit_id, f_path
125 125
126 126 def _get_default_encoding(self, c):
127 127 enc_list = getattr(c, 'default_encodings', [])
128 128 return enc_list[0] if enc_list else 'UTF-8'
129 129
130 130 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
131 131 """
132 132 This is a safe way to get commit. If an error occurs it redirects to
133 133 tip with proper message
134 134
135 135 :param commit_id: id of commit to fetch
136 136 :param redirect_after: toggle redirection
137 137 """
138 138 _ = self.request.translate
139 139
140 140 try:
141 141 return self.rhodecode_vcs_repo.get_commit(commit_id)
142 142 except EmptyRepositoryError:
143 143 if not redirect_after:
144 144 return None
145 145
146 146 _url = h.route_path(
147 147 'repo_files_add_file',
148 148 repo_name=self.db_repo_name, commit_id=0, f_path='',
149 149 _anchor='edit')
150 150
151 151 if h.HasRepoPermissionAny(
152 152 'repository.write', 'repository.admin')(self.db_repo_name):
153 153 add_new = h.link_to(
154 154 _('Click here to add a new file.'), _url, class_="alert-link")
155 155 else:
156 156 add_new = ""
157 157
158 158 h.flash(h.literal(
159 159 _('There are no files yet. %s') % add_new), category='warning')
160 160 raise HTTPFound(
161 161 h.route_path('repo_summary', repo_name=self.db_repo_name))
162 162
163 163 except (CommitDoesNotExistError, LookupError):
164 164 msg = _('No such commit exists for this repository')
165 165 h.flash(msg, category='error')
166 166 raise HTTPNotFound()
167 167 except RepositoryError as e:
168 168 h.flash(safe_str(h.escape(e)), category='error')
169 169 raise HTTPNotFound()
170 170
171 171 def _get_filenode_or_redirect(self, commit_obj, path):
172 172 """
173 173 Returns file_node, if error occurs or given path is directory,
174 174 it'll redirect to top level path
175 175 """
176 176 _ = self.request.translate
177 177
178 178 try:
179 179 file_node = commit_obj.get_node(path)
180 180 if file_node.is_dir():
181 181 raise RepositoryError('The given path is a directory')
182 182 except CommitDoesNotExistError:
183 183 log.exception('No such commit exists for this repository')
184 184 h.flash(_('No such commit exists for this repository'), category='error')
185 185 raise HTTPNotFound()
186 186 except RepositoryError as e:
187 187 log.warning('Repository error while fetching '
188 188 'filenode `%s`. Err:%s', path, e)
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 return file_node
193 193
194 194 def _is_valid_head(self, commit_id, repo):
195 195 branch_name = sha_commit_id = ''
196 196 is_head = False
197 197
198 198 if h.is_svn(repo) and not repo.is_empty():
199 199 # Note: Subversion only has one head.
200 200 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
201 201 is_head = True
202 202 return branch_name, sha_commit_id, is_head
203 203
204 204 for _branch_name, branch_commit_id in repo.branches.items():
205 205 # simple case we pass in branch name, it's a HEAD
206 206 if commit_id == _branch_name:
207 207 is_head = True
208 208 branch_name = _branch_name
209 209 sha_commit_id = branch_commit_id
210 210 break
211 211 # case when we pass in full sha commit_id, which is a head
212 212 elif commit_id == branch_commit_id:
213 213 is_head = True
214 214 branch_name = _branch_name
215 215 sha_commit_id = branch_commit_id
216 216 break
217 217
218 218 # checked branches, means we only need to try to get the branch/commit_sha
219 219 if not repo.is_empty:
220 220 commit = repo.get_commit(commit_id=commit_id)
221 221 if commit:
222 222 branch_name = commit.branch
223 223 sha_commit_id = commit.raw_id
224 224
225 225 return branch_name, sha_commit_id, is_head
226 226
227 227 def _get_tree_at_commit(
228 228 self, c, commit_id, f_path, full_load=False):
229 229
230 230 repo_id = self.db_repo.repo_id
231 231
232 232 cache_seconds = safe_int(
233 233 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
234 234 cache_on = cache_seconds > 0
235 235 log.debug(
236 236 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
237 237 'with caching: %s[TTL: %ss]' % (
238 238 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
239 239
240 240 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
241 241 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
242 242
243 243 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
244 244 condition=cache_on)
245 245 def compute_file_tree(repo_id, commit_id, f_path, full_load):
246 246 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
247 247 repo_id, commit_id, f_path)
248 248
249 249 c.full_load = full_load
250 250 return render(
251 251 'rhodecode:templates/files/files_browser_tree.mako',
252 252 self._get_template_context(c), self.request)
253 253
254 254 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
255 255
256 256 def _get_archive_spec(self, fname):
257 257 log.debug('Detecting archive spec for: `%s`', fname)
258 258
259 259 fileformat = None
260 260 ext = None
261 261 content_type = None
262 262 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
263 263 content_type, extension = ext_data
264 264
265 265 if fname.endswith(extension):
266 266 fileformat = a_type
267 267 log.debug('archive is of type: %s', fileformat)
268 268 ext = extension
269 269 break
270 270
271 271 if not fileformat:
272 272 raise ValueError()
273 273
274 274 # left over part of whole fname is the commit
275 275 commit_id = fname[:-len(ext)]
276 276
277 277 return commit_id, ext, fileformat, content_type
278 278
279 279 @LoginRequired()
280 280 @HasRepoPermissionAnyDecorator(
281 281 'repository.read', 'repository.write', 'repository.admin')
282 282 @view_config(
283 283 route_name='repo_archivefile', request_method='GET',
284 284 renderer=None)
285 285 def repo_archivefile(self):
286 286 # archive cache config
287 287 from rhodecode import CONFIG
288 288 _ = self.request.translate
289 289 self.load_default_context()
290 290
291 291 fname = self.request.matchdict['fname']
292 292 subrepos = self.request.GET.get('subrepos') == 'true'
293 293
294 294 if not self.db_repo.enable_downloads:
295 295 return Response(_('Downloads disabled'))
296 296
297 297 try:
298 298 commit_id, ext, fileformat, content_type = \
299 299 self._get_archive_spec(fname)
300 300 except ValueError:
301 301 return Response(_('Unknown archive type for: `{}`').format(
302 302 h.escape(fname)))
303 303
304 304 try:
305 305 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
306 306 except CommitDoesNotExistError:
307 307 return Response(_('Unknown commit_id {}').format(
308 308 h.escape(commit_id)))
309 309 except EmptyRepositoryError:
310 310 return Response(_('Empty repository'))
311 311
312 312 archive_name = '%s-%s%s%s' % (
313 313 safe_str(self.db_repo_name.replace('/', '_')),
314 314 '-sub' if subrepos else '',
315 315 safe_str(commit.short_id), ext)
316 316
317 317 use_cached_archive = False
318 318 archive_cache_enabled = CONFIG.get(
319 319 'archive_cache_dir') and not self.request.GET.get('no_cache')
320 320 cached_archive_path = None
321 321
322 322 if archive_cache_enabled:
323 323 # check if we it's ok to write
324 324 if not os.path.isdir(CONFIG['archive_cache_dir']):
325 325 os.makedirs(CONFIG['archive_cache_dir'])
326 326 cached_archive_path = os.path.join(
327 327 CONFIG['archive_cache_dir'], archive_name)
328 328 if os.path.isfile(cached_archive_path):
329 329 log.debug('Found cached archive in %s', cached_archive_path)
330 330 fd, archive = None, cached_archive_path
331 331 use_cached_archive = True
332 332 else:
333 333 log.debug('Archive %s is not yet cached', archive_name)
334 334
335 335 if not use_cached_archive:
336 336 # generate new archive
337 337 fd, archive = tempfile.mkstemp()
338 338 log.debug('Creating new temp archive in %s', archive)
339 339 try:
340 340 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
341 341 except ImproperArchiveTypeError:
342 342 return _('Unknown archive type')
343 343 if archive_cache_enabled:
344 344 # if we generated the archive and we have cache enabled
345 345 # let's use this for future
346 346 log.debug('Storing new archive in %s', cached_archive_path)
347 347 shutil.move(archive, cached_archive_path)
348 348 archive = cached_archive_path
349 349
350 350 # store download action
351 351 audit_logger.store_web(
352 352 'repo.archive.download', action_data={
353 353 'user_agent': self.request.user_agent,
354 354 'archive_name': archive_name,
355 355 'archive_spec': fname,
356 356 'archive_cached': use_cached_archive},
357 357 user=self._rhodecode_user,
358 358 repo=self.db_repo,
359 359 commit=True
360 360 )
361 361
362 362 def get_chunked_archive(archive_path):
363 363 with open(archive_path, 'rb') as stream:
364 364 while True:
365 365 data = stream.read(16 * 1024)
366 366 if not data:
367 367 if fd: # fd means we used temporary file
368 368 os.close(fd)
369 369 if not archive_cache_enabled:
370 370 log.debug('Destroying temp archive %s', archive_path)
371 371 os.remove(archive_path)
372 372 break
373 373 yield data
374 374
375 375 response = Response(app_iter=get_chunked_archive(archive))
376 376 response.content_disposition = str(
377 377 'attachment; filename=%s' % archive_name)
378 378 response.content_type = str(content_type)
379 379
380 380 return response
381 381
382 382 def _get_file_node(self, commit_id, f_path):
383 383 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
384 384 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
385 385 try:
386 386 node = commit.get_node(f_path)
387 387 if node.is_dir():
388 388 raise NodeError('%s path is a %s not a file'
389 389 % (node, type(node)))
390 390 except NodeDoesNotExistError:
391 391 commit = EmptyCommit(
392 392 commit_id=commit_id,
393 393 idx=commit.idx,
394 394 repo=commit.repository,
395 395 alias=commit.repository.alias,
396 396 message=commit.message,
397 397 author=commit.author,
398 398 date=commit.date)
399 399 node = FileNode(f_path, '', commit=commit)
400 400 else:
401 401 commit = EmptyCommit(
402 402 repo=self.rhodecode_vcs_repo,
403 403 alias=self.rhodecode_vcs_repo.alias)
404 404 node = FileNode(f_path, '', commit=commit)
405 405 return node
406 406
407 407 @LoginRequired()
408 408 @HasRepoPermissionAnyDecorator(
409 409 'repository.read', 'repository.write', 'repository.admin')
410 410 @view_config(
411 411 route_name='repo_files_diff', request_method='GET',
412 412 renderer=None)
413 413 def repo_files_diff(self):
414 414 c = self.load_default_context()
415 415 f_path = self._get_f_path(self.request.matchdict)
416 416 diff1 = self.request.GET.get('diff1', '')
417 417 diff2 = self.request.GET.get('diff2', '')
418 418
419 419 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
420 420
421 421 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
422 422 line_context = self.request.GET.get('context', 3)
423 423
424 424 if not any((diff1, diff2)):
425 425 h.flash(
426 426 'Need query parameter "diff1" or "diff2" to generate a diff.',
427 427 category='error')
428 428 raise HTTPBadRequest()
429 429
430 430 c.action = self.request.GET.get('diff')
431 431 if c.action not in ['download', 'raw']:
432 432 compare_url = h.route_path(
433 433 'repo_compare',
434 434 repo_name=self.db_repo_name,
435 435 source_ref_type='rev',
436 436 source_ref=diff1,
437 437 target_repo=self.db_repo_name,
438 438 target_ref_type='rev',
439 439 target_ref=diff2,
440 440 _query=dict(f_path=f_path))
441 441 # redirect to new view if we render diff
442 442 raise HTTPFound(compare_url)
443 443
444 444 try:
445 445 node1 = self._get_file_node(diff1, path1)
446 446 node2 = self._get_file_node(diff2, f_path)
447 447 except (RepositoryError, NodeError):
448 448 log.exception("Exception while trying to get node from repository")
449 449 raise HTTPFound(
450 450 h.route_path('repo_files', repo_name=self.db_repo_name,
451 451 commit_id='tip', f_path=f_path))
452 452
453 453 if all(isinstance(node.commit, EmptyCommit)
454 454 for node in (node1, node2)):
455 455 raise HTTPNotFound()
456 456
457 457 c.commit_1 = node1.commit
458 458 c.commit_2 = node2.commit
459 459
460 460 if c.action == 'download':
461 461 _diff = diffs.get_gitdiff(node1, node2,
462 462 ignore_whitespace=ignore_whitespace,
463 463 context=line_context)
464 464 diff = diffs.DiffProcessor(_diff, format='gitdiff')
465 465
466 466 response = Response(self.path_filter.get_raw_patch(diff))
467 467 response.content_type = 'text/plain'
468 468 response.content_disposition = (
469 469 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
470 470 )
471 471 charset = self._get_default_encoding(c)
472 472 if charset:
473 473 response.charset = charset
474 474 return response
475 475
476 476 elif c.action == 'raw':
477 477 _diff = diffs.get_gitdiff(node1, node2,
478 478 ignore_whitespace=ignore_whitespace,
479 479 context=line_context)
480 480 diff = diffs.DiffProcessor(_diff, format='gitdiff')
481 481
482 482 response = Response(self.path_filter.get_raw_patch(diff))
483 483 response.content_type = 'text/plain'
484 484 charset = self._get_default_encoding(c)
485 485 if charset:
486 486 response.charset = charset
487 487 return response
488 488
489 489 # in case we ever end up here
490 490 raise HTTPNotFound()
491 491
492 492 @LoginRequired()
493 493 @HasRepoPermissionAnyDecorator(
494 494 'repository.read', 'repository.write', 'repository.admin')
495 495 @view_config(
496 496 route_name='repo_files_diff_2way_redirect', request_method='GET',
497 497 renderer=None)
498 498 def repo_files_diff_2way_redirect(self):
499 499 """
500 500 Kept only to make OLD links work
501 501 """
502 502 f_path = self._get_f_path_unchecked(self.request.matchdict)
503 503 diff1 = self.request.GET.get('diff1', '')
504 504 diff2 = self.request.GET.get('diff2', '')
505 505
506 506 if not any((diff1, diff2)):
507 507 h.flash(
508 508 'Need query parameter "diff1" or "diff2" to generate a diff.',
509 509 category='error')
510 510 raise HTTPBadRequest()
511 511
512 512 compare_url = h.route_path(
513 513 'repo_compare',
514 514 repo_name=self.db_repo_name,
515 515 source_ref_type='rev',
516 516 source_ref=diff1,
517 517 target_ref_type='rev',
518 518 target_ref=diff2,
519 519 _query=dict(f_path=f_path, diffmode='sideside',
520 520 target_repo=self.db_repo_name,))
521 521 raise HTTPFound(compare_url)
522 522
523 523 @LoginRequired()
524 524 @HasRepoPermissionAnyDecorator(
525 525 'repository.read', 'repository.write', 'repository.admin')
526 526 @view_config(
527 527 route_name='repo_files', request_method='GET',
528 528 renderer=None)
529 529 @view_config(
530 530 route_name='repo_files:default_path', request_method='GET',
531 531 renderer=None)
532 532 @view_config(
533 533 route_name='repo_files:default_commit', request_method='GET',
534 534 renderer=None)
535 535 @view_config(
536 536 route_name='repo_files:rendered', request_method='GET',
537 537 renderer=None)
538 538 @view_config(
539 539 route_name='repo_files:annotated', request_method='GET',
540 540 renderer=None)
541 541 def repo_files(self):
542 542 c = self.load_default_context()
543 543
544 544 view_name = getattr(self.request.matched_route, 'name', None)
545 545
546 546 c.annotate = view_name == 'repo_files:annotated'
547 547 # default is false, but .rst/.md files later are auto rendered, we can
548 548 # overwrite auto rendering by setting this GET flag
549 549 c.renderer = view_name == 'repo_files:rendered' or \
550 550 not self.request.GET.get('no-render', False)
551 551
552 552 # redirect to given commit_id from form if given
553 553 get_commit_id = self.request.GET.get('at_rev', None)
554 554 if get_commit_id:
555 555 self._get_commit_or_redirect(get_commit_id)
556 556
557 557 commit_id, f_path = self._get_commit_and_path()
558 558 c.commit = self._get_commit_or_redirect(commit_id)
559 559 c.branch = self.request.GET.get('branch', None)
560 560 c.f_path = f_path
561 561
562 562 # prev link
563 563 try:
564 564 prev_commit = c.commit.prev(c.branch)
565 565 c.prev_commit = prev_commit
566 566 c.url_prev = h.route_path(
567 567 'repo_files', repo_name=self.db_repo_name,
568 568 commit_id=prev_commit.raw_id, f_path=f_path)
569 569 if c.branch:
570 570 c.url_prev += '?branch=%s' % c.branch
571 571 except (CommitDoesNotExistError, VCSError):
572 572 c.url_prev = '#'
573 573 c.prev_commit = EmptyCommit()
574 574
575 575 # next link
576 576 try:
577 577 next_commit = c.commit.next(c.branch)
578 578 c.next_commit = next_commit
579 579 c.url_next = h.route_path(
580 580 'repo_files', repo_name=self.db_repo_name,
581 581 commit_id=next_commit.raw_id, f_path=f_path)
582 582 if c.branch:
583 583 c.url_next += '?branch=%s' % c.branch
584 584 except (CommitDoesNotExistError, VCSError):
585 585 c.url_next = '#'
586 586 c.next_commit = EmptyCommit()
587 587
588 588 # files or dirs
589 589 try:
590 590 c.file = c.commit.get_node(f_path)
591 591 c.file_author = True
592 592 c.file_tree = ''
593 593
594 594 # load file content
595 595 if c.file.is_file():
596 596 c.lf_node = c.file.get_largefile_node()
597 597
598 598 c.file_source_page = 'true'
599 599 c.file_last_commit = c.file.last_commit
600 600 if c.file.size < c.visual.cut_off_limit_diff:
601 601 if c.annotate: # annotation has precedence over renderer
602 602 c.annotated_lines = filenode_as_annotated_lines_tokens(
603 603 c.file
604 604 )
605 605 else:
606 606 c.renderer = (
607 607 c.renderer and h.renderer_from_filename(c.file.path)
608 608 )
609 609 if not c.renderer:
610 610 c.lines = filenode_as_lines_tokens(c.file)
611 611
612 612 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
613 613 commit_id, self.rhodecode_vcs_repo)
614 614 c.on_branch_head = is_head
615 615
616 616 branch = c.commit.branch if (
617 617 c.commit.branch and '/' not in c.commit.branch) else None
618 618 c.branch_or_raw_id = branch or c.commit.raw_id
619 619 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
620 620
621 621 author = c.file_last_commit.author
622 622 c.authors = [[
623 623 h.email(author),
624 624 h.person(author, 'username_or_name_or_email'),
625 625 1
626 626 ]]
627 627
628 628 else: # load tree content at path
629 629 c.file_source_page = 'false'
630 630 c.authors = []
631 631 # this loads a simple tree without metadata to speed things up
632 632 # later via ajax we call repo_nodetree_full and fetch whole
633 633 c.file_tree = self._get_tree_at_commit(
634 634 c, c.commit.raw_id, f_path)
635 635
636 636 except RepositoryError as e:
637 637 h.flash(safe_str(h.escape(e)), category='error')
638 638 raise HTTPNotFound()
639 639
640 640 if self.request.environ.get('HTTP_X_PJAX'):
641 641 html = render('rhodecode:templates/files/files_pjax.mako',
642 642 self._get_template_context(c), self.request)
643 643 else:
644 644 html = render('rhodecode:templates/files/files.mako',
645 645 self._get_template_context(c), self.request)
646 646 return Response(html)
647 647
648 648 @HasRepoPermissionAnyDecorator(
649 649 'repository.read', 'repository.write', 'repository.admin')
650 650 @view_config(
651 651 route_name='repo_files:annotated_previous', request_method='GET',
652 652 renderer=None)
653 653 def repo_files_annotated_previous(self):
654 654 self.load_default_context()
655 655
656 656 commit_id, f_path = self._get_commit_and_path()
657 657 commit = self._get_commit_or_redirect(commit_id)
658 658 prev_commit_id = commit.raw_id
659 659 line_anchor = self.request.GET.get('line_anchor')
660 660 is_file = False
661 661 try:
662 662 _file = commit.get_node(f_path)
663 663 is_file = _file.is_file()
664 664 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
665 665 pass
666 666
667 667 if is_file:
668 668 history = commit.get_path_history(f_path)
669 669 prev_commit_id = history[1].raw_id \
670 670 if len(history) > 1 else prev_commit_id
671 671 prev_url = h.route_path(
672 672 'repo_files:annotated', repo_name=self.db_repo_name,
673 673 commit_id=prev_commit_id, f_path=f_path,
674 674 _anchor='L{}'.format(line_anchor))
675 675
676 676 raise HTTPFound(prev_url)
677 677
678 678 @LoginRequired()
679 679 @HasRepoPermissionAnyDecorator(
680 680 'repository.read', 'repository.write', 'repository.admin')
681 681 @view_config(
682 682 route_name='repo_nodetree_full', request_method='GET',
683 683 renderer=None, xhr=True)
684 684 @view_config(
685 685 route_name='repo_nodetree_full:default_path', request_method='GET',
686 686 renderer=None, xhr=True)
687 687 def repo_nodetree_full(self):
688 688 """
689 689 Returns rendered html of file tree that contains commit date,
690 690 author, commit_id for the specified combination of
691 691 repo, commit_id and file path
692 692 """
693 693 c = self.load_default_context()
694 694
695 695 commit_id, f_path = self._get_commit_and_path()
696 696 commit = self._get_commit_or_redirect(commit_id)
697 697 try:
698 698 dir_node = commit.get_node(f_path)
699 699 except RepositoryError as e:
700 700 return Response('error: {}'.format(h.escape(safe_str(e))))
701 701
702 702 if dir_node.is_file():
703 703 return Response('')
704 704
705 705 c.file = dir_node
706 706 c.commit = commit
707 707
708 708 html = self._get_tree_at_commit(
709 709 c, commit.raw_id, dir_node.path, full_load=True)
710 710
711 711 return Response(html)
712 712
713 713 def _get_attachement_headers(self, f_path):
714 714 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
715 715 safe_path = f_name.replace('"', '\\"')
716 716 encoded_path = urllib.quote(f_name)
717 717
718 718 return "attachment; " \
719 719 "filename=\"{}\"; " \
720 720 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
721 721
722 722 @LoginRequired()
723 723 @HasRepoPermissionAnyDecorator(
724 724 'repository.read', 'repository.write', 'repository.admin')
725 725 @view_config(
726 726 route_name='repo_file_raw', request_method='GET',
727 727 renderer=None)
728 728 def repo_file_raw(self):
729 729 """
730 730 Action for show as raw, some mimetypes are "rendered",
731 731 those include images, icons.
732 732 """
733 733 c = self.load_default_context()
734 734
735 735 commit_id, f_path = self._get_commit_and_path()
736 736 commit = self._get_commit_or_redirect(commit_id)
737 737 file_node = self._get_filenode_or_redirect(commit, f_path)
738 738
739 739 raw_mimetype_mapping = {
740 740 # map original mimetype to a mimetype used for "show as raw"
741 741 # you can also provide a content-disposition to override the
742 742 # default "attachment" disposition.
743 743 # orig_type: (new_type, new_dispo)
744 744
745 745 # show images inline:
746 746 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
747 747 # for example render an SVG with javascript inside or even render
748 748 # HTML.
749 749 'image/x-icon': ('image/x-icon', 'inline'),
750 750 'image/png': ('image/png', 'inline'),
751 751 'image/gif': ('image/gif', 'inline'),
752 752 'image/jpeg': ('image/jpeg', 'inline'),
753 753 'application/pdf': ('application/pdf', 'inline'),
754 754 }
755 755
756 756 mimetype = file_node.mimetype
757 757 try:
758 758 mimetype, disposition = raw_mimetype_mapping[mimetype]
759 759 except KeyError:
760 760 # we don't know anything special about this, handle it safely
761 761 if file_node.is_binary:
762 762 # do same as download raw for binary files
763 763 mimetype, disposition = 'application/octet-stream', 'attachment'
764 764 else:
765 765 # do not just use the original mimetype, but force text/plain,
766 766 # otherwise it would serve text/html and that might be unsafe.
767 767 # Note: underlying vcs library fakes text/plain mimetype if the
768 768 # mimetype can not be determined and it thinks it is not
769 769 # binary.This might lead to erroneous text display in some
770 770 # cases, but helps in other cases, like with text files
771 771 # without extension.
772 772 mimetype, disposition = 'text/plain', 'inline'
773 773
774 774 if disposition == 'attachment':
775 775 disposition = self._get_attachement_headers(f_path)
776 776
777 777 def stream_node():
778 778 yield file_node.raw_bytes
779 779
780 780 response = Response(app_iter=stream_node())
781 781 response.content_disposition = disposition
782 782 response.content_type = mimetype
783 783
784 784 charset = self._get_default_encoding(c)
785 785 if charset:
786 786 response.charset = charset
787 787
788 788 return response
789 789
790 790 @LoginRequired()
791 791 @HasRepoPermissionAnyDecorator(
792 792 'repository.read', 'repository.write', 'repository.admin')
793 793 @view_config(
794 794 route_name='repo_file_download', request_method='GET',
795 795 renderer=None)
796 796 @view_config(
797 797 route_name='repo_file_download:legacy', request_method='GET',
798 798 renderer=None)
799 799 def repo_file_download(self):
800 800 c = self.load_default_context()
801 801
802 802 commit_id, f_path = self._get_commit_and_path()
803 803 commit = self._get_commit_or_redirect(commit_id)
804 804 file_node = self._get_filenode_or_redirect(commit, f_path)
805 805
806 806 if self.request.GET.get('lf'):
807 807 # only if lf get flag is passed, we download this file
808 808 # as LFS/Largefile
809 809 lf_node = file_node.get_largefile_node()
810 810 if lf_node:
811 811 # overwrite our pointer with the REAL large-file
812 812 file_node = lf_node
813 813
814 814 disposition = self._get_attachement_headers(f_path)
815 815
816 816 def stream_node():
817 817 yield file_node.raw_bytes
818 818
819 819 response = Response(app_iter=stream_node())
820 820 response.content_disposition = disposition
821 821 response.content_type = file_node.mimetype
822 822
823 823 charset = self._get_default_encoding(c)
824 824 if charset:
825 825 response.charset = charset
826 826
827 827 return response
828 828
829 829 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
830 830
831 831 cache_seconds = safe_int(
832 832 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
833 833 cache_on = cache_seconds > 0
834 834 log.debug(
835 835 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
836 836 'with caching: %s[TTL: %ss]' % (
837 837 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
838 838
839 839 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
840 840 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
841 841
842 842 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
843 843 condition=cache_on)
844 844 def compute_file_search(repo_id, commit_id, f_path):
845 845 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
846 846 repo_id, commit_id, f_path)
847 847 try:
848 848 _d, _f = ScmModel().get_nodes(
849 849 repo_name, commit_id, f_path, flat=False)
850 850 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
851 851 log.exception(safe_str(e))
852 852 h.flash(safe_str(h.escape(e)), category='error')
853 853 raise HTTPFound(h.route_path(
854 854 'repo_files', repo_name=self.db_repo_name,
855 855 commit_id='tip', f_path='/'))
856 856 return _d + _f
857 857
858 858 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
859 859
860 860 @LoginRequired()
861 861 @HasRepoPermissionAnyDecorator(
862 862 'repository.read', 'repository.write', 'repository.admin')
863 863 @view_config(
864 864 route_name='repo_files_nodelist', request_method='GET',
865 865 renderer='json_ext', xhr=True)
866 866 def repo_nodelist(self):
867 867 self.load_default_context()
868 868
869 869 commit_id, f_path = self._get_commit_and_path()
870 870 commit = self._get_commit_or_redirect(commit_id)
871 871
872 872 metadata = self._get_nodelist_at_commit(
873 873 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
874 874 return {'nodes': metadata}
875 875
876 876 def _create_references(
877 877 self, branches_or_tags, symbolic_reference, f_path):
878 878 items = []
879 879 for name, commit_id in branches_or_tags.items():
880 880 sym_ref = symbolic_reference(commit_id, name, f_path)
881 881 items.append((sym_ref, name))
882 882 return items
883 883
884 884 def _symbolic_reference(self, commit_id, name, f_path):
885 885 return commit_id
886 886
887 887 def _symbolic_reference_svn(self, commit_id, name, f_path):
888 888 new_f_path = vcspath.join(name, f_path)
889 889 return u'%s@%s' % (new_f_path, commit_id)
890 890
891 891 def _get_node_history(self, commit_obj, f_path, commits=None):
892 892 """
893 893 get commit history for given node
894 894
895 895 :param commit_obj: commit to calculate history
896 896 :param f_path: path for node to calculate history for
897 897 :param commits: if passed don't calculate history and take
898 898 commits defined in this list
899 899 """
900 900 _ = self.request.translate
901 901
902 902 # calculate history based on tip
903 903 tip = self.rhodecode_vcs_repo.get_commit()
904 904 if commits is None:
905 905 pre_load = ["author", "branch"]
906 906 try:
907 907 commits = tip.get_path_history(f_path, pre_load=pre_load)
908 908 except (NodeDoesNotExistError, CommitError):
909 909 # this node is not present at tip!
910 910 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
911 911
912 912 history = []
913 913 commits_group = ([], _("Changesets"))
914 914 for commit in commits:
915 915 branch = ' (%s)' % commit.branch if commit.branch else ''
916 916 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
917 917 commits_group[0].append((commit.raw_id, n_desc,))
918 918 history.append(commits_group)
919 919
920 920 symbolic_reference = self._symbolic_reference
921 921
922 922 if self.rhodecode_vcs_repo.alias == 'svn':
923 923 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
924 924 f_path, self.rhodecode_vcs_repo)
925 925 if adjusted_f_path != f_path:
926 926 log.debug(
927 927 'Recognized svn tag or branch in file "%s", using svn '
928 928 'specific symbolic references', f_path)
929 929 f_path = adjusted_f_path
930 930 symbolic_reference = self._symbolic_reference_svn
931 931
932 932 branches = self._create_references(
933 933 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
934 934 branches_group = (branches, _("Branches"))
935 935
936 936 tags = self._create_references(
937 937 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
938 938 tags_group = (tags, _("Tags"))
939 939
940 940 history.append(branches_group)
941 941 history.append(tags_group)
942 942
943 943 return history, commits
944 944
945 945 @LoginRequired()
946 946 @HasRepoPermissionAnyDecorator(
947 947 'repository.read', 'repository.write', 'repository.admin')
948 948 @view_config(
949 949 route_name='repo_file_history', request_method='GET',
950 950 renderer='json_ext')
951 951 def repo_file_history(self):
952 952 self.load_default_context()
953 953
954 954 commit_id, f_path = self._get_commit_and_path()
955 955 commit = self._get_commit_or_redirect(commit_id)
956 956 file_node = self._get_filenode_or_redirect(commit, f_path)
957 957
958 958 if file_node.is_file():
959 959 file_history, _hist = self._get_node_history(commit, f_path)
960 960
961 961 res = []
962 962 for obj in file_history:
963 963 res.append({
964 964 'text': obj[1],
965 965 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
966 966 })
967 967
968 968 data = {
969 969 'more': False,
970 970 'results': res
971 971 }
972 972 return data
973 973
974 974 log.warning('Cannot fetch history for directory')
975 975 raise HTTPBadRequest()
976 976
977 977 @LoginRequired()
978 978 @HasRepoPermissionAnyDecorator(
979 979 'repository.read', 'repository.write', 'repository.admin')
980 980 @view_config(
981 981 route_name='repo_file_authors', request_method='GET',
982 982 renderer='rhodecode:templates/files/file_authors_box.mako')
983 983 def repo_file_authors(self):
984 984 c = self.load_default_context()
985 985
986 986 commit_id, f_path = self._get_commit_and_path()
987 987 commit = self._get_commit_or_redirect(commit_id)
988 988 file_node = self._get_filenode_or_redirect(commit, f_path)
989 989
990 990 if not file_node.is_file():
991 991 raise HTTPBadRequest()
992 992
993 993 c.file_last_commit = file_node.last_commit
994 994 if self.request.GET.get('annotate') == '1':
995 995 # use _hist from annotation if annotation mode is on
996 996 commit_ids = set(x[1] for x in file_node.annotate)
997 997 _hist = (
998 998 self.rhodecode_vcs_repo.get_commit(commit_id)
999 999 for commit_id in commit_ids)
1000 1000 else:
1001 1001 _f_history, _hist = self._get_node_history(commit, f_path)
1002 1002 c.file_author = False
1003 1003
1004 1004 unique = collections.OrderedDict()
1005 1005 for commit in _hist:
1006 1006 author = commit.author
1007 1007 if author not in unique:
1008 1008 unique[commit.author] = [
1009 1009 h.email(author),
1010 1010 h.person(author, 'username_or_name_or_email'),
1011 1011 1 # counter
1012 1012 ]
1013 1013
1014 1014 else:
1015 1015 # increase counter
1016 1016 unique[commit.author][2] += 1
1017 1017
1018 1018 c.authors = [val for val in unique.values()]
1019 1019
1020 1020 return self._get_template_context(c)
1021 1021
1022 1022 @LoginRequired()
1023 1023 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1024 1024 @view_config(
1025 1025 route_name='repo_files_remove_file', request_method='GET',
1026 1026 renderer='rhodecode:templates/files/files_delete.mako')
1027 1027 def repo_files_remove_file(self):
1028 1028 _ = self.request.translate
1029 1029 c = self.load_default_context()
1030 1030 commit_id, f_path = self._get_commit_and_path()
1031 1031
1032 1032 self._ensure_not_locked()
1033 1033 _branch_name, _sha_commit_id, is_head = \
1034 1034 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1035 1035
1036 1036 if not is_head:
1037 1037 h.flash(_('You can only delete files with commit '
1038 1038 'being a valid branch head.'), category='warning')
1039 1039 raise HTTPFound(
1040 1040 h.route_path('repo_files',
1041 1041 repo_name=self.db_repo_name, commit_id='tip',
1042 1042 f_path=f_path))
1043 1043
1044 1044 self.check_branch_permission(_branch_name)
1045 1045 c.commit = self._get_commit_or_redirect(commit_id)
1046 1046 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1047 1047
1048 1048 c.default_message = _(
1049 1049 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1050 1050 c.f_path = f_path
1051 1051
1052 1052 return self._get_template_context(c)
1053 1053
1054 1054 @LoginRequired()
1055 1055 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1056 1056 @CSRFRequired()
1057 1057 @view_config(
1058 1058 route_name='repo_files_delete_file', request_method='POST',
1059 1059 renderer=None)
1060 1060 def repo_files_delete_file(self):
1061 1061 _ = self.request.translate
1062 1062
1063 1063 c = self.load_default_context()
1064 1064 commit_id, f_path = self._get_commit_and_path()
1065 1065
1066 1066 self._ensure_not_locked()
1067 1067 _branch_name, _sha_commit_id, is_head = \
1068 1068 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1069 1069
1070 1070 if not is_head:
1071 1071 h.flash(_('You can only delete files with commit '
1072 1072 'being a valid branch head.'), category='warning')
1073 1073 raise HTTPFound(
1074 1074 h.route_path('repo_files',
1075 1075 repo_name=self.db_repo_name, commit_id='tip',
1076 1076 f_path=f_path))
1077 1077 self.check_branch_permission(_branch_name)
1078 1078
1079 1079 c.commit = self._get_commit_or_redirect(commit_id)
1080 1080 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1081 1081
1082 1082 c.default_message = _(
1083 1083 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1084 1084 c.f_path = f_path
1085 1085 node_path = f_path
1086 1086 author = self._rhodecode_db_user.full_contact
1087 1087 message = self.request.POST.get('message') or c.default_message
1088 1088 try:
1089 1089 nodes = {
1090 1090 node_path: {
1091 1091 'content': ''
1092 1092 }
1093 1093 }
1094 1094 ScmModel().delete_nodes(
1095 1095 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1096 1096 message=message,
1097 1097 nodes=nodes,
1098 1098 parent_commit=c.commit,
1099 1099 author=author,
1100 1100 )
1101 1101
1102 1102 h.flash(
1103 1103 _('Successfully deleted file `{}`').format(
1104 1104 h.escape(f_path)), category='success')
1105 1105 except Exception:
1106 1106 log.exception('Error during commit operation')
1107 1107 h.flash(_('Error occurred during commit'), category='error')
1108 1108 raise HTTPFound(
1109 1109 h.route_path('repo_commit', repo_name=self.db_repo_name,
1110 1110 commit_id='tip'))
1111 1111
1112 1112 @LoginRequired()
1113 1113 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1114 1114 @view_config(
1115 1115 route_name='repo_files_edit_file', request_method='GET',
1116 1116 renderer='rhodecode:templates/files/files_edit.mako')
1117 1117 def repo_files_edit_file(self):
1118 1118 _ = self.request.translate
1119 1119 c = self.load_default_context()
1120 1120 commit_id, f_path = self._get_commit_and_path()
1121 1121
1122 1122 self._ensure_not_locked()
1123 1123 _branch_name, _sha_commit_id, is_head = \
1124 1124 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1125 1125
1126 1126 if not is_head:
1127 1127 h.flash(_('You can only edit files with commit '
1128 1128 'being a valid branch head.'), category='warning')
1129 1129 raise HTTPFound(
1130 1130 h.route_path('repo_files',
1131 1131 repo_name=self.db_repo_name, commit_id='tip',
1132 1132 f_path=f_path))
1133 1133 self.check_branch_permission(_branch_name)
1134 1134
1135 1135 c.commit = self._get_commit_or_redirect(commit_id)
1136 1136 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1137 1137
1138 1138 if c.file.is_binary:
1139 1139 files_url = h.route_path(
1140 1140 'repo_files',
1141 1141 repo_name=self.db_repo_name,
1142 1142 commit_id=c.commit.raw_id, f_path=f_path)
1143 1143 raise HTTPFound(files_url)
1144 1144
1145 1145 c.default_message = _(
1146 1146 'Edited file {} via RhodeCode Enterprise').format(f_path)
1147 1147 c.f_path = f_path
1148 1148
1149 1149 return self._get_template_context(c)
1150 1150
1151 1151 @LoginRequired()
1152 1152 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1153 1153 @CSRFRequired()
1154 1154 @view_config(
1155 1155 route_name='repo_files_update_file', request_method='POST',
1156 1156 renderer=None)
1157 1157 def repo_files_update_file(self):
1158 1158 _ = self.request.translate
1159 1159 c = self.load_default_context()
1160 1160 commit_id, f_path = self._get_commit_and_path()
1161 1161
1162 1162 self._ensure_not_locked()
1163 1163 _branch_name, _sha_commit_id, is_head = \
1164 1164 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1165 1165
1166 1166 if not is_head:
1167 1167 h.flash(_('You can only edit files with commit '
1168 1168 'being a valid branch head.'), category='warning')
1169 1169 raise HTTPFound(
1170 1170 h.route_path('repo_files',
1171 1171 repo_name=self.db_repo_name, commit_id='tip',
1172 1172 f_path=f_path))
1173 1173
1174 1174 self.check_branch_permission(_branch_name)
1175 1175
1176 1176 c.commit = self._get_commit_or_redirect(commit_id)
1177 1177 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1178 1178
1179 1179 if c.file.is_binary:
1180 1180 raise HTTPFound(
1181 1181 h.route_path('repo_files',
1182 1182 repo_name=self.db_repo_name,
1183 1183 commit_id=c.commit.raw_id,
1184 1184 f_path=f_path))
1185 1185
1186 1186 c.default_message = _(
1187 1187 'Edited file {} via RhodeCode Enterprise').format(f_path)
1188 1188 c.f_path = f_path
1189 1189 old_content = c.file.content
1190 1190 sl = old_content.splitlines(1)
1191 1191 first_line = sl[0] if sl else ''
1192 1192
1193 1193 r_post = self.request.POST
1194 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1195 mode = detect_mode(first_line, 0)
1196 content = convert_line_endings(r_post.get('content', ''), mode)
1194 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1195 line_ending_mode = detect_mode(first_line, 0)
1196 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1197 1197
1198 1198 message = r_post.get('message') or c.default_message
1199 1199 org_f_path = c.file.unicode_path
1200 1200 filename = r_post['filename']
1201 1201 org_filename = c.file.name
1202 1202
1203 1203 if content == old_content and filename == org_filename:
1204 1204 h.flash(_('No changes'), category='warning')
1205 1205 raise HTTPFound(
1206 1206 h.route_path('repo_commit', repo_name=self.db_repo_name,
1207 1207 commit_id='tip'))
1208 1208 try:
1209 1209 mapping = {
1210 1210 org_f_path: {
1211 1211 'org_filename': org_f_path,
1212 1212 'filename': os.path.join(c.file.dir_path, filename),
1213 1213 'content': content,
1214 1214 'lexer': '',
1215 1215 'op': 'mod',
1216 'mode': c.file.mode
1216 1217 }
1217 1218 }
1218 1219
1219 1220 ScmModel().update_nodes(
1220 1221 user=self._rhodecode_db_user.user_id,
1221 1222 repo=self.db_repo,
1222 1223 message=message,
1223 1224 nodes=mapping,
1224 1225 parent_commit=c.commit,
1225 1226 )
1226 1227
1227 1228 h.flash(
1228 1229 _('Successfully committed changes to file `{}`').format(
1229 1230 h.escape(f_path)), category='success')
1230 1231 except Exception:
1231 1232 log.exception('Error occurred during commit')
1232 1233 h.flash(_('Error occurred during commit'), category='error')
1233 1234 raise HTTPFound(
1234 1235 h.route_path('repo_commit', repo_name=self.db_repo_name,
1235 1236 commit_id='tip'))
1236 1237
1237 1238 @LoginRequired()
1238 1239 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1239 1240 @view_config(
1240 1241 route_name='repo_files_add_file', request_method='GET',
1241 1242 renderer='rhodecode:templates/files/files_add.mako')
1242 1243 def repo_files_add_file(self):
1243 1244 _ = self.request.translate
1244 1245 c = self.load_default_context()
1245 1246 commit_id, f_path = self._get_commit_and_path()
1246 1247
1247 1248 self._ensure_not_locked()
1248 1249
1249 1250 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1250 1251 if c.commit is None:
1251 1252 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1252 1253 c.default_message = (_('Added file via RhodeCode Enterprise'))
1253 1254 c.f_path = f_path.lstrip('/') # ensure not relative path
1254 1255
1255 1256 if self.rhodecode_vcs_repo.is_empty:
1256 1257 # for empty repository we cannot check for current branch, we rely on
1257 1258 # c.commit.branch instead
1258 1259 _branch_name = c.commit.branch
1259 1260 is_head = True
1260 1261 else:
1261 1262 _branch_name, _sha_commit_id, is_head = \
1262 1263 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1263 1264
1264 1265 if not is_head:
1265 1266 h.flash(_('You can only add files with commit '
1266 1267 'being a valid branch head.'), category='warning')
1267 1268 raise HTTPFound(
1268 1269 h.route_path('repo_files',
1269 1270 repo_name=self.db_repo_name, commit_id='tip',
1270 1271 f_path=f_path))
1271 1272
1272 1273 self.check_branch_permission(_branch_name)
1273 1274
1274 1275 return self._get_template_context(c)
1275 1276
1276 1277 @LoginRequired()
1277 1278 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1278 1279 @CSRFRequired()
1279 1280 @view_config(
1280 1281 route_name='repo_files_create_file', request_method='POST',
1281 1282 renderer=None)
1282 1283 def repo_files_create_file(self):
1283 1284 _ = self.request.translate
1284 1285 c = self.load_default_context()
1285 1286 commit_id, f_path = self._get_commit_and_path()
1286 1287
1287 1288 self._ensure_not_locked()
1288 1289
1289 1290 r_post = self.request.POST
1290 1291
1291 1292 c.commit = self._get_commit_or_redirect(
1292 1293 commit_id, redirect_after=False)
1293 1294 if c.commit is None:
1294 1295 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1295 1296
1296 1297 if self.rhodecode_vcs_repo.is_empty:
1297 1298 # for empty repository we cannot check for current branch, we rely on
1298 1299 # c.commit.branch instead
1299 1300 _branch_name = c.commit.branch
1300 1301 is_head = True
1301 1302 else:
1302 1303 _branch_name, _sha_commit_id, is_head = \
1303 1304 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1304 1305
1305 1306 if not is_head:
1306 1307 h.flash(_('You can only add files with commit '
1307 1308 'being a valid branch head.'), category='warning')
1308 1309 raise HTTPFound(
1309 1310 h.route_path('repo_files',
1310 1311 repo_name=self.db_repo_name, commit_id='tip',
1311 1312 f_path=f_path))
1312 1313
1313 1314 self.check_branch_permission(_branch_name)
1314 1315
1315 1316 c.default_message = (_('Added file via RhodeCode Enterprise'))
1316 1317 c.f_path = f_path
1317 1318 unix_mode = 0
1318 1319 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1319 1320
1320 1321 message = r_post.get('message') or c.default_message
1321 1322 filename = r_post.get('filename')
1322 1323 location = r_post.get('location', '') # dir location
1323 1324 file_obj = r_post.get('upload_file', None)
1324 1325
1325 1326 if file_obj is not None and hasattr(file_obj, 'filename'):
1326 1327 filename = r_post.get('filename_upload')
1327 1328 content = file_obj.file
1328 1329
1329 1330 if hasattr(content, 'file'):
1330 1331 # non posix systems store real file under file attr
1331 1332 content = content.file
1332 1333
1333 1334 if self.rhodecode_vcs_repo.is_empty:
1334 1335 default_redirect_url = h.route_path(
1335 1336 'repo_summary', repo_name=self.db_repo_name)
1336 1337 else:
1337 1338 default_redirect_url = h.route_path(
1338 1339 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1339 1340
1340 1341 # If there's no commit, redirect to repo summary
1341 1342 if type(c.commit) is EmptyCommit:
1342 1343 redirect_url = h.route_path(
1343 1344 'repo_summary', repo_name=self.db_repo_name)
1344 1345 else:
1345 1346 redirect_url = default_redirect_url
1346 1347
1347 1348 if not filename:
1348 1349 h.flash(_('No filename'), category='warning')
1349 1350 raise HTTPFound(redirect_url)
1350 1351
1351 1352 # extract the location from filename,
1352 1353 # allows using foo/bar.txt syntax to create subdirectories
1353 1354 subdir_loc = filename.rsplit('/', 1)
1354 1355 if len(subdir_loc) == 2:
1355 1356 location = os.path.join(location, subdir_loc[0])
1356 1357
1357 1358 # strip all crap out of file, just leave the basename
1358 1359 filename = os.path.basename(filename)
1359 1360 node_path = os.path.join(location, filename)
1360 1361 author = self._rhodecode_db_user.full_contact
1361 1362
1362 1363 try:
1363 1364 nodes = {
1364 1365 node_path: {
1365 1366 'content': content
1366 1367 }
1367 1368 }
1368 1369 ScmModel().create_nodes(
1369 1370 user=self._rhodecode_db_user.user_id,
1370 1371 repo=self.db_repo,
1371 1372 message=message,
1372 1373 nodes=nodes,
1373 1374 parent_commit=c.commit,
1374 1375 author=author,
1375 1376 )
1376 1377
1377 1378 h.flash(
1378 1379 _('Successfully committed new file `{}`').format(
1379 1380 h.escape(node_path)), category='success')
1380 1381 except NonRelativePathError:
1381 1382 log.exception('Non Relative path found')
1382 1383 h.flash(_(
1383 1384 'The location specified must be a relative path and must not '
1384 1385 'contain .. in the path'), category='warning')
1385 1386 raise HTTPFound(default_redirect_url)
1386 1387 except (NodeError, NodeAlreadyExistsError) as e:
1387 1388 h.flash(_(h.escape(e)), category='error')
1388 1389 except Exception:
1389 1390 log.exception('Error occurred during commit')
1390 1391 h.flash(_('Error occurred during commit'), category='error')
1391 1392
1392 1393 raise HTTPFound(default_redirect_url)
@@ -1,834 +1,832 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Scm model for RhodeCode
23 23 """
24 24
25 25 import os.path
26 26 import traceback
27 27 import logging
28 28 import cStringIO
29 29
30 30 from sqlalchemy import func
31 31 from zope.cachedescriptors.property import Lazy as LazyProperty
32 32
33 33 import rhodecode
34 34 from rhodecode.lib.vcs import get_backend
35 35 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37 from rhodecode.lib.vcs.backends.base import EmptyCommit
38 38 from rhodecode.lib import helpers as h, rc_cache
39 39 from rhodecode.lib.auth import (
40 40 HasRepoPermissionAny, HasRepoGroupPermissionAny,
41 41 HasUserGroupPermissionAny)
42 42 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
43 43 from rhodecode.lib import hooks_utils
44 44 from rhodecode.lib.utils import (
45 45 get_filesystem_repos, make_db_config)
46 46 from rhodecode.lib.utils2 import (safe_str, safe_unicode)
47 47 from rhodecode.lib.system_info import get_system_info
48 48 from rhodecode.model import BaseModel
49 49 from rhodecode.model.db import (
50 50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 51 PullRequest)
52 52 from rhodecode.model.settings import VcsSettingsModel
53 53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UserTemp(object):
59 59 def __init__(self, user_id):
60 60 self.user_id = user_id
61 61
62 62 def __repr__(self):
63 63 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
64 64
65 65
66 66 class RepoTemp(object):
67 67 def __init__(self, repo_id):
68 68 self.repo_id = repo_id
69 69
70 70 def __repr__(self):
71 71 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
72 72
73 73
74 74 class SimpleCachedRepoList(object):
75 75 """
76 76 Lighter version of of iteration of repos without the scm initialisation,
77 77 and with cache usage
78 78 """
79 79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 80 self.db_repo_list = db_repo_list
81 81 self.repos_path = repos_path
82 82 self.order_by = order_by
83 83 self.reversed = (order_by or '').startswith('-')
84 84 if not perm_set:
85 85 perm_set = ['repository.read', 'repository.write',
86 86 'repository.admin']
87 87 self.perm_set = perm_set
88 88
89 89 def __len__(self):
90 90 return len(self.db_repo_list)
91 91
92 92 def __repr__(self):
93 93 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
94 94
95 95 def __iter__(self):
96 96 for dbr in self.db_repo_list:
97 97 # check permission at this level
98 98 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 99 dbr.repo_name, 'SimpleCachedRepoList check')
100 100 if not has_perm:
101 101 continue
102 102
103 103 tmp_d = {
104 104 'name': dbr.repo_name,
105 105 'dbrepo': dbr.get_dict(),
106 106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 107 }
108 108 yield tmp_d
109 109
110 110
111 111 class _PermCheckIterator(object):
112 112
113 113 def __init__(
114 114 self, obj_list, obj_attr, perm_set, perm_checker,
115 115 extra_kwargs=None):
116 116 """
117 117 Creates iterator from given list of objects, additionally
118 118 checking permission for them from perm_set var
119 119
120 120 :param obj_list: list of db objects
121 121 :param obj_attr: attribute of object to pass into perm_checker
122 122 :param perm_set: list of permissions to check
123 123 :param perm_checker: callable to check permissions against
124 124 """
125 125 self.obj_list = obj_list
126 126 self.obj_attr = obj_attr
127 127 self.perm_set = perm_set
128 128 self.perm_checker = perm_checker
129 129 self.extra_kwargs = extra_kwargs or {}
130 130
131 131 def __len__(self):
132 132 return len(self.obj_list)
133 133
134 134 def __repr__(self):
135 135 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
136 136
137 137 def __iter__(self):
138 138 checker = self.perm_checker(*self.perm_set)
139 139 for db_obj in self.obj_list:
140 140 # check permission at this level
141 141 name = getattr(db_obj, self.obj_attr, None)
142 142 if not checker(name, self.__class__.__name__, **self.extra_kwargs):
143 143 continue
144 144
145 145 yield db_obj
146 146
147 147
148 148 class RepoList(_PermCheckIterator):
149 149
150 150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 151 if not perm_set:
152 152 perm_set = [
153 153 'repository.read', 'repository.write', 'repository.admin']
154 154
155 155 super(RepoList, self).__init__(
156 156 obj_list=db_repo_list,
157 157 obj_attr='repo_name', perm_set=perm_set,
158 158 perm_checker=HasRepoPermissionAny,
159 159 extra_kwargs=extra_kwargs)
160 160
161 161
162 162 class RepoGroupList(_PermCheckIterator):
163 163
164 164 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
165 165 if not perm_set:
166 166 perm_set = ['group.read', 'group.write', 'group.admin']
167 167
168 168 super(RepoGroupList, self).__init__(
169 169 obj_list=db_repo_group_list,
170 170 obj_attr='group_name', perm_set=perm_set,
171 171 perm_checker=HasRepoGroupPermissionAny,
172 172 extra_kwargs=extra_kwargs)
173 173
174 174
175 175 class UserGroupList(_PermCheckIterator):
176 176
177 177 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
178 178 if not perm_set:
179 179 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
180 180
181 181 super(UserGroupList, self).__init__(
182 182 obj_list=db_user_group_list,
183 183 obj_attr='users_group_name', perm_set=perm_set,
184 184 perm_checker=HasUserGroupPermissionAny,
185 185 extra_kwargs=extra_kwargs)
186 186
187 187
188 188 class ScmModel(BaseModel):
189 189 """
190 190 Generic Scm Model
191 191 """
192 192
193 193 @LazyProperty
194 194 def repos_path(self):
195 195 """
196 196 Gets the repositories root path from database
197 197 """
198 198
199 199 settings_model = VcsSettingsModel(sa=self.sa)
200 200 return settings_model.get_repos_location()
201 201
202 202 def repo_scan(self, repos_path=None):
203 203 """
204 204 Listing of repositories in given path. This path should not be a
205 205 repository itself. Return a dictionary of repository objects
206 206
207 207 :param repos_path: path to directory containing repositories
208 208 """
209 209
210 210 if repos_path is None:
211 211 repos_path = self.repos_path
212 212
213 213 log.info('scanning for repositories in %s', repos_path)
214 214
215 215 config = make_db_config()
216 216 config.set('extensions', 'largefiles', '')
217 217 repos = {}
218 218
219 219 for name, path in get_filesystem_repos(repos_path, recursive=True):
220 220 # name need to be decomposed and put back together using the /
221 221 # since this is internal storage separator for rhodecode
222 222 name = Repository.normalize_repo_name(name)
223 223
224 224 try:
225 225 if name in repos:
226 226 raise RepositoryError('Duplicate repository name %s '
227 227 'found in %s' % (name, path))
228 228 elif path[0] in rhodecode.BACKENDS:
229 229 klass = get_backend(path[0])
230 230 repos[name] = klass(path[1], config=config)
231 231 except OSError:
232 232 continue
233 233 log.debug('found %s paths with repositories', len(repos))
234 234 return repos
235 235
236 236 def get_repos(self, all_repos=None, sort_key=None):
237 237 """
238 238 Get all repositories from db and for each repo create it's
239 239 backend instance and fill that backed with information from database
240 240
241 241 :param all_repos: list of repository names as strings
242 242 give specific repositories list, good for filtering
243 243
244 244 :param sort_key: initial sorting of repositories
245 245 """
246 246 if all_repos is None:
247 247 all_repos = self.sa.query(Repository)\
248 248 .filter(Repository.group_id == None)\
249 249 .order_by(func.lower(Repository.repo_name)).all()
250 250 repo_iter = SimpleCachedRepoList(
251 251 all_repos, repos_path=self.repos_path, order_by=sort_key)
252 252 return repo_iter
253 253
254 254 def get_repo_groups(self, all_groups=None):
255 255 if all_groups is None:
256 256 all_groups = RepoGroup.query()\
257 257 .filter(RepoGroup.group_parent_id == None).all()
258 258 return [x for x in RepoGroupList(all_groups)]
259 259
260 260 def mark_for_invalidation(self, repo_name, delete=False):
261 261 """
262 262 Mark caches of this repo invalid in the database. `delete` flag
263 263 removes the cache entries
264 264
265 265 :param repo_name: the repo_name for which caches should be marked
266 266 invalid, or deleted
267 267 :param delete: delete the entry keys instead of setting bool
268 268 flag on them, and also purge caches used by the dogpile
269 269 """
270 270 repo = Repository.get_by_repo_name(repo_name)
271 271
272 272 if repo:
273 273 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
274 274 repo_id=repo.repo_id)
275 275 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
276 276
277 277 repo_id = repo.repo_id
278 278 config = repo._config
279 279 config.set('extensions', 'largefiles', '')
280 280 repo.update_commit_cache(config=config, cs_cache=None)
281 281 if delete:
282 282 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
283 283 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
284 284
285 285 def toggle_following_repo(self, follow_repo_id, user_id):
286 286
287 287 f = self.sa.query(UserFollowing)\
288 288 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
289 289 .filter(UserFollowing.user_id == user_id).scalar()
290 290
291 291 if f is not None:
292 292 try:
293 293 self.sa.delete(f)
294 294 return
295 295 except Exception:
296 296 log.error(traceback.format_exc())
297 297 raise
298 298
299 299 try:
300 300 f = UserFollowing()
301 301 f.user_id = user_id
302 302 f.follows_repo_id = follow_repo_id
303 303 self.sa.add(f)
304 304 except Exception:
305 305 log.error(traceback.format_exc())
306 306 raise
307 307
308 308 def toggle_following_user(self, follow_user_id, user_id):
309 309 f = self.sa.query(UserFollowing)\
310 310 .filter(UserFollowing.follows_user_id == follow_user_id)\
311 311 .filter(UserFollowing.user_id == user_id).scalar()
312 312
313 313 if f is not None:
314 314 try:
315 315 self.sa.delete(f)
316 316 return
317 317 except Exception:
318 318 log.error(traceback.format_exc())
319 319 raise
320 320
321 321 try:
322 322 f = UserFollowing()
323 323 f.user_id = user_id
324 324 f.follows_user_id = follow_user_id
325 325 self.sa.add(f)
326 326 except Exception:
327 327 log.error(traceback.format_exc())
328 328 raise
329 329
330 330 def is_following_repo(self, repo_name, user_id, cache=False):
331 331 r = self.sa.query(Repository)\
332 332 .filter(Repository.repo_name == repo_name).scalar()
333 333
334 334 f = self.sa.query(UserFollowing)\
335 335 .filter(UserFollowing.follows_repository == r)\
336 336 .filter(UserFollowing.user_id == user_id).scalar()
337 337
338 338 return f is not None
339 339
340 340 def is_following_user(self, username, user_id, cache=False):
341 341 u = User.get_by_username(username)
342 342
343 343 f = self.sa.query(UserFollowing)\
344 344 .filter(UserFollowing.follows_user == u)\
345 345 .filter(UserFollowing.user_id == user_id).scalar()
346 346
347 347 return f is not None
348 348
349 349 def get_followers(self, repo):
350 350 repo = self._get_repo(repo)
351 351
352 352 return self.sa.query(UserFollowing)\
353 353 .filter(UserFollowing.follows_repository == repo).count()
354 354
355 355 def get_forks(self, repo):
356 356 repo = self._get_repo(repo)
357 357 return self.sa.query(Repository)\
358 358 .filter(Repository.fork == repo).count()
359 359
360 360 def get_pull_requests(self, repo):
361 361 repo = self._get_repo(repo)
362 362 return self.sa.query(PullRequest)\
363 363 .filter(PullRequest.target_repo == repo)\
364 364 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
365 365
366 366 def mark_as_fork(self, repo, fork, user):
367 367 repo = self._get_repo(repo)
368 368 fork = self._get_repo(fork)
369 369 if fork and repo.repo_id == fork.repo_id:
370 370 raise Exception("Cannot set repository as fork of itself")
371 371
372 372 if fork and repo.repo_type != fork.repo_type:
373 373 raise RepositoryError(
374 374 "Cannot set repository as fork of repository with other type")
375 375
376 376 repo.fork = fork
377 377 self.sa.add(repo)
378 378 return repo
379 379
380 380 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
381 381 dbrepo = self._get_repo(repo)
382 382 remote_uri = remote_uri or dbrepo.clone_uri
383 383 if not remote_uri:
384 384 raise Exception("This repository doesn't have a clone uri")
385 385
386 386 repo = dbrepo.scm_instance(cache=False)
387 387 repo.config.clear_section('hooks')
388 388
389 389 try:
390 390 # NOTE(marcink): add extra validation so we skip invalid urls
391 391 # this is due this tasks can be executed via scheduler without
392 392 # proper validation of remote_uri
393 393 if validate_uri:
394 394 config = make_db_config(clear_session=False)
395 395 url_validator(remote_uri, dbrepo.repo_type, config)
396 396 except InvalidCloneUrl:
397 397 raise
398 398
399 399 repo_name = dbrepo.repo_name
400 400 try:
401 401 # TODO: we need to make sure those operations call proper hooks !
402 402 repo.fetch(remote_uri)
403 403
404 404 self.mark_for_invalidation(repo_name)
405 405 except Exception:
406 406 log.error(traceback.format_exc())
407 407 raise
408 408
409 409 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
410 410 dbrepo = self._get_repo(repo)
411 411 remote_uri = remote_uri or dbrepo.push_uri
412 412 if not remote_uri:
413 413 raise Exception("This repository doesn't have a clone uri")
414 414
415 415 repo = dbrepo.scm_instance(cache=False)
416 416 repo.config.clear_section('hooks')
417 417
418 418 try:
419 419 # NOTE(marcink): add extra validation so we skip invalid urls
420 420 # this is due this tasks can be executed via scheduler without
421 421 # proper validation of remote_uri
422 422 if validate_uri:
423 423 config = make_db_config(clear_session=False)
424 424 url_validator(remote_uri, dbrepo.repo_type, config)
425 425 except InvalidCloneUrl:
426 426 raise
427 427
428 428 try:
429 429 repo.push(remote_uri)
430 430 except Exception:
431 431 log.error(traceback.format_exc())
432 432 raise
433 433
434 434 def commit_change(self, repo, repo_name, commit, user, author, message,
435 435 content, f_path):
436 436 """
437 437 Commits changes
438 438
439 439 :param repo: SCM instance
440 440
441 441 """
442 442 user = self._get_user(user)
443 443
444 444 # decoding here will force that we have proper encoded values
445 445 # in any other case this will throw exceptions and deny commit
446 446 content = safe_str(content)
447 447 path = safe_str(f_path)
448 448 # message and author needs to be unicode
449 449 # proper backend should then translate that into required type
450 450 message = safe_unicode(message)
451 451 author = safe_unicode(author)
452 452 imc = repo.in_memory_commit
453 453 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
454 454 try:
455 455 # TODO: handle pre-push action !
456 456 tip = imc.commit(
457 457 message=message, author=author, parents=[commit],
458 458 branch=commit.branch)
459 459 except Exception as e:
460 460 log.error(traceback.format_exc())
461 461 raise IMCCommitError(str(e))
462 462 finally:
463 463 # always clear caches, if commit fails we want fresh object also
464 464 self.mark_for_invalidation(repo_name)
465 465
466 466 # We trigger the post-push action
467 467 hooks_utils.trigger_post_push_hook(
468 468 username=user.username, action='push_local', hook_type='post_push',
469 469 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
470 470 return tip
471 471
472 472 def _sanitize_path(self, f_path):
473 473 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
474 474 raise NonRelativePathError('%s is not an relative path' % f_path)
475 475 if f_path:
476 476 f_path = os.path.normpath(f_path)
477 477 return f_path
478 478
479 479 def get_dirnode_metadata(self, request, commit, dir_node):
480 480 if not dir_node.is_dir():
481 481 return []
482 482
483 483 data = []
484 484 for node in dir_node:
485 485 if not node.is_file():
486 486 # we skip file-nodes
487 487 continue
488 488
489 489 last_commit = node.last_commit
490 490 last_commit_date = last_commit.date
491 491 data.append({
492 492 'name': node.name,
493 493 'size': h.format_byte_size_binary(node.size),
494 494 'modified_at': h.format_date(last_commit_date),
495 495 'modified_ts': last_commit_date.isoformat(),
496 496 'revision': last_commit.revision,
497 497 'short_id': last_commit.short_id,
498 498 'message': h.escape(last_commit.message),
499 499 'author': h.escape(last_commit.author),
500 500 'user_profile': h.gravatar_with_user(
501 501 request, last_commit.author),
502 502 })
503 503
504 504 return data
505 505
506 506 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
507 507 extended_info=False, content=False, max_file_bytes=None):
508 508 """
509 509 recursive walk in root dir and return a set of all path in that dir
510 510 based on repository walk function
511 511
512 512 :param repo_name: name of repository
513 513 :param commit_id: commit id for which to list nodes
514 514 :param root_path: root path to list
515 515 :param flat: return as a list, if False returns a dict with description
516 516 :param max_file_bytes: will not return file contents over this limit
517 517
518 518 """
519 519 _files = list()
520 520 _dirs = list()
521 521 try:
522 522 _repo = self._get_repo(repo_name)
523 523 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
524 524 root_path = root_path.lstrip('/')
525 525 for __, dirs, files in commit.walk(root_path):
526 526 for f in files:
527 527 _content = None
528 528 _data = f.unicode_path
529 529 over_size_limit = (max_file_bytes is not None
530 530 and f.size > max_file_bytes)
531 531
532 532 if not flat:
533 533 _data = {
534 534 "name": h.escape(f.unicode_path),
535 535 "type": "file",
536 536 }
537 537 if extended_info:
538 538 _data.update({
539 539 "md5": f.md5,
540 540 "binary": f.is_binary,
541 541 "size": f.size,
542 542 "extension": f.extension,
543 543 "mimetype": f.mimetype,
544 544 "lines": f.lines()[0]
545 545 })
546 546
547 547 if content:
548 548 full_content = None
549 549 if not f.is_binary and not over_size_limit:
550 550 full_content = safe_str(f.content)
551 551
552 552 _data.update({
553 553 "content": full_content,
554 554 })
555 555 _files.append(_data)
556 556 for d in dirs:
557 557 _data = d.unicode_path
558 558 if not flat:
559 559 _data = {
560 560 "name": h.escape(d.unicode_path),
561 561 "type": "dir",
562 562 }
563 563 if extended_info:
564 564 _data.update({
565 565 "md5": None,
566 566 "binary": None,
567 567 "size": None,
568 568 "extension": None,
569 569 })
570 570 if content:
571 571 _data.update({
572 572 "content": None
573 573 })
574 574 _dirs.append(_data)
575 575 except RepositoryError:
576 576 log.debug("Exception in get_nodes", exc_info=True)
577 577 raise
578 578
579 579 return _dirs, _files
580 580
581 581 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
582 582 author=None, trigger_push_hook=True):
583 583 """
584 584 Commits given multiple nodes into repo
585 585
586 586 :param user: RhodeCode User object or user_id, the commiter
587 587 :param repo: RhodeCode Repository object
588 588 :param message: commit message
589 589 :param nodes: mapping {filename:{'content':content},...}
590 590 :param parent_commit: parent commit, can be empty than it's
591 591 initial commit
592 592 :param author: author of commit, cna be different that commiter
593 593 only for git
594 594 :param trigger_push_hook: trigger push hooks
595 595
596 596 :returns: new commited commit
597 597 """
598 598
599 599 user = self._get_user(user)
600 600 scm_instance = repo.scm_instance(cache=False)
601 601
602 602 processed_nodes = []
603 603 for f_path in nodes:
604 604 f_path = self._sanitize_path(f_path)
605 605 content = nodes[f_path]['content']
606 606 f_path = safe_str(f_path)
607 607 # decoding here will force that we have proper encoded values
608 608 # in any other case this will throw exceptions and deny commit
609 609 if isinstance(content, (basestring,)):
610 610 content = safe_str(content)
611 611 elif isinstance(content, (file, cStringIO.OutputType,)):
612 612 content = content.read()
613 613 else:
614 614 raise Exception('Content is of unrecognized type %s' % (
615 615 type(content)
616 616 ))
617 617 processed_nodes.append((f_path, content))
618 618
619 619 message = safe_unicode(message)
620 620 commiter = user.full_contact
621 621 author = safe_unicode(author) if author else commiter
622 622
623 623 imc = scm_instance.in_memory_commit
624 624
625 625 if not parent_commit:
626 626 parent_commit = EmptyCommit(alias=scm_instance.alias)
627 627
628 628 if isinstance(parent_commit, EmptyCommit):
629 629 # EmptyCommit means we we're editing empty repository
630 630 parents = None
631 631 else:
632 632 parents = [parent_commit]
633 633 # add multiple nodes
634 634 for path, content in processed_nodes:
635 635 imc.add(FileNode(path, content=content))
636 636 # TODO: handle pre push scenario
637 637 tip = imc.commit(message=message,
638 638 author=author,
639 639 parents=parents,
640 640 branch=parent_commit.branch)
641 641
642 642 self.mark_for_invalidation(repo.repo_name)
643 643 if trigger_push_hook:
644 644 hooks_utils.trigger_post_push_hook(
645 645 username=user.username, action='push_local',
646 646 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
647 647 hook_type='post_push',
648 648 commit_ids=[tip.raw_id])
649 649 return tip
650 650
651 651 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
652 652 author=None, trigger_push_hook=True):
653 653 user = self._get_user(user)
654 654 scm_instance = repo.scm_instance(cache=False)
655 655
656 656 message = safe_unicode(message)
657 657 commiter = user.full_contact
658 658 author = safe_unicode(author) if author else commiter
659 659
660 660 imc = scm_instance.in_memory_commit
661 661
662 662 if not parent_commit:
663 663 parent_commit = EmptyCommit(alias=scm_instance.alias)
664 664
665 665 if isinstance(parent_commit, EmptyCommit):
666 666 # EmptyCommit means we we're editing empty repository
667 667 parents = None
668 668 else:
669 669 parents = [parent_commit]
670 670
671 671 # add multiple nodes
672 672 for _filename, data in nodes.items():
673 673 # new filename, can be renamed from the old one, also sanitaze
674 674 # the path for any hack around relative paths like ../../ etc.
675 675 filename = self._sanitize_path(data['filename'])
676 676 old_filename = self._sanitize_path(_filename)
677 677 content = data['content']
678
679 filenode = FileNode(old_filename, content=content)
678 file_mode = data.get('mode')
679 filenode = FileNode(old_filename, content=content, mode=file_mode)
680 680 op = data['op']
681 681 if op == 'add':
682 682 imc.add(filenode)
683 683 elif op == 'del':
684 684 imc.remove(filenode)
685 685 elif op == 'mod':
686 686 if filename != old_filename:
687 # TODO: handle renames more efficient, needs vcs lib
688 # changes
687 # TODO: handle renames more efficient, needs vcs lib changes
689 688 imc.remove(filenode)
690 imc.add(FileNode(filename, content=content))
689 imc.add(FileNode(filename, content=content, mode=file_mode))
691 690 else:
692 691 imc.change(filenode)
693 692
694 693 try:
695 # TODO: handle pre push scenario
696 # commit changes
694 # TODO: handle pre push scenario commit changes
697 695 tip = imc.commit(message=message,
698 696 author=author,
699 697 parents=parents,
700 698 branch=parent_commit.branch)
701 699 except NodeNotChangedError:
702 700 raise
703 701 except Exception as e:
704 702 log.exception("Unexpected exception during call to imc.commit")
705 703 raise IMCCommitError(str(e))
706 704 finally:
707 705 # always clear caches, if commit fails we want fresh object also
708 706 self.mark_for_invalidation(repo.repo_name)
709 707
710 708 if trigger_push_hook:
711 709 hooks_utils.trigger_post_push_hook(
712 710 username=user.username, action='push_local', hook_type='post_push',
713 711 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
714 712 commit_ids=[tip.raw_id])
715 713
716 714 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
717 715 author=None, trigger_push_hook=True):
718 716 """
719 717 Deletes given multiple nodes into `repo`
720 718
721 719 :param user: RhodeCode User object or user_id, the committer
722 720 :param repo: RhodeCode Repository object
723 721 :param message: commit message
724 722 :param nodes: mapping {filename:{'content':content},...}
725 723 :param parent_commit: parent commit, can be empty than it's initial
726 724 commit
727 725 :param author: author of commit, cna be different that commiter only
728 726 for git
729 727 :param trigger_push_hook: trigger push hooks
730 728
731 729 :returns: new commit after deletion
732 730 """
733 731
734 732 user = self._get_user(user)
735 733 scm_instance = repo.scm_instance(cache=False)
736 734
737 735 processed_nodes = []
738 736 for f_path in nodes:
739 737 f_path = self._sanitize_path(f_path)
740 738 # content can be empty but for compatabilty it allows same dicts
741 739 # structure as add_nodes
742 740 content = nodes[f_path].get('content')
743 741 processed_nodes.append((f_path, content))
744 742
745 743 message = safe_unicode(message)
746 744 commiter = user.full_contact
747 745 author = safe_unicode(author) if author else commiter
748 746
749 747 imc = scm_instance.in_memory_commit
750 748
751 749 if not parent_commit:
752 750 parent_commit = EmptyCommit(alias=scm_instance.alias)
753 751
754 752 if isinstance(parent_commit, EmptyCommit):
755 753 # EmptyCommit means we we're editing empty repository
756 754 parents = None
757 755 else:
758 756 parents = [parent_commit]
759 757 # add multiple nodes
760 758 for path, content in processed_nodes:
761 759 imc.remove(FileNode(path, content=content))
762 760
763 761 # TODO: handle pre push scenario
764 762 tip = imc.commit(message=message,
765 763 author=author,
766 764 parents=parents,
767 765 branch=parent_commit.branch)
768 766
769 767 self.mark_for_invalidation(repo.repo_name)
770 768 if trigger_push_hook:
771 769 hooks_utils.trigger_post_push_hook(
772 770 username=user.username, action='push_local', hook_type='post_push',
773 771 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
774 772 commit_ids=[tip.raw_id])
775 773 return tip
776 774
777 775 def strip(self, repo, commit_id, branch):
778 776 scm_instance = repo.scm_instance(cache=False)
779 777 scm_instance.config.clear_section('hooks')
780 778 scm_instance.strip(commit_id, branch)
781 779 self.mark_for_invalidation(repo.repo_name)
782 780
783 781 def get_unread_journal(self):
784 782 return self.sa.query(UserLog).count()
785 783
786 784 def get_repo_landing_revs(self, translator, repo=None):
787 785 """
788 786 Generates select option with tags branches and bookmarks (for hg only)
789 787 grouped by type
790 788
791 789 :param repo:
792 790 """
793 791 _ = translator
794 792 repo = self._get_repo(repo)
795 793
796 794 hist_l = [
797 795 ['rev:tip', _('latest tip')]
798 796 ]
799 797 choices = [
800 798 'rev:tip'
801 799 ]
802 800
803 801 if not repo:
804 802 return choices, hist_l
805 803
806 804 repo = repo.scm_instance()
807 805
808 806 branches_group = (
809 807 [(u'branch:%s' % safe_unicode(b), safe_unicode(b))
810 808 for b in repo.branches],
811 809 _("Branches"))
812 810 hist_l.append(branches_group)
813 811 choices.extend([x[0] for x in branches_group[0]])
814 812
815 813 if repo.alias == 'hg':
816 814 bookmarks_group = (
817 815 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
818 816 for b in repo.bookmarks],
819 817 _("Bookmarks"))
820 818 hist_l.append(bookmarks_group)
821 819 choices.extend([x[0] for x in bookmarks_group[0]])
822 820
823 821 tags_group = (
824 822 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
825 823 for t in repo.tags],
826 824 _("Tags"))
827 825 hist_l.append(tags_group)
828 826 choices.extend([x[0] for x in tags_group[0]])
829 827
830 828 return choices, hist_l
831 829
832 830 def get_server_info(self, environ=None):
833 831 server_info = get_system_info(environ)
834 832 return server_info
General Comments 0
You need to be logged in to leave comments. Login now