##// END OF EJS Templates
quick-filter: use a dedicated method for fetching quick filter nodes....
marcink -
r3925:66f1ac00 default
parent child Browse files
Show More
@@ -1,1552 +1,1551 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 28 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 branch_name, rule)
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_rev[1]
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError):
185 185 msg = _('No such commit exists for this repository')
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(safe_str(h.escape(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 214 def _is_valid_head(self, commit_id, repo):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 240 if not repo.is_empty():
241 241 commit = repo.get_commit(commit_id=commit_id)
242 242 if commit:
243 243 branch_name = commit.branch
244 244 sha_commit_id = commit.raw_id
245 245
246 246 return branch_name, sha_commit_id, is_head
247 247
248 248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False):
249 249
250 250 repo_id = self.db_repo.repo_id
251 251 force_recache = self.get_recache_flag()
252 252
253 253 cache_seconds = safe_int(
254 254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 255 cache_on = not force_recache and cache_seconds > 0
256 256 log.debug(
257 257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 258 'with caching: %s[TTL: %ss]' % (
259 259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260 260
261 261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 263
264 264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
265 265 condition=cache_on)
266 266 def compute_file_tree(ver, repo_id, commit_id, f_path, full_load):
267 267 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
268 268 ver, repo_id, commit_id, f_path)
269 269
270 270 c.full_load = full_load
271 271 return render(
272 272 'rhodecode:templates/files/files_browser_tree.mako',
273 273 self._get_template_context(c), self.request)
274 274
275 275 return compute_file_tree('v1', self.db_repo.repo_id, commit_id, f_path, full_load)
276 276
277 277 def _get_archive_spec(self, fname):
278 278 log.debug('Detecting archive spec for: `%s`', fname)
279 279
280 280 fileformat = None
281 281 ext = None
282 282 content_type = None
283 283 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
284 284
285 285 if fname.endswith(extension):
286 286 fileformat = a_type
287 287 log.debug('archive is of type: %s', fileformat)
288 288 ext = extension
289 289 break
290 290
291 291 if not fileformat:
292 292 raise ValueError()
293 293
294 294 # left over part of whole fname is the commit
295 295 commit_id = fname[:-len(ext)]
296 296
297 297 return commit_id, ext, fileformat, content_type
298 298
299 299 def create_pure_path(self, *parts):
300 300 # Split paths and sanitize them, removing any ../ etc
301 301 sanitized_path = [
302 302 x for x in pathlib2.PurePath(*parts).parts
303 303 if x not in ['.', '..']]
304 304
305 305 pure_path = pathlib2.PurePath(*sanitized_path)
306 306 return pure_path
307 307
308 308 def _is_lf_enabled(self, target_repo):
309 309 lf_enabled = False
310 310
311 311 lf_key_for_vcs_map = {
312 312 'hg': 'extensions_largefiles',
313 313 'git': 'vcs_git_lfs_enabled'
314 314 }
315 315
316 316 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
317 317
318 318 if lf_key_for_vcs:
319 319 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
320 320
321 321 return lf_enabled
322 322
323 323 @LoginRequired()
324 324 @HasRepoPermissionAnyDecorator(
325 325 'repository.read', 'repository.write', 'repository.admin')
326 326 @view_config(
327 327 route_name='repo_archivefile', request_method='GET',
328 328 renderer=None)
329 329 def repo_archivefile(self):
330 330 # archive cache config
331 331 from rhodecode import CONFIG
332 332 _ = self.request.translate
333 333 self.load_default_context()
334 334 default_at_path = '/'
335 335 fname = self.request.matchdict['fname']
336 336 subrepos = self.request.GET.get('subrepos') == 'true'
337 337 at_path = self.request.GET.get('at_path') or default_at_path
338 338
339 339 if not self.db_repo.enable_downloads:
340 340 return Response(_('Downloads disabled'))
341 341
342 342 try:
343 343 commit_id, ext, fileformat, content_type = \
344 344 self._get_archive_spec(fname)
345 345 except ValueError:
346 346 return Response(_('Unknown archive type for: `{}`').format(
347 347 h.escape(fname)))
348 348
349 349 try:
350 350 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
351 351 except CommitDoesNotExistError:
352 352 return Response(_('Unknown commit_id {}').format(
353 353 h.escape(commit_id)))
354 354 except EmptyRepositoryError:
355 355 return Response(_('Empty repository'))
356 356
357 357 try:
358 358 at_path = commit.get_node(at_path).path or default_at_path
359 359 except Exception:
360 360 return Response(_('No node at path {} for this repository').format(at_path))
361 361
362 362 path_sha = sha1(at_path)[:8]
363 363
364 364 # original backward compat name of archive
365 365 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
366 366 short_sha = safe_str(commit.short_id)
367 367
368 368 if at_path == default_at_path:
369 369 archive_name = '{}-{}{}{}'.format(
370 370 clean_name,
371 371 '-sub' if subrepos else '',
372 372 short_sha,
373 373 ext)
374 374 # custom path and new name
375 375 else:
376 376 archive_name = '{}-{}{}-{}{}'.format(
377 377 clean_name,
378 378 '-sub' if subrepos else '',
379 379 short_sha,
380 380 path_sha,
381 381 ext)
382 382
383 383 use_cached_archive = False
384 384 archive_cache_enabled = CONFIG.get(
385 385 'archive_cache_dir') and not self.request.GET.get('no_cache')
386 386 cached_archive_path = None
387 387
388 388 if archive_cache_enabled:
389 389 # check if we it's ok to write
390 390 if not os.path.isdir(CONFIG['archive_cache_dir']):
391 391 os.makedirs(CONFIG['archive_cache_dir'])
392 392 cached_archive_path = os.path.join(
393 393 CONFIG['archive_cache_dir'], archive_name)
394 394 if os.path.isfile(cached_archive_path):
395 395 log.debug('Found cached archive in %s', cached_archive_path)
396 396 fd, archive = None, cached_archive_path
397 397 use_cached_archive = True
398 398 else:
399 399 log.debug('Archive %s is not yet cached', archive_name)
400 400
401 401 if not use_cached_archive:
402 402 # generate new archive
403 403 fd, archive = tempfile.mkstemp()
404 404 log.debug('Creating new temp archive in %s', archive)
405 405 try:
406 406 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
407 407 archive_at_path=at_path)
408 408 except ImproperArchiveTypeError:
409 409 return _('Unknown archive type')
410 410 if archive_cache_enabled:
411 411 # if we generated the archive and we have cache enabled
412 412 # let's use this for future
413 413 log.debug('Storing new archive in %s', cached_archive_path)
414 414 shutil.move(archive, cached_archive_path)
415 415 archive = cached_archive_path
416 416
417 417 # store download action
418 418 audit_logger.store_web(
419 419 'repo.archive.download', action_data={
420 420 'user_agent': self.request.user_agent,
421 421 'archive_name': archive_name,
422 422 'archive_spec': fname,
423 423 'archive_cached': use_cached_archive},
424 424 user=self._rhodecode_user,
425 425 repo=self.db_repo,
426 426 commit=True
427 427 )
428 428
429 429 def get_chunked_archive(archive_path):
430 430 with open(archive_path, 'rb') as stream:
431 431 while True:
432 432 data = stream.read(16 * 1024)
433 433 if not data:
434 434 if fd: # fd means we used temporary file
435 435 os.close(fd)
436 436 if not archive_cache_enabled:
437 437 log.debug('Destroying temp archive %s', archive_path)
438 438 os.remove(archive_path)
439 439 break
440 440 yield data
441 441
442 442 response = Response(app_iter=get_chunked_archive(archive))
443 443 response.content_disposition = str(
444 444 'attachment; filename=%s' % archive_name)
445 445 response.content_type = str(content_type)
446 446
447 447 return response
448 448
449 449 def _get_file_node(self, commit_id, f_path):
450 450 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
451 451 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
452 452 try:
453 453 node = commit.get_node(f_path)
454 454 if node.is_dir():
455 455 raise NodeError('%s path is a %s not a file'
456 456 % (node, type(node)))
457 457 except NodeDoesNotExistError:
458 458 commit = EmptyCommit(
459 459 commit_id=commit_id,
460 460 idx=commit.idx,
461 461 repo=commit.repository,
462 462 alias=commit.repository.alias,
463 463 message=commit.message,
464 464 author=commit.author,
465 465 date=commit.date)
466 466 node = FileNode(f_path, '', commit=commit)
467 467 else:
468 468 commit = EmptyCommit(
469 469 repo=self.rhodecode_vcs_repo,
470 470 alias=self.rhodecode_vcs_repo.alias)
471 471 node = FileNode(f_path, '', commit=commit)
472 472 return node
473 473
474 474 @LoginRequired()
475 475 @HasRepoPermissionAnyDecorator(
476 476 'repository.read', 'repository.write', 'repository.admin')
477 477 @view_config(
478 478 route_name='repo_files_diff', request_method='GET',
479 479 renderer=None)
480 480 def repo_files_diff(self):
481 481 c = self.load_default_context()
482 482 f_path = self._get_f_path(self.request.matchdict)
483 483 diff1 = self.request.GET.get('diff1', '')
484 484 diff2 = self.request.GET.get('diff2', '')
485 485
486 486 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
487 487
488 488 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
489 489 line_context = self.request.GET.get('context', 3)
490 490
491 491 if not any((diff1, diff2)):
492 492 h.flash(
493 493 'Need query parameter "diff1" or "diff2" to generate a diff.',
494 494 category='error')
495 495 raise HTTPBadRequest()
496 496
497 497 c.action = self.request.GET.get('diff')
498 498 if c.action not in ['download', 'raw']:
499 499 compare_url = h.route_path(
500 500 'repo_compare',
501 501 repo_name=self.db_repo_name,
502 502 source_ref_type='rev',
503 503 source_ref=diff1,
504 504 target_repo=self.db_repo_name,
505 505 target_ref_type='rev',
506 506 target_ref=diff2,
507 507 _query=dict(f_path=f_path))
508 508 # redirect to new view if we render diff
509 509 raise HTTPFound(compare_url)
510 510
511 511 try:
512 512 node1 = self._get_file_node(diff1, path1)
513 513 node2 = self._get_file_node(diff2, f_path)
514 514 except (RepositoryError, NodeError):
515 515 log.exception("Exception while trying to get node from repository")
516 516 raise HTTPFound(
517 517 h.route_path('repo_files', repo_name=self.db_repo_name,
518 518 commit_id='tip', f_path=f_path))
519 519
520 520 if all(isinstance(node.commit, EmptyCommit)
521 521 for node in (node1, node2)):
522 522 raise HTTPNotFound()
523 523
524 524 c.commit_1 = node1.commit
525 525 c.commit_2 = node2.commit
526 526
527 527 if c.action == 'download':
528 528 _diff = diffs.get_gitdiff(node1, node2,
529 529 ignore_whitespace=ignore_whitespace,
530 530 context=line_context)
531 531 diff = diffs.DiffProcessor(_diff, format='gitdiff')
532 532
533 533 response = Response(self.path_filter.get_raw_patch(diff))
534 534 response.content_type = 'text/plain'
535 535 response.content_disposition = (
536 536 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
537 537 )
538 538 charset = self._get_default_encoding(c)
539 539 if charset:
540 540 response.charset = charset
541 541 return response
542 542
543 543 elif c.action == 'raw':
544 544 _diff = diffs.get_gitdiff(node1, node2,
545 545 ignore_whitespace=ignore_whitespace,
546 546 context=line_context)
547 547 diff = diffs.DiffProcessor(_diff, format='gitdiff')
548 548
549 549 response = Response(self.path_filter.get_raw_patch(diff))
550 550 response.content_type = 'text/plain'
551 551 charset = self._get_default_encoding(c)
552 552 if charset:
553 553 response.charset = charset
554 554 return response
555 555
556 556 # in case we ever end up here
557 557 raise HTTPNotFound()
558 558
559 559 @LoginRequired()
560 560 @HasRepoPermissionAnyDecorator(
561 561 'repository.read', 'repository.write', 'repository.admin')
562 562 @view_config(
563 563 route_name='repo_files_diff_2way_redirect', request_method='GET',
564 564 renderer=None)
565 565 def repo_files_diff_2way_redirect(self):
566 566 """
567 567 Kept only to make OLD links work
568 568 """
569 569 f_path = self._get_f_path_unchecked(self.request.matchdict)
570 570 diff1 = self.request.GET.get('diff1', '')
571 571 diff2 = self.request.GET.get('diff2', '')
572 572
573 573 if not any((diff1, diff2)):
574 574 h.flash(
575 575 'Need query parameter "diff1" or "diff2" to generate a diff.',
576 576 category='error')
577 577 raise HTTPBadRequest()
578 578
579 579 compare_url = h.route_path(
580 580 'repo_compare',
581 581 repo_name=self.db_repo_name,
582 582 source_ref_type='rev',
583 583 source_ref=diff1,
584 584 target_ref_type='rev',
585 585 target_ref=diff2,
586 586 _query=dict(f_path=f_path, diffmode='sideside',
587 587 target_repo=self.db_repo_name,))
588 588 raise HTTPFound(compare_url)
589 589
590 590 @LoginRequired()
591 591 @HasRepoPermissionAnyDecorator(
592 592 'repository.read', 'repository.write', 'repository.admin')
593 593 @view_config(
594 594 route_name='repo_files', request_method='GET',
595 595 renderer=None)
596 596 @view_config(
597 597 route_name='repo_files:default_path', request_method='GET',
598 598 renderer=None)
599 599 @view_config(
600 600 route_name='repo_files:default_commit', request_method='GET',
601 601 renderer=None)
602 602 @view_config(
603 603 route_name='repo_files:rendered', request_method='GET',
604 604 renderer=None)
605 605 @view_config(
606 606 route_name='repo_files:annotated', request_method='GET',
607 607 renderer=None)
608 608 def repo_files(self):
609 609 c = self.load_default_context()
610 610
611 611 view_name = getattr(self.request.matched_route, 'name', None)
612 612
613 613 c.annotate = view_name == 'repo_files:annotated'
614 614 # default is false, but .rst/.md files later are auto rendered, we can
615 615 # overwrite auto rendering by setting this GET flag
616 616 c.renderer = view_name == 'repo_files:rendered' or \
617 617 not self.request.GET.get('no-render', False)
618 618
619 619 # redirect to given commit_id from form if given
620 620 get_commit_id = self.request.GET.get('at_rev', None)
621 621 if get_commit_id:
622 622 self._get_commit_or_redirect(get_commit_id)
623 623
624 624 commit_id, f_path = self._get_commit_and_path()
625 625 c.commit = self._get_commit_or_redirect(commit_id)
626 626 c.branch = self.request.GET.get('branch', None)
627 627 c.f_path = f_path
628 628
629 629 # prev link
630 630 try:
631 631 prev_commit = c.commit.prev(c.branch)
632 632 c.prev_commit = prev_commit
633 633 c.url_prev = h.route_path(
634 634 'repo_files', repo_name=self.db_repo_name,
635 635 commit_id=prev_commit.raw_id, f_path=f_path)
636 636 if c.branch:
637 637 c.url_prev += '?branch=%s' % c.branch
638 638 except (CommitDoesNotExistError, VCSError):
639 639 c.url_prev = '#'
640 640 c.prev_commit = EmptyCommit()
641 641
642 642 # next link
643 643 try:
644 644 next_commit = c.commit.next(c.branch)
645 645 c.next_commit = next_commit
646 646 c.url_next = h.route_path(
647 647 'repo_files', repo_name=self.db_repo_name,
648 648 commit_id=next_commit.raw_id, f_path=f_path)
649 649 if c.branch:
650 650 c.url_next += '?branch=%s' % c.branch
651 651 except (CommitDoesNotExistError, VCSError):
652 652 c.url_next = '#'
653 653 c.next_commit = EmptyCommit()
654 654
655 655 # files or dirs
656 656 try:
657 657 c.file = c.commit.get_node(f_path)
658 658 c.file_author = True
659 659 c.file_tree = ''
660 660
661 661 # load file content
662 662 if c.file.is_file():
663 663 c.lf_node = {}
664 664
665 665 has_lf_enabled = self._is_lf_enabled(self.db_repo)
666 666 if has_lf_enabled:
667 667 c.lf_node = c.file.get_largefile_node()
668 668
669 669 c.file_source_page = 'true'
670 670 c.file_last_commit = c.file.last_commit
671 671
672 672 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
673 673
674 674 if not (c.file_size_too_big or c.file.is_binary):
675 675 if c.annotate: # annotation has precedence over renderer
676 676 c.annotated_lines = filenode_as_annotated_lines_tokens(
677 677 c.file
678 678 )
679 679 else:
680 680 c.renderer = (
681 681 c.renderer and h.renderer_from_filename(c.file.path)
682 682 )
683 683 if not c.renderer:
684 684 c.lines = filenode_as_lines_tokens(c.file)
685 685
686 686 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
687 687 commit_id, self.rhodecode_vcs_repo)
688 688 c.on_branch_head = is_head
689 689
690 690 branch = c.commit.branch if (
691 691 c.commit.branch and '/' not in c.commit.branch) else None
692 692 c.branch_or_raw_id = branch or c.commit.raw_id
693 693 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
694 694
695 695 author = c.file_last_commit.author
696 696 c.authors = [[
697 697 h.email(author),
698 698 h.person(author, 'username_or_name_or_email'),
699 699 1
700 700 ]]
701 701
702 702 else: # load tree content at path
703 703 c.file_source_page = 'false'
704 704 c.authors = []
705 705 # this loads a simple tree without metadata to speed things up
706 706 # later via ajax we call repo_nodetree_full and fetch whole
707 707 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path)
708 708
709 709 c.readme_data, c.readme_file = \
710 710 self._get_readme_data(self.db_repo, c.visual.default_renderer,
711 711 c.commit.raw_id, f_path)
712 712
713 713 except RepositoryError as e:
714 714 h.flash(safe_str(h.escape(e)), category='error')
715 715 raise HTTPNotFound()
716 716
717 717 if self.request.environ.get('HTTP_X_PJAX'):
718 718 html = render('rhodecode:templates/files/files_pjax.mako',
719 719 self._get_template_context(c), self.request)
720 720 else:
721 721 html = render('rhodecode:templates/files/files.mako',
722 722 self._get_template_context(c), self.request)
723 723 return Response(html)
724 724
725 725 @HasRepoPermissionAnyDecorator(
726 726 'repository.read', 'repository.write', 'repository.admin')
727 727 @view_config(
728 728 route_name='repo_files:annotated_previous', request_method='GET',
729 729 renderer=None)
730 730 def repo_files_annotated_previous(self):
731 731 self.load_default_context()
732 732
733 733 commit_id, f_path = self._get_commit_and_path()
734 734 commit = self._get_commit_or_redirect(commit_id)
735 735 prev_commit_id = commit.raw_id
736 736 line_anchor = self.request.GET.get('line_anchor')
737 737 is_file = False
738 738 try:
739 739 _file = commit.get_node(f_path)
740 740 is_file = _file.is_file()
741 741 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
742 742 pass
743 743
744 744 if is_file:
745 745 history = commit.get_path_history(f_path)
746 746 prev_commit_id = history[1].raw_id \
747 747 if len(history) > 1 else prev_commit_id
748 748 prev_url = h.route_path(
749 749 'repo_files:annotated', repo_name=self.db_repo_name,
750 750 commit_id=prev_commit_id, f_path=f_path,
751 751 _anchor='L{}'.format(line_anchor))
752 752
753 753 raise HTTPFound(prev_url)
754 754
755 755 @LoginRequired()
756 756 @HasRepoPermissionAnyDecorator(
757 757 'repository.read', 'repository.write', 'repository.admin')
758 758 @view_config(
759 759 route_name='repo_nodetree_full', request_method='GET',
760 760 renderer=None, xhr=True)
761 761 @view_config(
762 762 route_name='repo_nodetree_full:default_path', request_method='GET',
763 763 renderer=None, xhr=True)
764 764 def repo_nodetree_full(self):
765 765 """
766 766 Returns rendered html of file tree that contains commit date,
767 767 author, commit_id for the specified combination of
768 768 repo, commit_id and file path
769 769 """
770 770 c = self.load_default_context()
771 771
772 772 commit_id, f_path = self._get_commit_and_path()
773 773 commit = self._get_commit_or_redirect(commit_id)
774 774 try:
775 775 dir_node = commit.get_node(f_path)
776 776 except RepositoryError as e:
777 777 return Response('error: {}'.format(h.escape(safe_str(e))))
778 778
779 779 if dir_node.is_file():
780 780 return Response('')
781 781
782 782 c.file = dir_node
783 783 c.commit = commit
784 784
785 785 html = self._get_tree_at_commit(
786 786 c, commit.raw_id, dir_node.path, full_load=True)
787 787
788 788 return Response(html)
789 789
790 790 def _get_attachement_headers(self, f_path):
791 791 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
792 792 safe_path = f_name.replace('"', '\\"')
793 793 encoded_path = urllib.quote(f_name)
794 794
795 795 return "attachment; " \
796 796 "filename=\"{}\"; " \
797 797 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
798 798
799 799 @LoginRequired()
800 800 @HasRepoPermissionAnyDecorator(
801 801 'repository.read', 'repository.write', 'repository.admin')
802 802 @view_config(
803 803 route_name='repo_file_raw', request_method='GET',
804 804 renderer=None)
805 805 def repo_file_raw(self):
806 806 """
807 807 Action for show as raw, some mimetypes are "rendered",
808 808 those include images, icons.
809 809 """
810 810 c = self.load_default_context()
811 811
812 812 commit_id, f_path = self._get_commit_and_path()
813 813 commit = self._get_commit_or_redirect(commit_id)
814 814 file_node = self._get_filenode_or_redirect(commit, f_path)
815 815
816 816 raw_mimetype_mapping = {
817 817 # map original mimetype to a mimetype used for "show as raw"
818 818 # you can also provide a content-disposition to override the
819 819 # default "attachment" disposition.
820 820 # orig_type: (new_type, new_dispo)
821 821
822 822 # show images inline:
823 823 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
824 824 # for example render an SVG with javascript inside or even render
825 825 # HTML.
826 826 'image/x-icon': ('image/x-icon', 'inline'),
827 827 'image/png': ('image/png', 'inline'),
828 828 'image/gif': ('image/gif', 'inline'),
829 829 'image/jpeg': ('image/jpeg', 'inline'),
830 830 'application/pdf': ('application/pdf', 'inline'),
831 831 }
832 832
833 833 mimetype = file_node.mimetype
834 834 try:
835 835 mimetype, disposition = raw_mimetype_mapping[mimetype]
836 836 except KeyError:
837 837 # we don't know anything special about this, handle it safely
838 838 if file_node.is_binary:
839 839 # do same as download raw for binary files
840 840 mimetype, disposition = 'application/octet-stream', 'attachment'
841 841 else:
842 842 # do not just use the original mimetype, but force text/plain,
843 843 # otherwise it would serve text/html and that might be unsafe.
844 844 # Note: underlying vcs library fakes text/plain mimetype if the
845 845 # mimetype can not be determined and it thinks it is not
846 846 # binary.This might lead to erroneous text display in some
847 847 # cases, but helps in other cases, like with text files
848 848 # without extension.
849 849 mimetype, disposition = 'text/plain', 'inline'
850 850
851 851 if disposition == 'attachment':
852 852 disposition = self._get_attachement_headers(f_path)
853 853
854 854 stream_content = file_node.stream_bytes()
855 855
856 856 response = Response(app_iter=stream_content)
857 857 response.content_disposition = disposition
858 858 response.content_type = mimetype
859 859
860 860 charset = self._get_default_encoding(c)
861 861 if charset:
862 862 response.charset = charset
863 863
864 864 return response
865 865
866 866 @LoginRequired()
867 867 @HasRepoPermissionAnyDecorator(
868 868 'repository.read', 'repository.write', 'repository.admin')
869 869 @view_config(
870 870 route_name='repo_file_download', request_method='GET',
871 871 renderer=None)
872 872 @view_config(
873 873 route_name='repo_file_download:legacy', request_method='GET',
874 874 renderer=None)
875 875 def repo_file_download(self):
876 876 c = self.load_default_context()
877 877
878 878 commit_id, f_path = self._get_commit_and_path()
879 879 commit = self._get_commit_or_redirect(commit_id)
880 880 file_node = self._get_filenode_or_redirect(commit, f_path)
881 881
882 882 if self.request.GET.get('lf'):
883 883 # only if lf get flag is passed, we download this file
884 884 # as LFS/Largefile
885 885 lf_node = file_node.get_largefile_node()
886 886 if lf_node:
887 887 # overwrite our pointer with the REAL large-file
888 888 file_node = lf_node
889 889
890 890 disposition = self._get_attachement_headers(f_path)
891 891
892 892 stream_content = file_node.stream_bytes()
893 893
894 894 response = Response(app_iter=stream_content)
895 895 response.content_disposition = disposition
896 896 response.content_type = file_node.mimetype
897 897
898 898 charset = self._get_default_encoding(c)
899 899 if charset:
900 900 response.charset = charset
901 901
902 902 return response
903 903
904 904 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
905 905
906 906 cache_seconds = safe_int(
907 907 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
908 908 cache_on = cache_seconds > 0
909 909 log.debug(
910 910 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
911 911 'with caching: %s[TTL: %ss]' % (
912 912 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
913 913
914 914 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
915 915 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
916 916
917 917 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
918 918 condition=cache_on)
919 919 def compute_file_search(repo_id, commit_id, f_path):
920 920 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
921 921 repo_id, commit_id, f_path)
922 922 try:
923 _d, _f = ScmModel().get_nodes(
924 repo_name, commit_id, f_path, flat=False)
923 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, commit_id, f_path)
925 924 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
926 925 log.exception(safe_str(e))
927 926 h.flash(safe_str(h.escape(e)), category='error')
928 927 raise HTTPFound(h.route_path(
929 928 'repo_files', repo_name=self.db_repo_name,
930 929 commit_id='tip', f_path='/'))
931 930
932 931 return _d + _f
933 932
934 933 result = compute_file_search(self.db_repo.repo_id, commit_id, f_path)
935 934 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
936 935
937 936 @LoginRequired()
938 937 @HasRepoPermissionAnyDecorator(
939 938 'repository.read', 'repository.write', 'repository.admin')
940 939 @view_config(
941 940 route_name='repo_files_nodelist', request_method='GET',
942 941 renderer='json_ext', xhr=True)
943 942 def repo_nodelist(self):
944 943 self.load_default_context()
945 944
946 945 commit_id, f_path = self._get_commit_and_path()
947 946 commit = self._get_commit_or_redirect(commit_id)
948 947
949 948 metadata = self._get_nodelist_at_commit(
950 949 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
951 950 return {'nodes': metadata}
952 951
953 952 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
954 953 items = []
955 954 for name, commit_id in branches_or_tags.items():
956 955 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
957 956 items.append((sym_ref, name, ref_type))
958 957 return items
959 958
960 959 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
961 960 return commit_id
962 961
963 962 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
964 963 new_f_path = vcspath.join(name, f_path)
965 964 return u'%s@%s' % (new_f_path, commit_id)
966 965
967 966 def _get_node_history(self, commit_obj, f_path, commits=None):
968 967 """
969 968 get commit history for given node
970 969
971 970 :param commit_obj: commit to calculate history
972 971 :param f_path: path for node to calculate history for
973 972 :param commits: if passed don't calculate history and take
974 973 commits defined in this list
975 974 """
976 975 _ = self.request.translate
977 976
978 977 # calculate history based on tip
979 978 tip = self.rhodecode_vcs_repo.get_commit()
980 979 if commits is None:
981 980 pre_load = ["author", "branch"]
982 981 try:
983 982 commits = tip.get_path_history(f_path, pre_load=pre_load)
984 983 except (NodeDoesNotExistError, CommitError):
985 984 # this node is not present at tip!
986 985 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
987 986
988 987 history = []
989 988 commits_group = ([], _("Changesets"))
990 989 for commit in commits:
991 990 branch = ' (%s)' % commit.branch if commit.branch else ''
992 991 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
993 992 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
994 993 history.append(commits_group)
995 994
996 995 symbolic_reference = self._symbolic_reference
997 996
998 997 if self.rhodecode_vcs_repo.alias == 'svn':
999 998 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1000 999 f_path, self.rhodecode_vcs_repo)
1001 1000 if adjusted_f_path != f_path:
1002 1001 log.debug(
1003 1002 'Recognized svn tag or branch in file "%s", using svn '
1004 1003 'specific symbolic references', f_path)
1005 1004 f_path = adjusted_f_path
1006 1005 symbolic_reference = self._symbolic_reference_svn
1007 1006
1008 1007 branches = self._create_references(
1009 1008 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1010 1009 branches_group = (branches, _("Branches"))
1011 1010
1012 1011 tags = self._create_references(
1013 1012 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1014 1013 tags_group = (tags, _("Tags"))
1015 1014
1016 1015 history.append(branches_group)
1017 1016 history.append(tags_group)
1018 1017
1019 1018 return history, commits
1020 1019
1021 1020 @LoginRequired()
1022 1021 @HasRepoPermissionAnyDecorator(
1023 1022 'repository.read', 'repository.write', 'repository.admin')
1024 1023 @view_config(
1025 1024 route_name='repo_file_history', request_method='GET',
1026 1025 renderer='json_ext')
1027 1026 def repo_file_history(self):
1028 1027 self.load_default_context()
1029 1028
1030 1029 commit_id, f_path = self._get_commit_and_path()
1031 1030 commit = self._get_commit_or_redirect(commit_id)
1032 1031 file_node = self._get_filenode_or_redirect(commit, f_path)
1033 1032
1034 1033 if file_node.is_file():
1035 1034 file_history, _hist = self._get_node_history(commit, f_path)
1036 1035
1037 1036 res = []
1038 1037 for obj in file_history:
1039 1038 res.append({
1040 1039 'text': obj[1],
1041 1040 'children': [{'id': o[0], 'text': o[1], 'type': o[2]} for o in obj[0]]
1042 1041 })
1043 1042
1044 1043 data = {
1045 1044 'more': False,
1046 1045 'results': res
1047 1046 }
1048 1047 return data
1049 1048
1050 1049 log.warning('Cannot fetch history for directory')
1051 1050 raise HTTPBadRequest()
1052 1051
1053 1052 @LoginRequired()
1054 1053 @HasRepoPermissionAnyDecorator(
1055 1054 'repository.read', 'repository.write', 'repository.admin')
1056 1055 @view_config(
1057 1056 route_name='repo_file_authors', request_method='GET',
1058 1057 renderer='rhodecode:templates/files/file_authors_box.mako')
1059 1058 def repo_file_authors(self):
1060 1059 c = self.load_default_context()
1061 1060
1062 1061 commit_id, f_path = self._get_commit_and_path()
1063 1062 commit = self._get_commit_or_redirect(commit_id)
1064 1063 file_node = self._get_filenode_or_redirect(commit, f_path)
1065 1064
1066 1065 if not file_node.is_file():
1067 1066 raise HTTPBadRequest()
1068 1067
1069 1068 c.file_last_commit = file_node.last_commit
1070 1069 if self.request.GET.get('annotate') == '1':
1071 1070 # use _hist from annotation if annotation mode is on
1072 1071 commit_ids = set(x[1] for x in file_node.annotate)
1073 1072 _hist = (
1074 1073 self.rhodecode_vcs_repo.get_commit(commit_id)
1075 1074 for commit_id in commit_ids)
1076 1075 else:
1077 1076 _f_history, _hist = self._get_node_history(commit, f_path)
1078 1077 c.file_author = False
1079 1078
1080 1079 unique = collections.OrderedDict()
1081 1080 for commit in _hist:
1082 1081 author = commit.author
1083 1082 if author not in unique:
1084 1083 unique[commit.author] = [
1085 1084 h.email(author),
1086 1085 h.person(author, 'username_or_name_or_email'),
1087 1086 1 # counter
1088 1087 ]
1089 1088
1090 1089 else:
1091 1090 # increase counter
1092 1091 unique[commit.author][2] += 1
1093 1092
1094 1093 c.authors = [val for val in unique.values()]
1095 1094
1096 1095 return self._get_template_context(c)
1097 1096
1098 1097 @LoginRequired()
1099 1098 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1100 1099 @view_config(
1101 1100 route_name='repo_files_remove_file', request_method='GET',
1102 1101 renderer='rhodecode:templates/files/files_delete.mako')
1103 1102 def repo_files_remove_file(self):
1104 1103 _ = self.request.translate
1105 1104 c = self.load_default_context()
1106 1105 commit_id, f_path = self._get_commit_and_path()
1107 1106
1108 1107 self._ensure_not_locked()
1109 1108 _branch_name, _sha_commit_id, is_head = \
1110 1109 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1111 1110
1112 1111 self.forbid_non_head(is_head, f_path)
1113 1112 self.check_branch_permission(_branch_name)
1114 1113
1115 1114 c.commit = self._get_commit_or_redirect(commit_id)
1116 1115 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1117 1116
1118 1117 c.default_message = _(
1119 1118 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1120 1119 c.f_path = f_path
1121 1120
1122 1121 return self._get_template_context(c)
1123 1122
1124 1123 @LoginRequired()
1125 1124 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1126 1125 @CSRFRequired()
1127 1126 @view_config(
1128 1127 route_name='repo_files_delete_file', request_method='POST',
1129 1128 renderer=None)
1130 1129 def repo_files_delete_file(self):
1131 1130 _ = self.request.translate
1132 1131
1133 1132 c = self.load_default_context()
1134 1133 commit_id, f_path = self._get_commit_and_path()
1135 1134
1136 1135 self._ensure_not_locked()
1137 1136 _branch_name, _sha_commit_id, is_head = \
1138 1137 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1139 1138
1140 1139 self.forbid_non_head(is_head, f_path)
1141 1140 self.check_branch_permission(_branch_name)
1142 1141
1143 1142 c.commit = self._get_commit_or_redirect(commit_id)
1144 1143 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1145 1144
1146 1145 c.default_message = _(
1147 1146 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1148 1147 c.f_path = f_path
1149 1148 node_path = f_path
1150 1149 author = self._rhodecode_db_user.full_contact
1151 1150 message = self.request.POST.get('message') or c.default_message
1152 1151 try:
1153 1152 nodes = {
1154 1153 node_path: {
1155 1154 'content': ''
1156 1155 }
1157 1156 }
1158 1157 ScmModel().delete_nodes(
1159 1158 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1160 1159 message=message,
1161 1160 nodes=nodes,
1162 1161 parent_commit=c.commit,
1163 1162 author=author,
1164 1163 )
1165 1164
1166 1165 h.flash(
1167 1166 _('Successfully deleted file `{}`').format(
1168 1167 h.escape(f_path)), category='success')
1169 1168 except Exception:
1170 1169 log.exception('Error during commit operation')
1171 1170 h.flash(_('Error occurred during commit'), category='error')
1172 1171 raise HTTPFound(
1173 1172 h.route_path('repo_commit', repo_name=self.db_repo_name,
1174 1173 commit_id='tip'))
1175 1174
1176 1175 @LoginRequired()
1177 1176 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1178 1177 @view_config(
1179 1178 route_name='repo_files_edit_file', request_method='GET',
1180 1179 renderer='rhodecode:templates/files/files_edit.mako')
1181 1180 def repo_files_edit_file(self):
1182 1181 _ = self.request.translate
1183 1182 c = self.load_default_context()
1184 1183 commit_id, f_path = self._get_commit_and_path()
1185 1184
1186 1185 self._ensure_not_locked()
1187 1186 _branch_name, _sha_commit_id, is_head = \
1188 1187 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1189 1188
1190 1189 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1191 1190 self.check_branch_permission(_branch_name, commit_id=commit_id)
1192 1191
1193 1192 c.commit = self._get_commit_or_redirect(commit_id)
1194 1193 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1195 1194
1196 1195 if c.file.is_binary:
1197 1196 files_url = h.route_path(
1198 1197 'repo_files',
1199 1198 repo_name=self.db_repo_name,
1200 1199 commit_id=c.commit.raw_id, f_path=f_path)
1201 1200 raise HTTPFound(files_url)
1202 1201
1203 1202 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1204 1203 c.f_path = f_path
1205 1204
1206 1205 return self._get_template_context(c)
1207 1206
1208 1207 @LoginRequired()
1209 1208 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1210 1209 @CSRFRequired()
1211 1210 @view_config(
1212 1211 route_name='repo_files_update_file', request_method='POST',
1213 1212 renderer=None)
1214 1213 def repo_files_update_file(self):
1215 1214 _ = self.request.translate
1216 1215 c = self.load_default_context()
1217 1216 commit_id, f_path = self._get_commit_and_path()
1218 1217
1219 1218 self._ensure_not_locked()
1220 1219
1221 1220 c.commit = self._get_commit_or_redirect(commit_id)
1222 1221 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1223 1222
1224 1223 if c.file.is_binary:
1225 1224 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1226 1225 commit_id=c.commit.raw_id, f_path=f_path))
1227 1226
1228 1227 _branch_name, _sha_commit_id, is_head = \
1229 1228 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1230 1229
1231 1230 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1232 1231 self.check_branch_permission(_branch_name, commit_id=commit_id)
1233 1232
1234 1233 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1235 1234 c.f_path = f_path
1236 1235
1237 1236 old_content = c.file.content
1238 1237 sl = old_content.splitlines(1)
1239 1238 first_line = sl[0] if sl else ''
1240 1239
1241 1240 r_post = self.request.POST
1242 1241 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1243 1242 line_ending_mode = detect_mode(first_line, 0)
1244 1243 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1245 1244
1246 1245 message = r_post.get('message') or c.default_message
1247 1246 org_node_path = c.file.unicode_path
1248 1247 filename = r_post['filename']
1249 1248
1250 1249 root_path = c.file.dir_path
1251 1250 pure_path = self.create_pure_path(root_path, filename)
1252 1251 node_path = safe_unicode(bytes(pure_path))
1253 1252
1254 1253 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1255 1254 commit_id=commit_id)
1256 1255 if content == old_content and node_path == org_node_path:
1257 1256 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1258 1257 category='warning')
1259 1258 raise HTTPFound(default_redirect_url)
1260 1259
1261 1260 try:
1262 1261 mapping = {
1263 1262 org_node_path: {
1264 1263 'org_filename': org_node_path,
1265 1264 'filename': node_path,
1266 1265 'content': content,
1267 1266 'lexer': '',
1268 1267 'op': 'mod',
1269 1268 'mode': c.file.mode
1270 1269 }
1271 1270 }
1272 1271
1273 1272 commit = ScmModel().update_nodes(
1274 1273 user=self._rhodecode_db_user.user_id,
1275 1274 repo=self.db_repo,
1276 1275 message=message,
1277 1276 nodes=mapping,
1278 1277 parent_commit=c.commit,
1279 1278 )
1280 1279
1281 1280 h.flash(_('Successfully committed changes to file `{}`').format(
1282 1281 h.escape(f_path)), category='success')
1283 1282 default_redirect_url = h.route_path(
1284 1283 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1285 1284
1286 1285 except Exception:
1287 1286 log.exception('Error occurred during commit')
1288 1287 h.flash(_('Error occurred during commit'), category='error')
1289 1288
1290 1289 raise HTTPFound(default_redirect_url)
1291 1290
1292 1291 @LoginRequired()
1293 1292 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1294 1293 @view_config(
1295 1294 route_name='repo_files_add_file', request_method='GET',
1296 1295 renderer='rhodecode:templates/files/files_add.mako')
1297 1296 @view_config(
1298 1297 route_name='repo_files_upload_file', request_method='GET',
1299 1298 renderer='rhodecode:templates/files/files_upload.mako')
1300 1299 def repo_files_add_file(self):
1301 1300 _ = self.request.translate
1302 1301 c = self.load_default_context()
1303 1302 commit_id, f_path = self._get_commit_and_path()
1304 1303
1305 1304 self._ensure_not_locked()
1306 1305
1307 1306 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1308 1307 if c.commit is None:
1309 1308 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1310 1309
1311 1310 if self.rhodecode_vcs_repo.is_empty():
1312 1311 # for empty repository we cannot check for current branch, we rely on
1313 1312 # c.commit.branch instead
1314 1313 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1315 1314 else:
1316 1315 _branch_name, _sha_commit_id, is_head = \
1317 1316 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1318 1317
1319 1318 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1320 1319 self.check_branch_permission(_branch_name, commit_id=commit_id)
1321 1320
1322 1321 c.default_message = (_('Added file via RhodeCode Enterprise'))
1323 1322 c.f_path = f_path.lstrip('/') # ensure not relative path
1324 1323
1325 1324 return self._get_template_context(c)
1326 1325
1327 1326 @LoginRequired()
1328 1327 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1329 1328 @CSRFRequired()
1330 1329 @view_config(
1331 1330 route_name='repo_files_create_file', request_method='POST',
1332 1331 renderer=None)
1333 1332 def repo_files_create_file(self):
1334 1333 _ = self.request.translate
1335 1334 c = self.load_default_context()
1336 1335 commit_id, f_path = self._get_commit_and_path()
1337 1336
1338 1337 self._ensure_not_locked()
1339 1338
1340 1339 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1341 1340 if c.commit is None:
1342 1341 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1343 1342
1344 1343 # calculate redirect URL
1345 1344 if self.rhodecode_vcs_repo.is_empty():
1346 1345 default_redirect_url = h.route_path(
1347 1346 'repo_summary', repo_name=self.db_repo_name)
1348 1347 else:
1349 1348 default_redirect_url = h.route_path(
1350 1349 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1351 1350
1352 1351 if self.rhodecode_vcs_repo.is_empty():
1353 1352 # for empty repository we cannot check for current branch, we rely on
1354 1353 # c.commit.branch instead
1355 1354 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1356 1355 else:
1357 1356 _branch_name, _sha_commit_id, is_head = \
1358 1357 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1359 1358
1360 1359 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1361 1360 self.check_branch_permission(_branch_name, commit_id=commit_id)
1362 1361
1363 1362 c.default_message = (_('Added file via RhodeCode Enterprise'))
1364 1363 c.f_path = f_path
1365 1364
1366 1365 r_post = self.request.POST
1367 1366 message = r_post.get('message') or c.default_message
1368 1367 filename = r_post.get('filename')
1369 1368 unix_mode = 0
1370 1369 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1371 1370
1372 1371 if not filename:
1373 1372 # If there's no commit, redirect to repo summary
1374 1373 if type(c.commit) is EmptyCommit:
1375 1374 redirect_url = h.route_path(
1376 1375 'repo_summary', repo_name=self.db_repo_name)
1377 1376 else:
1378 1377 redirect_url = default_redirect_url
1379 1378 h.flash(_('No filename specified'), category='warning')
1380 1379 raise HTTPFound(redirect_url)
1381 1380
1382 1381 root_path = f_path
1383 1382 pure_path = self.create_pure_path(root_path, filename)
1384 1383 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1385 1384
1386 1385 author = self._rhodecode_db_user.full_contact
1387 1386 nodes = {
1388 1387 node_path: {
1389 1388 'content': content
1390 1389 }
1391 1390 }
1392 1391
1393 1392 try:
1394 1393
1395 1394 commit = ScmModel().create_nodes(
1396 1395 user=self._rhodecode_db_user.user_id,
1397 1396 repo=self.db_repo,
1398 1397 message=message,
1399 1398 nodes=nodes,
1400 1399 parent_commit=c.commit,
1401 1400 author=author,
1402 1401 )
1403 1402
1404 1403 h.flash(_('Successfully committed new file `{}`').format(
1405 1404 h.escape(node_path)), category='success')
1406 1405
1407 1406 default_redirect_url = h.route_path(
1408 1407 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1409 1408
1410 1409 except NonRelativePathError:
1411 1410 log.exception('Non Relative path found')
1412 1411 h.flash(_('The location specified must be a relative path and must not '
1413 1412 'contain .. in the path'), category='warning')
1414 1413 raise HTTPFound(default_redirect_url)
1415 1414 except (NodeError, NodeAlreadyExistsError) as e:
1416 1415 h.flash(_(h.escape(e)), category='error')
1417 1416 except Exception:
1418 1417 log.exception('Error occurred during commit')
1419 1418 h.flash(_('Error occurred during commit'), category='error')
1420 1419
1421 1420 raise HTTPFound(default_redirect_url)
1422 1421
1423 1422 @LoginRequired()
1424 1423 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1425 1424 @CSRFRequired()
1426 1425 @view_config(
1427 1426 route_name='repo_files_upload_file', request_method='POST',
1428 1427 renderer='json_ext')
1429 1428 def repo_files_upload_file(self):
1430 1429 _ = self.request.translate
1431 1430 c = self.load_default_context()
1432 1431 commit_id, f_path = self._get_commit_and_path()
1433 1432
1434 1433 self._ensure_not_locked()
1435 1434
1436 1435 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1437 1436 if c.commit is None:
1438 1437 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1439 1438
1440 1439 # calculate redirect URL
1441 1440 if self.rhodecode_vcs_repo.is_empty():
1442 1441 default_redirect_url = h.route_path(
1443 1442 'repo_summary', repo_name=self.db_repo_name)
1444 1443 else:
1445 1444 default_redirect_url = h.route_path(
1446 1445 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1447 1446
1448 1447 if self.rhodecode_vcs_repo.is_empty():
1449 1448 # for empty repository we cannot check for current branch, we rely on
1450 1449 # c.commit.branch instead
1451 1450 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1452 1451 else:
1453 1452 _branch_name, _sha_commit_id, is_head = \
1454 1453 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1455 1454
1456 1455 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1457 1456 if error:
1458 1457 return {
1459 1458 'error': error,
1460 1459 'redirect_url': default_redirect_url
1461 1460 }
1462 1461 error = self.check_branch_permission(_branch_name, json_mode=True)
1463 1462 if error:
1464 1463 return {
1465 1464 'error': error,
1466 1465 'redirect_url': default_redirect_url
1467 1466 }
1468 1467
1469 1468 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1470 1469 c.f_path = f_path
1471 1470
1472 1471 r_post = self.request.POST
1473 1472
1474 1473 message = c.default_message
1475 1474 user_message = r_post.getall('message')
1476 1475 if isinstance(user_message, list) and user_message:
1477 1476 # we take the first from duplicated results if it's not empty
1478 1477 message = user_message[0] if user_message[0] else message
1479 1478
1480 1479 nodes = {}
1481 1480
1482 1481 for file_obj in r_post.getall('files_upload') or []:
1483 1482 content = file_obj.file
1484 1483 filename = file_obj.filename
1485 1484
1486 1485 root_path = f_path
1487 1486 pure_path = self.create_pure_path(root_path, filename)
1488 1487 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1489 1488
1490 1489 nodes[node_path] = {
1491 1490 'content': content
1492 1491 }
1493 1492
1494 1493 if not nodes:
1495 1494 error = 'missing files'
1496 1495 return {
1497 1496 'error': error,
1498 1497 'redirect_url': default_redirect_url
1499 1498 }
1500 1499
1501 1500 author = self._rhodecode_db_user.full_contact
1502 1501
1503 1502 try:
1504 1503 commit = ScmModel().create_nodes(
1505 1504 user=self._rhodecode_db_user.user_id,
1506 1505 repo=self.db_repo,
1507 1506 message=message,
1508 1507 nodes=nodes,
1509 1508 parent_commit=c.commit,
1510 1509 author=author,
1511 1510 )
1512 1511 if len(nodes) == 1:
1513 1512 flash_message = _('Successfully committed {} new files').format(len(nodes))
1514 1513 else:
1515 1514 flash_message = _('Successfully committed 1 new file')
1516 1515
1517 1516 h.flash(flash_message, category='success')
1518 1517
1519 1518 default_redirect_url = h.route_path(
1520 1519 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1521 1520
1522 1521 except NonRelativePathError:
1523 1522 log.exception('Non Relative path found')
1524 1523 error = _('The location specified must be a relative path and must not '
1525 1524 'contain .. in the path')
1526 1525 h.flash(error, category='warning')
1527 1526
1528 1527 return {
1529 1528 'error': error,
1530 1529 'redirect_url': default_redirect_url
1531 1530 }
1532 1531 except (NodeError, NodeAlreadyExistsError) as e:
1533 1532 error = h.escape(e)
1534 1533 h.flash(error, category='error')
1535 1534
1536 1535 return {
1537 1536 'error': error,
1538 1537 'redirect_url': default_redirect_url
1539 1538 }
1540 1539 except Exception:
1541 1540 log.exception('Error occurred during commit')
1542 1541 error = _('Error occurred during commit')
1543 1542 h.flash(error, category='error')
1544 1543 return {
1545 1544 'error': error,
1546 1545 'redirect_url': default_redirect_url
1547 1546 }
1548 1547
1549 1548 return {
1550 1549 'error': None,
1551 1550 'redirect_url': default_redirect_url
1552 1551 }
@@ -1,870 +1,870 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 Module holding everything related to vcs nodes, with vcs2 architecture.
23 23 """
24 24
25 25 import os
26 26 import stat
27 27
28 28 from zope.cachedescriptors.property import Lazy as LazyProperty
29 29
30 30 import rhodecode
31 31 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
32 32 from rhodecode.lib.utils import safe_unicode, safe_str
33 33 from rhodecode.lib.utils2 import md5
34 34 from rhodecode.lib.vcs import path as vcspath
35 35 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
36 36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 37 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
38 38
39 39 LARGEFILE_PREFIX = '.hglf'
40 40
41 41
42 42 class NodeKind:
43 43 SUBMODULE = -1
44 44 DIR = 1
45 45 FILE = 2
46 46 LARGEFILE = 3
47 47
48 48
49 49 class NodeState:
50 50 ADDED = u'added'
51 51 CHANGED = u'changed'
52 52 NOT_CHANGED = u'not changed'
53 53 REMOVED = u'removed'
54 54
55 55
56 56 class NodeGeneratorBase(object):
57 57 """
58 58 Base class for removed added and changed filenodes, it's a lazy generator
59 59 class that will create filenodes only on iteration or call
60 60
61 61 The len method doesn't need to create filenodes at all
62 62 """
63 63
64 64 def __init__(self, current_paths, cs):
65 65 self.cs = cs
66 66 self.current_paths = current_paths
67 67
68 68 def __call__(self):
69 69 return [n for n in self]
70 70
71 71 def __getslice__(self, i, j):
72 72 for p in self.current_paths[i:j]:
73 73 yield self.cs.get_node(p)
74 74
75 75 def __len__(self):
76 76 return len(self.current_paths)
77 77
78 78 def __iter__(self):
79 79 for p in self.current_paths:
80 80 yield self.cs.get_node(p)
81 81
82 82
83 83 class AddedFileNodesGenerator(NodeGeneratorBase):
84 84 """
85 85 Class holding added files for current commit
86 86 """
87 87
88 88
89 89 class ChangedFileNodesGenerator(NodeGeneratorBase):
90 90 """
91 91 Class holding changed files for current commit
92 92 """
93 93
94 94
95 95 class RemovedFileNodesGenerator(NodeGeneratorBase):
96 96 """
97 97 Class holding removed files for current commit
98 98 """
99 99 def __iter__(self):
100 100 for p in self.current_paths:
101 101 yield RemovedFileNode(path=p)
102 102
103 103 def __getslice__(self, i, j):
104 104 for p in self.current_paths[i:j]:
105 105 yield RemovedFileNode(path=p)
106 106
107 107
108 108 class Node(object):
109 109 """
110 110 Simplest class representing file or directory on repository. SCM backends
111 111 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
112 112 directly.
113 113
114 114 Node's ``path`` cannot start with slash as we operate on *relative* paths
115 115 only. Moreover, every single node is identified by the ``path`` attribute,
116 116 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
117 117 """
118 118 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
119 119 # security attacks could be used with this
120 120 commit = None
121 121
122 122 def __init__(self, path, kind):
123 123 self._validate_path(path) # can throw exception if path is invalid
124 124 self.path = safe_str(path.rstrip('/')) # we store paths as str
125 125 if path == '' and kind != NodeKind.DIR:
126 126 raise NodeError("Only DirNode and its subclasses may be "
127 127 "initialized with empty path")
128 128 self.kind = kind
129 129
130 130 if self.is_root() and not self.is_dir():
131 131 raise NodeError("Root node cannot be FILE kind")
132 132
133 133 def _validate_path(self, path):
134 134 if path.startswith('/'):
135 135 raise NodeError(
136 136 "Cannot initialize Node objects with slash at "
137 137 "the beginning as only relative paths are supported. "
138 138 "Got %s" % (path,))
139 139
140 140 @LazyProperty
141 141 def parent(self):
142 142 parent_path = self.get_parent_path()
143 143 if parent_path:
144 144 if self.commit:
145 145 return self.commit.get_node(parent_path)
146 146 return DirNode(parent_path)
147 147 return None
148 148
149 149 @LazyProperty
150 150 def unicode_path(self):
151 151 return safe_unicode(self.path)
152 152
153 153 @LazyProperty
154 154 def has_rtlo(self):
155 155 """Detects if a path has right-to-left-override marker"""
156 156 return self.RTLO_MARKER in self.unicode_path
157 157
158 158 @LazyProperty
159 159 def unicode_path_safe(self):
160 160 """
161 161 Special SAFE representation of path without the right-to-left-override.
162 162 This should be only used for "showing" the file, cannot be used for any
163 163 urls etc.
164 164 """
165 165 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
166 166
167 167 @LazyProperty
168 168 def dir_path(self):
169 169 """
170 170 Returns name of the directory from full path of this vcs node. Empty
171 171 string is returned if there's no directory in the path
172 172 """
173 173 _parts = self.path.rstrip('/').rsplit('/', 1)
174 174 if len(_parts) == 2:
175 175 return safe_unicode(_parts[0])
176 176 return u''
177 177
178 178 @LazyProperty
179 179 def name(self):
180 180 """
181 181 Returns name of the node so if its path
182 182 then only last part is returned.
183 183 """
184 184 return safe_unicode(self.path.rstrip('/').split('/')[-1])
185 185
186 186 @property
187 187 def kind(self):
188 188 return self._kind
189 189
190 190 @kind.setter
191 191 def kind(self, kind):
192 192 if hasattr(self, '_kind'):
193 193 raise NodeError("Cannot change node's kind")
194 194 else:
195 195 self._kind = kind
196 196 # Post setter check (path's trailing slash)
197 197 if self.path.endswith('/'):
198 198 raise NodeError("Node's path cannot end with slash")
199 199
200 200 def __cmp__(self, other):
201 201 """
202 202 Comparator using name of the node, needed for quick list sorting.
203 203 """
204 204
205 205 kind_cmp = cmp(self.kind, other.kind)
206 206 if kind_cmp:
207 207 if isinstance(self, SubModuleNode):
208 208 # we make submodules equal to dirnode for "sorting" purposes
209 209 return NodeKind.DIR
210 210 return kind_cmp
211 211 return cmp(self.name, other.name)
212 212
213 213 def __eq__(self, other):
214 214 for attr in ['name', 'path', 'kind']:
215 215 if getattr(self, attr) != getattr(other, attr):
216 216 return False
217 217 if self.is_file():
218 218 if self.content != other.content:
219 219 return False
220 220 else:
221 221 # For DirNode's check without entering each dir
222 222 self_nodes_paths = list(sorted(n.path for n in self.nodes))
223 223 other_nodes_paths = list(sorted(n.path for n in self.nodes))
224 224 if self_nodes_paths != other_nodes_paths:
225 225 return False
226 226 return True
227 227
228 228 def __ne__(self, other):
229 229 return not self.__eq__(other)
230 230
231 231 def __repr__(self):
232 232 return '<%s %r>' % (self.__class__.__name__, self.path)
233 233
234 234 def __str__(self):
235 235 return self.__repr__()
236 236
237 237 def __unicode__(self):
238 238 return self.name
239 239
240 240 def get_parent_path(self):
241 241 """
242 242 Returns node's parent path or empty string if node is root.
243 243 """
244 244 if self.is_root():
245 245 return ''
246 246 return vcspath.dirname(self.path.rstrip('/')) + '/'
247 247
248 248 def is_file(self):
249 249 """
250 250 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
251 251 otherwise.
252 252 """
253 253 return self.kind == NodeKind.FILE
254 254
255 255 def is_dir(self):
256 256 """
257 257 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
258 258 otherwise.
259 259 """
260 260 return self.kind == NodeKind.DIR
261 261
262 262 def is_root(self):
263 263 """
264 264 Returns ``True`` if node is a root node and ``False`` otherwise.
265 265 """
266 266 return self.kind == NodeKind.DIR and self.path == ''
267 267
268 268 def is_submodule(self):
269 269 """
270 270 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
271 271 otherwise.
272 272 """
273 273 return self.kind == NodeKind.SUBMODULE
274 274
275 275 def is_largefile(self):
276 276 """
277 277 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
278 278 otherwise
279 279 """
280 280 return self.kind == NodeKind.LARGEFILE
281 281
282 282 def is_link(self):
283 283 if self.commit:
284 284 return self.commit.is_link(self.path)
285 285 return False
286 286
287 287 @LazyProperty
288 288 def added(self):
289 289 return self.state is NodeState.ADDED
290 290
291 291 @LazyProperty
292 292 def changed(self):
293 293 return self.state is NodeState.CHANGED
294 294
295 295 @LazyProperty
296 296 def not_changed(self):
297 297 return self.state is NodeState.NOT_CHANGED
298 298
299 299 @LazyProperty
300 300 def removed(self):
301 301 return self.state is NodeState.REMOVED
302 302
303 303
304 304 class FileNode(Node):
305 305 """
306 306 Class representing file nodes.
307 307
308 308 :attribute: path: path to the node, relative to repository's root
309 309 :attribute: content: if given arbitrary sets content of the file
310 310 :attribute: commit: if given, first time content is accessed, callback
311 311 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
312 312 """
313 313 _filter_pre_load = []
314 314
315 315 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
316 316 """
317 317 Only one of ``content`` and ``commit`` may be given. Passing both
318 318 would raise ``NodeError`` exception.
319 319
320 320 :param path: relative path to the node
321 321 :param content: content may be passed to constructor
322 322 :param commit: if given, will use it to lazily fetch content
323 323 :param mode: ST_MODE (i.e. 0100644)
324 324 """
325 325 if content and commit:
326 326 raise NodeError("Cannot use both content and commit")
327 327 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
328 328 self.commit = commit
329 329 self._content = content
330 330 self._mode = mode or FILEMODE_DEFAULT
331 331
332 332 self._set_bulk_properties(pre_load)
333 333
334 334 def _set_bulk_properties(self, pre_load):
335 335 if not pre_load:
336 336 return
337 337 pre_load = [entry for entry in pre_load
338 338 if entry not in self._filter_pre_load]
339 339 if not pre_load:
340 340 return
341 341
342 342 for attr_name in pre_load:
343 343 result = getattr(self, attr_name)
344 344 if callable(result):
345 345 result = result()
346 346 self.__dict__[attr_name] = result
347 347
348 348 @LazyProperty
349 349 def mode(self):
350 350 """
351 351 Returns lazily mode of the FileNode. If `commit` is not set, would
352 352 use value given at initialization or `FILEMODE_DEFAULT` (default).
353 353 """
354 354 if self.commit:
355 355 mode = self.commit.get_file_mode(self.path)
356 356 else:
357 357 mode = self._mode
358 358 return mode
359 359
360 360 @LazyProperty
361 361 def raw_bytes(self):
362 362 """
363 363 Returns lazily the raw bytes of the FileNode.
364 364 """
365 365 if self.commit:
366 366 if self._content is None:
367 367 self._content = self.commit.get_file_content(self.path)
368 368 content = self._content
369 369 else:
370 370 content = self._content
371 371 return content
372 372
373 373 def stream_bytes(self):
374 374 """
375 375 Returns an iterator that will stream the content of the file directly from
376 376 vcsserver without loading it to memory.
377 377 """
378 378 if self.commit:
379 379 return self.commit.get_file_content_streamed(self.path)
380 380 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
381 381
382 382 @LazyProperty
383 383 def md5(self):
384 384 """
385 385 Returns md5 of the file node.
386 386 """
387 387 return md5(self.raw_bytes)
388 388
389 389 def metadata_uncached(self):
390 390 """
391 391 Returns md5, binary flag of the file node, without any cache usage.
392 392 """
393 393
394 394 content = self.content_uncached()
395 395
396 396 is_binary = content and '\0' in content
397 397 size = 0
398 398 if content:
399 399 size = len(content)
400 400
401 401 return is_binary, md5(content), size, content
402 402
403 403 def content_uncached(self):
404 404 """
405 405 Returns lazily content of the FileNode. If possible, would try to
406 406 decode content from UTF-8.
407 407 """
408 408 if self.commit:
409 409 content = self.commit.get_file_content(self.path)
410 410 else:
411 411 content = self._content
412 412 return content
413 413
414 414 @LazyProperty
415 415 def content(self):
416 416 """
417 417 Returns lazily content of the FileNode. If possible, would try to
418 418 decode content from UTF-8.
419 419 """
420 420 content = self.raw_bytes
421 421
422 422 if self.is_binary:
423 423 return content
424 424 return safe_unicode(content)
425 425
426 426 @LazyProperty
427 427 def size(self):
428 428 if self.commit:
429 429 return self.commit.get_file_size(self.path)
430 430 raise NodeError(
431 431 "Cannot retrieve size of the file without related "
432 432 "commit attribute")
433 433
434 434 @LazyProperty
435 435 def message(self):
436 436 if self.commit:
437 437 return self.last_commit.message
438 438 raise NodeError(
439 439 "Cannot retrieve message of the file without related "
440 440 "commit attribute")
441 441
442 442 @LazyProperty
443 443 def last_commit(self):
444 444 if self.commit:
445 445 pre_load = ["author", "date", "message", "parents"]
446 446 return self.commit.get_path_commit(self.path, pre_load=pre_load)
447 447 raise NodeError(
448 448 "Cannot retrieve last commit of the file without "
449 449 "related commit attribute")
450 450
451 451 def get_mimetype(self):
452 452 """
453 453 Mimetype is calculated based on the file's content. If ``_mimetype``
454 454 attribute is available, it will be returned (backends which store
455 455 mimetypes or can easily recognize them, should set this private
456 456 attribute to indicate that type should *NOT* be calculated).
457 457 """
458 458
459 459 if hasattr(self, '_mimetype'):
460 460 if (isinstance(self._mimetype, (tuple, list,)) and
461 461 len(self._mimetype) == 2):
462 462 return self._mimetype
463 463 else:
464 464 raise NodeError('given _mimetype attribute must be an 2 '
465 465 'element list or tuple')
466 466
467 467 db = get_mimetypes_db()
468 468 mtype, encoding = db.guess_type(self.name)
469 469
470 470 if mtype is None:
471 471 if self.is_binary:
472 472 mtype = 'application/octet-stream'
473 473 encoding = None
474 474 else:
475 475 mtype = 'text/plain'
476 476 encoding = None
477 477
478 478 # try with pygments
479 479 try:
480 480 from pygments.lexers import get_lexer_for_filename
481 481 mt = get_lexer_for_filename(self.name).mimetypes
482 482 except Exception:
483 483 mt = None
484 484
485 485 if mt:
486 486 mtype = mt[0]
487 487
488 488 return mtype, encoding
489 489
490 490 @LazyProperty
491 491 def mimetype(self):
492 492 """
493 493 Wrapper around full mimetype info. It returns only type of fetched
494 494 mimetype without the encoding part. use get_mimetype function to fetch
495 495 full set of (type,encoding)
496 496 """
497 497 return self.get_mimetype()[0]
498 498
499 499 @LazyProperty
500 500 def mimetype_main(self):
501 501 return self.mimetype.split('/')[0]
502 502
503 503 @classmethod
504 504 def get_lexer(cls, filename, content=None):
505 505 from pygments import lexers
506 506
507 507 extension = filename.split('.')[-1]
508 508 lexer = None
509 509
510 510 try:
511 511 lexer = lexers.guess_lexer_for_filename(
512 512 filename, content, stripnl=False)
513 513 except lexers.ClassNotFound:
514 514 lexer = None
515 515
516 516 # try our EXTENSION_MAP
517 517 if not lexer:
518 518 try:
519 519 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
520 520 if lexer_class:
521 521 lexer = lexers.get_lexer_by_name(lexer_class[0])
522 522 except lexers.ClassNotFound:
523 523 lexer = None
524 524
525 525 if not lexer:
526 526 lexer = lexers.TextLexer(stripnl=False)
527 527
528 528 return lexer
529 529
530 530 @LazyProperty
531 531 def lexer(self):
532 532 """
533 533 Returns pygment's lexer class. Would try to guess lexer taking file's
534 534 content, name and mimetype.
535 535 """
536 536 return self.get_lexer(self.name, self.content)
537 537
538 538 @LazyProperty
539 539 def lexer_alias(self):
540 540 """
541 541 Returns first alias of the lexer guessed for this file.
542 542 """
543 543 return self.lexer.aliases[0]
544 544
545 545 @LazyProperty
546 546 def history(self):
547 547 """
548 548 Returns a list of commit for this file in which the file was changed
549 549 """
550 550 if self.commit is None:
551 551 raise NodeError('Unable to get commit for this FileNode')
552 552 return self.commit.get_path_history(self.path)
553 553
554 554 @LazyProperty
555 555 def annotate(self):
556 556 """
557 557 Returns a list of three element tuples with lineno, commit and line
558 558 """
559 559 if self.commit is None:
560 560 raise NodeError('Unable to get commit for this FileNode')
561 561 pre_load = ["author", "date", "message", "parents"]
562 562 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
563 563
564 564 @LazyProperty
565 565 def state(self):
566 566 if not self.commit:
567 567 raise NodeError(
568 568 "Cannot check state of the node if it's not "
569 569 "linked with commit")
570 570 elif self.path in (node.path for node in self.commit.added):
571 571 return NodeState.ADDED
572 572 elif self.path in (node.path for node in self.commit.changed):
573 573 return NodeState.CHANGED
574 574 else:
575 575 return NodeState.NOT_CHANGED
576 576
577 577 @LazyProperty
578 578 def is_binary(self):
579 579 """
580 580 Returns True if file has binary content.
581 581 """
582 582 if self.commit:
583 583 return self.commit.is_node_binary(self.path)
584 584 else:
585 585 raw_bytes = self._content
586 586 return raw_bytes and '\0' in raw_bytes
587 587
588 588 @LazyProperty
589 589 def extension(self):
590 590 """Returns filenode extension"""
591 591 return self.name.split('.')[-1]
592 592
593 593 @property
594 594 def is_executable(self):
595 595 """
596 596 Returns ``True`` if file has executable flag turned on.
597 597 """
598 598 return bool(self.mode & stat.S_IXUSR)
599 599
600 600 def get_largefile_node(self):
601 601 """
602 602 Try to return a Mercurial FileNode from this node. It does internal
603 603 checks inside largefile store, if that file exist there it will
604 604 create special instance of LargeFileNode which can get content from
605 605 LF store.
606 606 """
607 607 if self.commit:
608 608 return self.commit.get_largefile_node(self.path)
609 609
610 610 def lines(self, count_empty=False):
611 611 all_lines, empty_lines = 0, 0
612 612
613 613 if not self.is_binary:
614 614 content = self.content
615 615 if count_empty:
616 616 all_lines = 0
617 617 empty_lines = 0
618 618 for line in content.splitlines(True):
619 619 if line == '\n':
620 620 empty_lines += 1
621 621 all_lines += 1
622 622
623 623 return all_lines, all_lines - empty_lines
624 624 else:
625 625 # fast method
626 626 empty_lines = all_lines = content.count('\n')
627 627 if all_lines == 0 and content:
628 628 # one-line without a newline
629 629 empty_lines = all_lines = 1
630 630
631 631 return all_lines, empty_lines
632 632
633 633 def __repr__(self):
634 634 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
635 635 getattr(self.commit, 'short_id', ''))
636 636
637 637
638 638 class RemovedFileNode(FileNode):
639 639 """
640 640 Dummy FileNode class - trying to access any public attribute except path,
641 641 name, kind or state (or methods/attributes checking those two) would raise
642 642 RemovedFileNodeError.
643 643 """
644 644 ALLOWED_ATTRIBUTES = [
645 645 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
646 646 'added', 'changed', 'not_changed', 'removed'
647 647 ]
648 648
649 649 def __init__(self, path):
650 650 """
651 651 :param path: relative path to the node
652 652 """
653 653 super(RemovedFileNode, self).__init__(path=path)
654 654
655 655 def __getattribute__(self, attr):
656 656 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
657 657 return super(RemovedFileNode, self).__getattribute__(attr)
658 658 raise RemovedFileNodeError(
659 659 "Cannot access attribute %s on RemovedFileNode" % attr)
660 660
661 661 @LazyProperty
662 662 def state(self):
663 663 return NodeState.REMOVED
664 664
665 665
666 666 class DirNode(Node):
667 667 """
668 668 DirNode stores list of files and directories within this node.
669 669 Nodes may be used standalone but within repository context they
670 lazily fetch data within same repositorty's commit.
670 lazily fetch data within same repository's commit.
671 671 """
672 672
673 673 def __init__(self, path, nodes=(), commit=None):
674 674 """
675 675 Only one of ``nodes`` and ``commit`` may be given. Passing both
676 676 would raise ``NodeError`` exception.
677 677
678 678 :param path: relative path to the node
679 679 :param nodes: content may be passed to constructor
680 680 :param commit: if given, will use it to lazily fetch content
681 681 """
682 682 if nodes and commit:
683 683 raise NodeError("Cannot use both nodes and commit")
684 684 super(DirNode, self).__init__(path, NodeKind.DIR)
685 685 self.commit = commit
686 686 self._nodes = nodes
687 687
688 688 @LazyProperty
689 689 def content(self):
690 690 raise NodeError(
691 691 "%s represents a dir and has no `content` attribute" % self)
692 692
693 693 @LazyProperty
694 694 def nodes(self):
695 695 if self.commit:
696 696 nodes = self.commit.get_nodes(self.path)
697 697 else:
698 698 nodes = self._nodes
699 699 self._nodes_dict = dict((node.path, node) for node in nodes)
700 700 return sorted(nodes)
701 701
702 702 @LazyProperty
703 703 def files(self):
704 704 return sorted((node for node in self.nodes if node.is_file()))
705 705
706 706 @LazyProperty
707 707 def dirs(self):
708 708 return sorted((node for node in self.nodes if node.is_dir()))
709 709
710 710 def __iter__(self):
711 711 for node in self.nodes:
712 712 yield node
713 713
714 714 def get_node(self, path):
715 715 """
716 716 Returns node from within this particular ``DirNode``, so it is now
717 717 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
718 718 'docs'. In order to access deeper nodes one must fetch nodes between
719 719 them first - this would work::
720 720
721 721 docs = root.get_node('docs')
722 722 docs.get_node('api').get_node('index.rst')
723 723
724 724 :param: path - relative to the current node
725 725
726 726 .. note::
727 727 To access lazily (as in example above) node have to be initialized
728 728 with related commit object - without it node is out of
729 729 context and may know nothing about anything else than nearest
730 730 (located at same level) nodes.
731 731 """
732 732 try:
733 733 path = path.rstrip('/')
734 734 if path == '':
735 735 raise NodeError("Cannot retrieve node without path")
736 736 self.nodes # access nodes first in order to set _nodes_dict
737 737 paths = path.split('/')
738 738 if len(paths) == 1:
739 739 if not self.is_root():
740 740 path = '/'.join((self.path, paths[0]))
741 741 else:
742 742 path = paths[0]
743 743 return self._nodes_dict[path]
744 744 elif len(paths) > 1:
745 745 if self.commit is None:
746 746 raise NodeError("Cannot access deeper nodes without commit")
747 747 else:
748 748 path1, path2 = paths[0], '/'.join(paths[1:])
749 749 return self.get_node(path1).get_node(path2)
750 750 else:
751 751 raise KeyError
752 752 except KeyError:
753 753 raise NodeError("Node does not exist at %s" % path)
754 754
755 755 @LazyProperty
756 756 def state(self):
757 757 raise NodeError("Cannot access state of DirNode")
758 758
759 759 @LazyProperty
760 760 def size(self):
761 761 size = 0
762 762 for root, dirs, files in self.commit.walk(self.path):
763 763 for f in files:
764 764 size += f.size
765 765
766 766 return size
767 767
768 768 @LazyProperty
769 769 def last_commit(self):
770 770 if self.commit:
771 771 pre_load = ["author", "date", "message", "parents"]
772 772 return self.commit.get_path_commit(self.path, pre_load=pre_load)
773 773 raise NodeError(
774 774 "Cannot retrieve last commit of the file without "
775 775 "related commit attribute")
776 776
777 777 def __repr__(self):
778 778 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
779 779 getattr(self.commit, 'short_id', ''))
780 780
781 781
782 782 class RootNode(DirNode):
783 783 """
784 784 DirNode being the root node of the repository.
785 785 """
786 786
787 787 def __init__(self, nodes=(), commit=None):
788 788 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
789 789
790 790 def __repr__(self):
791 791 return '<%s>' % self.__class__.__name__
792 792
793 793
794 794 class SubModuleNode(Node):
795 795 """
796 796 represents a SubModule of Git or SubRepo of Mercurial
797 797 """
798 798 is_binary = False
799 799 size = 0
800 800
801 801 def __init__(self, name, url=None, commit=None, alias=None):
802 802 self.path = name
803 803 self.kind = NodeKind.SUBMODULE
804 804 self.alias = alias
805 805
806 806 # we have to use EmptyCommit here since this can point to svn/git/hg
807 807 # submodules we cannot get from repository
808 808 self.commit = EmptyCommit(str(commit), alias=alias)
809 809 self.url = url or self._extract_submodule_url()
810 810
811 811 def __repr__(self):
812 812 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
813 813 getattr(self.commit, 'short_id', ''))
814 814
815 815 def _extract_submodule_url(self):
816 816 # TODO: find a way to parse gits submodule file and extract the
817 817 # linking URL
818 818 return self.path
819 819
820 820 @LazyProperty
821 821 def name(self):
822 822 """
823 823 Returns name of the node so if its path
824 824 then only last part is returned.
825 825 """
826 826 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
827 827 return u'%s @ %s' % (org, self.commit.short_id)
828 828
829 829
830 830 class LargeFileNode(FileNode):
831 831
832 832 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
833 833 self.path = path
834 834 self.org_path = org_path
835 835 self.kind = NodeKind.LARGEFILE
836 836 self.alias = alias
837 837
838 838 def _validate_path(self, path):
839 839 """
840 840 we override check since the LargeFileNode path is system absolute
841 841 """
842 842 pass
843 843
844 844 def __repr__(self):
845 845 return '<%s %r>' % (self.__class__.__name__, self.path)
846 846
847 847 @LazyProperty
848 848 def size(self):
849 849 return os.stat(self.path).st_size
850 850
851 851 @LazyProperty
852 852 def raw_bytes(self):
853 853 with open(self.path, 'rb') as f:
854 854 content = f.read()
855 855 return content
856 856
857 857 @LazyProperty
858 858 def name(self):
859 859 """
860 860 Overwrites name to be the org lf path
861 861 """
862 862 return self.org_path
863 863
864 864 def stream_bytes(self):
865 865 with open(self.path, 'rb') as stream:
866 866 while True:
867 867 data = stream.read(16 * 1024)
868 868 if not data:
869 869 break
870 870 yield data
@@ -1,972 +1,1008 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 backend = get_backend(path[0])
230 230 repos[name] = backend(path[1], config=config,
231 231 with_wire={"cache": False})
232 232 except OSError:
233 233 continue
234 234 log.debug('found %s paths with repositories', len(repos))
235 235 return repos
236 236
237 237 def get_repos(self, all_repos=None, sort_key=None):
238 238 """
239 239 Get all repositories from db and for each repo create it's
240 240 backend instance and fill that backed with information from database
241 241
242 242 :param all_repos: list of repository names as strings
243 243 give specific repositories list, good for filtering
244 244
245 245 :param sort_key: initial sorting of repositories
246 246 """
247 247 if all_repos is None:
248 248 all_repos = self.sa.query(Repository)\
249 249 .filter(Repository.group_id == None)\
250 250 .order_by(func.lower(Repository.repo_name)).all()
251 251 repo_iter = SimpleCachedRepoList(
252 252 all_repos, repos_path=self.repos_path, order_by=sort_key)
253 253 return repo_iter
254 254
255 255 def get_repo_groups(self, all_groups=None):
256 256 if all_groups is None:
257 257 all_groups = RepoGroup.query()\
258 258 .filter(RepoGroup.group_parent_id == None).all()
259 259 return [x for x in RepoGroupList(all_groups)]
260 260
261 261 def mark_for_invalidation(self, repo_name, delete=False):
262 262 """
263 263 Mark caches of this repo invalid in the database. `delete` flag
264 264 removes the cache entries
265 265
266 266 :param repo_name: the repo_name for which caches should be marked
267 267 invalid, or deleted
268 268 :param delete: delete the entry keys instead of setting bool
269 269 flag on them, and also purge caches used by the dogpile
270 270 """
271 271 repo = Repository.get_by_repo_name(repo_name)
272 272
273 273 if repo:
274 274 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
275 275 repo_id=repo.repo_id)
276 276 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
277 277
278 278 repo_id = repo.repo_id
279 279 config = repo._config
280 280 config.set('extensions', 'largefiles', '')
281 281 repo.update_commit_cache(config=config, cs_cache=None)
282 282 if delete:
283 283 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
284 284 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid)
285 285
286 286 def toggle_following_repo(self, follow_repo_id, user_id):
287 287
288 288 f = self.sa.query(UserFollowing)\
289 289 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
290 290 .filter(UserFollowing.user_id == user_id).scalar()
291 291
292 292 if f is not None:
293 293 try:
294 294 self.sa.delete(f)
295 295 return
296 296 except Exception:
297 297 log.error(traceback.format_exc())
298 298 raise
299 299
300 300 try:
301 301 f = UserFollowing()
302 302 f.user_id = user_id
303 303 f.follows_repo_id = follow_repo_id
304 304 self.sa.add(f)
305 305 except Exception:
306 306 log.error(traceback.format_exc())
307 307 raise
308 308
309 309 def toggle_following_user(self, follow_user_id, user_id):
310 310 f = self.sa.query(UserFollowing)\
311 311 .filter(UserFollowing.follows_user_id == follow_user_id)\
312 312 .filter(UserFollowing.user_id == user_id).scalar()
313 313
314 314 if f is not None:
315 315 try:
316 316 self.sa.delete(f)
317 317 return
318 318 except Exception:
319 319 log.error(traceback.format_exc())
320 320 raise
321 321
322 322 try:
323 323 f = UserFollowing()
324 324 f.user_id = user_id
325 325 f.follows_user_id = follow_user_id
326 326 self.sa.add(f)
327 327 except Exception:
328 328 log.error(traceback.format_exc())
329 329 raise
330 330
331 331 def is_following_repo(self, repo_name, user_id, cache=False):
332 332 r = self.sa.query(Repository)\
333 333 .filter(Repository.repo_name == repo_name).scalar()
334 334
335 335 f = self.sa.query(UserFollowing)\
336 336 .filter(UserFollowing.follows_repository == r)\
337 337 .filter(UserFollowing.user_id == user_id).scalar()
338 338
339 339 return f is not None
340 340
341 341 def is_following_user(self, username, user_id, cache=False):
342 342 u = User.get_by_username(username)
343 343
344 344 f = self.sa.query(UserFollowing)\
345 345 .filter(UserFollowing.follows_user == u)\
346 346 .filter(UserFollowing.user_id == user_id).scalar()
347 347
348 348 return f is not None
349 349
350 350 def get_followers(self, repo):
351 351 repo = self._get_repo(repo)
352 352
353 353 return self.sa.query(UserFollowing)\
354 354 .filter(UserFollowing.follows_repository == repo).count()
355 355
356 356 def get_forks(self, repo):
357 357 repo = self._get_repo(repo)
358 358 return self.sa.query(Repository)\
359 359 .filter(Repository.fork == repo).count()
360 360
361 361 def get_pull_requests(self, repo):
362 362 repo = self._get_repo(repo)
363 363 return self.sa.query(PullRequest)\
364 364 .filter(PullRequest.target_repo == repo)\
365 365 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
366 366
367 367 def mark_as_fork(self, repo, fork, user):
368 368 repo = self._get_repo(repo)
369 369 fork = self._get_repo(fork)
370 370 if fork and repo.repo_id == fork.repo_id:
371 371 raise Exception("Cannot set repository as fork of itself")
372 372
373 373 if fork and repo.repo_type != fork.repo_type:
374 374 raise RepositoryError(
375 375 "Cannot set repository as fork of repository with other type")
376 376
377 377 repo.fork = fork
378 378 self.sa.add(repo)
379 379 return repo
380 380
381 381 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
382 382 dbrepo = self._get_repo(repo)
383 383 remote_uri = remote_uri or dbrepo.clone_uri
384 384 if not remote_uri:
385 385 raise Exception("This repository doesn't have a clone uri")
386 386
387 387 repo = dbrepo.scm_instance(cache=False)
388 388 repo.config.clear_section('hooks')
389 389
390 390 try:
391 391 # NOTE(marcink): add extra validation so we skip invalid urls
392 392 # this is due this tasks can be executed via scheduler without
393 393 # proper validation of remote_uri
394 394 if validate_uri:
395 395 config = make_db_config(clear_session=False)
396 396 url_validator(remote_uri, dbrepo.repo_type, config)
397 397 except InvalidCloneUrl:
398 398 raise
399 399
400 400 repo_name = dbrepo.repo_name
401 401 try:
402 402 # TODO: we need to make sure those operations call proper hooks !
403 403 repo.fetch(remote_uri)
404 404
405 405 self.mark_for_invalidation(repo_name)
406 406 except Exception:
407 407 log.error(traceback.format_exc())
408 408 raise
409 409
410 410 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
411 411 dbrepo = self._get_repo(repo)
412 412 remote_uri = remote_uri or dbrepo.push_uri
413 413 if not remote_uri:
414 414 raise Exception("This repository doesn't have a clone uri")
415 415
416 416 repo = dbrepo.scm_instance(cache=False)
417 417 repo.config.clear_section('hooks')
418 418
419 419 try:
420 420 # NOTE(marcink): add extra validation so we skip invalid urls
421 421 # this is due this tasks can be executed via scheduler without
422 422 # proper validation of remote_uri
423 423 if validate_uri:
424 424 config = make_db_config(clear_session=False)
425 425 url_validator(remote_uri, dbrepo.repo_type, config)
426 426 except InvalidCloneUrl:
427 427 raise
428 428
429 429 try:
430 430 repo.push(remote_uri)
431 431 except Exception:
432 432 log.error(traceback.format_exc())
433 433 raise
434 434
435 435 def commit_change(self, repo, repo_name, commit, user, author, message,
436 436 content, f_path):
437 437 """
438 438 Commits changes
439 439
440 440 :param repo: SCM instance
441 441
442 442 """
443 443 user = self._get_user(user)
444 444
445 445 # decoding here will force that we have proper encoded values
446 446 # in any other case this will throw exceptions and deny commit
447 447 content = safe_str(content)
448 448 path = safe_str(f_path)
449 449 # message and author needs to be unicode
450 450 # proper backend should then translate that into required type
451 451 message = safe_unicode(message)
452 452 author = safe_unicode(author)
453 453 imc = repo.in_memory_commit
454 454 imc.change(FileNode(path, content, mode=commit.get_file_mode(f_path)))
455 455 try:
456 456 # TODO: handle pre-push action !
457 457 tip = imc.commit(
458 458 message=message, author=author, parents=[commit],
459 459 branch=commit.branch)
460 460 except Exception as e:
461 461 log.error(traceback.format_exc())
462 462 raise IMCCommitError(str(e))
463 463 finally:
464 464 # always clear caches, if commit fails we want fresh object also
465 465 self.mark_for_invalidation(repo_name)
466 466
467 467 # We trigger the post-push action
468 468 hooks_utils.trigger_post_push_hook(
469 469 username=user.username, action='push_local', hook_type='post_push',
470 470 repo_name=repo_name, repo_alias=repo.alias, commit_ids=[tip.raw_id])
471 471 return tip
472 472
473 473 def _sanitize_path(self, f_path):
474 474 if f_path.startswith('/') or f_path.startswith('./') or '../' in f_path:
475 475 raise NonRelativePathError('%s is not an relative path' % f_path)
476 476 if f_path:
477 477 f_path = os.path.normpath(f_path)
478 478 return f_path
479 479
480 480 def get_dirnode_metadata(self, request, commit, dir_node):
481 481 if not dir_node.is_dir():
482 482 return []
483 483
484 484 data = []
485 485 for node in dir_node:
486 486 if not node.is_file():
487 487 # we skip file-nodes
488 488 continue
489 489
490 490 last_commit = node.last_commit
491 491 last_commit_date = last_commit.date
492 492 data.append({
493 493 'name': node.name,
494 494 'size': h.format_byte_size_binary(node.size),
495 495 'modified_at': h.format_date(last_commit_date),
496 496 'modified_ts': last_commit_date.isoformat(),
497 497 'revision': last_commit.revision,
498 498 'short_id': last_commit.short_id,
499 499 'message': h.escape(last_commit.message),
500 500 'author': h.escape(last_commit.author),
501 501 'user_profile': h.gravatar_with_user(
502 502 request, last_commit.author),
503 503 })
504 504
505 505 return data
506 506
507 507 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
508 508 extended_info=False, content=False, max_file_bytes=None):
509 509 """
510 510 recursive walk in root dir and return a set of all path in that dir
511 511 based on repository walk function
512 512
513 513 :param repo_name: name of repository
514 514 :param commit_id: commit id for which to list nodes
515 515 :param root_path: root path to list
516 516 :param flat: return as a list, if False returns a dict with description
517 517 :param extended_info: show additional info such as md5, binary, size etc
518 518 :param content: add nodes content to the return data
519 519 :param max_file_bytes: will not return file contents over this limit
520 520
521 521 """
522 522 _files = list()
523 523 _dirs = list()
524 524 try:
525 525 _repo = self._get_repo(repo_name)
526 526 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
527 527 root_path = root_path.lstrip('/')
528 528 for __, dirs, files in commit.walk(root_path):
529 529
530 530 for f in files:
531 531 _content = None
532 532 _data = f_name = f.unicode_path
533 533
534 534 if not flat:
535 535 _data = {
536 536 "name": h.escape(f_name),
537 537 "type": "file",
538 538 }
539 539 if extended_info:
540 540 _data.update({
541 541 "md5": f.md5,
542 542 "binary": f.is_binary,
543 543 "size": f.size,
544 544 "extension": f.extension,
545 545 "mimetype": f.mimetype,
546 546 "lines": f.lines()[0]
547 547 })
548 548
549 549 if content:
550 550 over_size_limit = (max_file_bytes is not None
551 551 and f.size > max_file_bytes)
552 552 full_content = None
553 553 if not f.is_binary and not over_size_limit:
554 554 full_content = safe_str(f.content)
555 555
556 556 _data.update({
557 557 "content": full_content,
558 558 })
559 559 _files.append(_data)
560 560
561 561 for d in dirs:
562 562 _data = d_name = d.unicode_path
563 563 if not flat:
564 564 _data = {
565 565 "name": h.escape(d_name),
566 566 "type": "dir",
567 567 }
568 568 if extended_info:
569 569 _data.update({
570 570 "md5": None,
571 571 "binary": None,
572 572 "size": None,
573 573 "extension": None,
574 574 })
575 575 if content:
576 576 _data.update({
577 577 "content": None
578 578 })
579 579 _dirs.append(_data)
580 580 except RepositoryError:
581 581 log.exception("Exception in get_nodes")
582 582 raise
583 583
584 584 return _dirs, _files
585 585
586 def get_quick_filter_nodes(self, repo_name, commit_id, root_path='/'):
587 """
588 Generate files for quick filter in files view
589 """
590
591 _files = list()
592 _dirs = list()
593 try:
594 _repo = self._get_repo(repo_name)
595 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
596 root_path = root_path.lstrip('/')
597 for __, dirs, files in commit.walk(root_path):
598
599 for f in files:
600
601 _data = {
602 "name": h.escape(f.unicode_path),
603 "type": "file",
604 }
605
606 _files.append(_data)
607
608 for d in dirs:
609
610 _data = {
611 "name": h.escape(d.unicode_path),
612 "type": "dir",
613 }
614
615 _dirs.append(_data)
616 except RepositoryError:
617 log.exception("Exception in get_quick_filter_nodes")
618 raise
619
620 return _dirs, _files
621
586 622 def get_node(self, repo_name, commit_id, file_path,
587 623 extended_info=False, content=False, max_file_bytes=None, cache=True):
588 624 """
589 625 retrieve single node from commit
590 626 """
591 627 try:
592 628
593 629 _repo = self._get_repo(repo_name)
594 630 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
595 631
596 632 file_node = commit.get_node(file_path)
597 633 if file_node.is_dir():
598 634 raise RepositoryError('The given path is a directory')
599 635
600 636 _content = None
601 637 f_name = file_node.unicode_path
602 638
603 639 file_data = {
604 640 "name": h.escape(f_name),
605 641 "type": "file",
606 642 }
607 643
608 644 if extended_info:
609 645 file_data.update({
610 646 "extension": file_node.extension,
611 647 "mimetype": file_node.mimetype,
612 648 })
613 649
614 650 if cache:
615 651 md5 = file_node.md5
616 652 is_binary = file_node.is_binary
617 653 size = file_node.size
618 654 else:
619 655 is_binary, md5, size, _content = file_node.metadata_uncached()
620 656
621 657 file_data.update({
622 658 "md5": md5,
623 659 "binary": is_binary,
624 660 "size": size,
625 661 })
626 662
627 663 if content and cache:
628 664 # get content + cache
629 665 size = file_node.size
630 666 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
631 667 full_content = None
632 668 if not file_node.is_binary and not over_size_limit:
633 669 full_content = safe_unicode(file_node.content)
634 670
635 671 file_data.update({
636 672 "content": full_content,
637 673 })
638 674 elif content:
639 675 # get content *without* cache
640 676 if _content is None:
641 677 is_binary, md5, size, _content = file_node.metadata_uncached()
642 678
643 679 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
644 680 full_content = None
645 681 if not is_binary and not over_size_limit:
646 682 full_content = safe_unicode(_content)
647 683
648 684 file_data.update({
649 685 "content": full_content,
650 686 })
651 687
652 688 except RepositoryError:
653 689 log.exception("Exception in get_node")
654 690 raise
655 691
656 692 return file_data
657 693
658 694 def get_fts_data(self, repo_name, commit_id, root_path='/'):
659 695 """
660 696 Fetch node tree for usage in full text search
661 697 """
662 698
663 699 tree_info = list()
664 700
665 701 try:
666 702 _repo = self._get_repo(repo_name)
667 703 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
668 704 root_path = root_path.lstrip('/')
669 705 for __, dirs, files in commit.walk(root_path):
670 706
671 707 for f in files:
672 708 is_binary, md5, size, _content = f.metadata_uncached()
673 709 _data = {
674 710 "name": f.unicode_path,
675 711 "md5": md5,
676 712 "extension": f.extension,
677 713 "binary": is_binary,
678 714 "size": size
679 715 }
680 716
681 717 tree_info.append(_data)
682 718
683 719 except RepositoryError:
684 720 log.exception("Exception in get_nodes")
685 721 raise
686 722
687 723 return tree_info
688 724
689 725 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
690 726 author=None, trigger_push_hook=True):
691 727 """
692 728 Commits given multiple nodes into repo
693 729
694 730 :param user: RhodeCode User object or user_id, the commiter
695 731 :param repo: RhodeCode Repository object
696 732 :param message: commit message
697 733 :param nodes: mapping {filename:{'content':content},...}
698 734 :param parent_commit: parent commit, can be empty than it's
699 735 initial commit
700 736 :param author: author of commit, cna be different that commiter
701 737 only for git
702 738 :param trigger_push_hook: trigger push hooks
703 739
704 740 :returns: new commited commit
705 741 """
706 742
707 743 user = self._get_user(user)
708 744 scm_instance = repo.scm_instance(cache=False)
709 745
710 746 processed_nodes = []
711 747 for f_path in nodes:
712 748 f_path = self._sanitize_path(f_path)
713 749 content = nodes[f_path]['content']
714 750 f_path = safe_str(f_path)
715 751 # decoding here will force that we have proper encoded values
716 752 # in any other case this will throw exceptions and deny commit
717 753 if isinstance(content, (basestring,)):
718 754 content = safe_str(content)
719 755 elif isinstance(content, (file, cStringIO.OutputType,)):
720 756 content = content.read()
721 757 else:
722 758 raise Exception('Content is of unrecognized type %s' % (
723 759 type(content)
724 760 ))
725 761 processed_nodes.append((f_path, content))
726 762
727 763 message = safe_unicode(message)
728 764 commiter = user.full_contact
729 765 author = safe_unicode(author) if author else commiter
730 766
731 767 imc = scm_instance.in_memory_commit
732 768
733 769 if not parent_commit:
734 770 parent_commit = EmptyCommit(alias=scm_instance.alias)
735 771
736 772 if isinstance(parent_commit, EmptyCommit):
737 773 # EmptyCommit means we we're editing empty repository
738 774 parents = None
739 775 else:
740 776 parents = [parent_commit]
741 777 # add multiple nodes
742 778 for path, content in processed_nodes:
743 779 imc.add(FileNode(path, content=content))
744 780 # TODO: handle pre push scenario
745 781 tip = imc.commit(message=message,
746 782 author=author,
747 783 parents=parents,
748 784 branch=parent_commit.branch)
749 785
750 786 self.mark_for_invalidation(repo.repo_name)
751 787 if trigger_push_hook:
752 788 hooks_utils.trigger_post_push_hook(
753 789 username=user.username, action='push_local',
754 790 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
755 791 hook_type='post_push',
756 792 commit_ids=[tip.raw_id])
757 793 return tip
758 794
759 795 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
760 796 author=None, trigger_push_hook=True):
761 797 user = self._get_user(user)
762 798 scm_instance = repo.scm_instance(cache=False)
763 799
764 800 message = safe_unicode(message)
765 801 commiter = user.full_contact
766 802 author = safe_unicode(author) if author else commiter
767 803
768 804 imc = scm_instance.in_memory_commit
769 805
770 806 if not parent_commit:
771 807 parent_commit = EmptyCommit(alias=scm_instance.alias)
772 808
773 809 if isinstance(parent_commit, EmptyCommit):
774 810 # EmptyCommit means we we're editing empty repository
775 811 parents = None
776 812 else:
777 813 parents = [parent_commit]
778 814
779 815 # add multiple nodes
780 816 for _filename, data in nodes.items():
781 817 # new filename, can be renamed from the old one, also sanitaze
782 818 # the path for any hack around relative paths like ../../ etc.
783 819 filename = self._sanitize_path(data['filename'])
784 820 old_filename = self._sanitize_path(_filename)
785 821 content = data['content']
786 822 file_mode = data.get('mode')
787 823 filenode = FileNode(old_filename, content=content, mode=file_mode)
788 824 op = data['op']
789 825 if op == 'add':
790 826 imc.add(filenode)
791 827 elif op == 'del':
792 828 imc.remove(filenode)
793 829 elif op == 'mod':
794 830 if filename != old_filename:
795 831 # TODO: handle renames more efficient, needs vcs lib changes
796 832 imc.remove(filenode)
797 833 imc.add(FileNode(filename, content=content, mode=file_mode))
798 834 else:
799 835 imc.change(filenode)
800 836
801 837 try:
802 838 # TODO: handle pre push scenario commit changes
803 839 tip = imc.commit(message=message,
804 840 author=author,
805 841 parents=parents,
806 842 branch=parent_commit.branch)
807 843 except NodeNotChangedError:
808 844 raise
809 845 except Exception as e:
810 846 log.exception("Unexpected exception during call to imc.commit")
811 847 raise IMCCommitError(str(e))
812 848 finally:
813 849 # always clear caches, if commit fails we want fresh object also
814 850 self.mark_for_invalidation(repo.repo_name)
815 851
816 852 if trigger_push_hook:
817 853 hooks_utils.trigger_post_push_hook(
818 854 username=user.username, action='push_local', hook_type='post_push',
819 855 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
820 856 commit_ids=[tip.raw_id])
821 857
822 858 return tip
823 859
824 860 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
825 861 author=None, trigger_push_hook=True):
826 862 """
827 863 Deletes given multiple nodes into `repo`
828 864
829 865 :param user: RhodeCode User object or user_id, the committer
830 866 :param repo: RhodeCode Repository object
831 867 :param message: commit message
832 868 :param nodes: mapping {filename:{'content':content},...}
833 869 :param parent_commit: parent commit, can be empty than it's initial
834 870 commit
835 871 :param author: author of commit, cna be different that commiter only
836 872 for git
837 873 :param trigger_push_hook: trigger push hooks
838 874
839 875 :returns: new commit after deletion
840 876 """
841 877
842 878 user = self._get_user(user)
843 879 scm_instance = repo.scm_instance(cache=False)
844 880
845 881 processed_nodes = []
846 882 for f_path in nodes:
847 883 f_path = self._sanitize_path(f_path)
848 884 # content can be empty but for compatabilty it allows same dicts
849 885 # structure as add_nodes
850 886 content = nodes[f_path].get('content')
851 887 processed_nodes.append((f_path, content))
852 888
853 889 message = safe_unicode(message)
854 890 commiter = user.full_contact
855 891 author = safe_unicode(author) if author else commiter
856 892
857 893 imc = scm_instance.in_memory_commit
858 894
859 895 if not parent_commit:
860 896 parent_commit = EmptyCommit(alias=scm_instance.alias)
861 897
862 898 if isinstance(parent_commit, EmptyCommit):
863 899 # EmptyCommit means we we're editing empty repository
864 900 parents = None
865 901 else:
866 902 parents = [parent_commit]
867 903 # add multiple nodes
868 904 for path, content in processed_nodes:
869 905 imc.remove(FileNode(path, content=content))
870 906
871 907 # TODO: handle pre push scenario
872 908 tip = imc.commit(message=message,
873 909 author=author,
874 910 parents=parents,
875 911 branch=parent_commit.branch)
876 912
877 913 self.mark_for_invalidation(repo.repo_name)
878 914 if trigger_push_hook:
879 915 hooks_utils.trigger_post_push_hook(
880 916 username=user.username, action='push_local', hook_type='post_push',
881 917 repo_name=repo.repo_name, repo_alias=scm_instance.alias,
882 918 commit_ids=[tip.raw_id])
883 919 return tip
884 920
885 921 def strip(self, repo, commit_id, branch):
886 922 scm_instance = repo.scm_instance(cache=False)
887 923 scm_instance.config.clear_section('hooks')
888 924 scm_instance.strip(commit_id, branch)
889 925 self.mark_for_invalidation(repo.repo_name)
890 926
891 927 def get_unread_journal(self):
892 928 return self.sa.query(UserLog).count()
893 929
894 930 @classmethod
895 931 def backend_landing_ref(cls, repo_type):
896 932 """
897 933 Return a default landing ref based on a repository type.
898 934 """
899 935
900 936 landing_ref = {
901 937 'hg': ('branch:default', 'default'),
902 938 'git': ('branch:master', 'master'),
903 939 'svn': ('rev:tip', 'latest tip'),
904 940 'default': ('rev:tip', 'latest tip'),
905 941 }
906 942
907 943 return landing_ref.get(repo_type) or landing_ref['default']
908 944
909 945 def get_repo_landing_revs(self, translator, repo=None):
910 946 """
911 947 Generates select option with tags branches and bookmarks (for hg only)
912 948 grouped by type
913 949
914 950 :param repo:
915 951 """
916 952 _ = translator
917 953 repo = self._get_repo(repo)
918 954
919 955 if repo:
920 956 repo_type = repo.repo_type
921 957 else:
922 958 repo_type = 'default'
923 959
924 960 default_landing_ref, landing_ref_lbl = self.backend_landing_ref(repo_type)
925 961
926 962 default_ref_options = [
927 963 [default_landing_ref, landing_ref_lbl]
928 964 ]
929 965 default_choices = [
930 966 default_landing_ref
931 967 ]
932 968
933 969 if not repo:
934 970 return default_choices, default_ref_options
935 971
936 972 repo = repo.scm_instance()
937 973
938 974 ref_options = [('rev:tip', 'latest tip')]
939 975 choices = ['rev:tip']
940 976
941 977 # branches
942 978 branch_group = [(u'branch:%s' % safe_unicode(b), safe_unicode(b)) for b in repo.branches]
943 979 if not branch_group:
944 980 # new repo, or without maybe a branch?
945 981 branch_group = default_ref_options
946 982
947 983 branches_group = (branch_group, _("Branches"))
948 984 ref_options.append(branches_group)
949 985 choices.extend([x[0] for x in branches_group[0]])
950 986
951 987 # bookmarks for HG
952 988 if repo.alias == 'hg':
953 989 bookmarks_group = (
954 990 [(u'book:%s' % safe_unicode(b), safe_unicode(b))
955 991 for b in repo.bookmarks],
956 992 _("Bookmarks"))
957 993 ref_options.append(bookmarks_group)
958 994 choices.extend([x[0] for x in bookmarks_group[0]])
959 995
960 996 # tags
961 997 tags_group = (
962 998 [(u'tag:%s' % safe_unicode(t), safe_unicode(t))
963 999 for t in repo.tags],
964 1000 _("Tags"))
965 1001 ref_options.append(tags_group)
966 1002 choices.extend([x[0] for x in tags_group[0]])
967 1003
968 1004 return choices, ref_options
969 1005
970 1006 def get_server_info(self, environ=None):
971 1007 server_info = get_system_info(environ)
972 1008 return server_info
General Comments 0
You need to be logged in to leave comments. Login now