##// END OF EJS Templates
fix(submodules): fixed an error if reaching out submodule path....
super-admin -
r5261:550d6219 default
parent child Browse files
Show More
@@ -1,1583 +1,1584 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import itertools
20 20 import logging
21 21 import os
22 22 import collections
23 23 import urllib.request
24 24 import urllib.parse
25 25 import urllib.error
26 26 import pathlib
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 import rhodecode
34 34 from rhodecode.apps._base import RepoAppView
35 35
36 36
37 37 from rhodecode.lib import diffs, helpers as h, rc_cache
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.hash_utils import sha1_safe
40 40 from rhodecode.lib.rc_cache.archive_cache import get_archival_cache_store, get_archival_config, ReentrantLock
41 41 from rhodecode.lib.str_utils import safe_bytes, convert_special_chars
42 42 from rhodecode.lib.view_utils import parse_path_ref
43 43 from rhodecode.lib.exceptions import NonRelativePathError
44 44 from rhodecode.lib.codeblocks import (
45 45 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
46 46 from rhodecode.lib.utils2 import convert_line_endings, detect_mode
47 47 from rhodecode.lib.type_utils import str2bool
48 48 from rhodecode.lib.str_utils import safe_str, safe_int
49 49 from rhodecode.lib.auth import (
50 50 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
51 51 from rhodecode.lib.vcs import path as vcspath
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit
53 53 from rhodecode.lib.vcs.conf import settings
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55 from rhodecode.lib.vcs.exceptions import (
56 56 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
57 57 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
58 58 NodeDoesNotExistError, CommitError, NodeError)
59 59
60 60 from rhodecode.model.scm import ScmModel
61 61 from rhodecode.model.db import Repository
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 def get_archive_name(db_repo_id, db_repo_name, commit_sha, ext, subrepos=False, path_sha='', with_hash=True):
67 67 # original backward compat name of archive
68 68 clean_name = safe_str(convert_special_chars(db_repo_name).replace('/', '_'))
69 69
70 70 # e.g vcsserver-id-abcd-sub-1-abcfdef-archive-all.zip
71 71 # vcsserver-id-abcd-sub-0-abcfdef-COMMIT_SHA-PATH_SHA.zip
72 72 id_sha = sha1_safe(str(db_repo_id))[:4]
73 73 sub_repo = 'sub-1' if subrepos else 'sub-0'
74 74 commit = commit_sha if with_hash else 'archive'
75 75 path_marker = (path_sha if with_hash else '') or 'all'
76 76 archive_name = f'{clean_name}-id-{id_sha}-{sub_repo}-{commit}-{path_marker}{ext}'
77 77
78 78 return archive_name
79 79
80 80
81 81 def get_path_sha(at_path):
82 82 return safe_str(sha1_safe(at_path)[:8])
83 83
84 84
85 85 def _get_archive_spec(fname):
86 86 log.debug('Detecting archive spec for: `%s`', fname)
87 87
88 88 fileformat = None
89 89 ext = None
90 90 content_type = None
91 91 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
92 92
93 93 if fname.endswith(extension):
94 94 fileformat = a_type
95 95 log.debug('archive is of type: %s', fileformat)
96 96 ext = extension
97 97 break
98 98
99 99 if not fileformat:
100 100 raise ValueError()
101 101
102 102 # left over part of whole fname is the commit
103 103 commit_id = fname[:-len(ext)]
104 104
105 105 return commit_id, ext, fileformat, content_type
106 106
107 107
108 108 class RepoFilesView(RepoAppView):
109 109
110 110 @staticmethod
111 111 def adjust_file_path_for_svn(f_path, repo):
112 112 """
113 113 Computes the relative path of `f_path`.
114 114
115 115 This is mainly based on prefix matching of the recognized tags and
116 116 branches in the underlying repository.
117 117 """
118 118 tags_and_branches = itertools.chain(
119 119 repo.branches.keys(),
120 120 repo.tags.keys())
121 121 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
122 122
123 123 for name in tags_and_branches:
124 124 if f_path.startswith(f'{name}/'):
125 125 f_path = vcspath.relpath(f_path, name)
126 126 break
127 127 return f_path
128 128
129 129 def load_default_context(self):
130 130 c = self._get_local_tmpl_context(include_app_defaults=True)
131 131 c.rhodecode_repo = self.rhodecode_vcs_repo
132 132 c.enable_downloads = self.db_repo.enable_downloads
133 133 return c
134 134
135 135 def _ensure_not_locked(self, commit_id='tip'):
136 136 _ = self.request.translate
137 137
138 138 repo = self.db_repo
139 139 if repo.enable_locking and repo.locked[0]:
140 140 h.flash(_('This repository has been locked by %s on %s')
141 141 % (h.person_by_id(repo.locked[0]),
142 142 h.format_date(h.time_to_datetime(repo.locked[1]))),
143 143 'warning')
144 144 files_url = h.route_path(
145 145 'repo_files:default_path',
146 146 repo_name=self.db_repo_name, commit_id=commit_id)
147 147 raise HTTPFound(files_url)
148 148
149 149 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
150 150 _ = self.request.translate
151 151
152 152 if not is_head:
153 153 message = _('Cannot modify file. '
154 154 'Given commit `{}` is not head of a branch.').format(commit_id)
155 155 h.flash(message, category='warning')
156 156
157 157 if json_mode:
158 158 return message
159 159
160 160 files_url = h.route_path(
161 161 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
162 162 f_path=f_path)
163 163 raise HTTPFound(files_url)
164 164
165 165 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
166 166 _ = self.request.translate
167 167
168 168 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
169 169 self.db_repo_name, branch_name)
170 170 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
171 171 message = _('Branch `{}` changes forbidden by rule {}.').format(
172 172 h.escape(branch_name), h.escape(rule))
173 173 h.flash(message, 'warning')
174 174
175 175 if json_mode:
176 176 return message
177 177
178 178 files_url = h.route_path(
179 179 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
180 180
181 181 raise HTTPFound(files_url)
182 182
183 183 def _get_commit_and_path(self):
184 184 default_commit_id = self.db_repo.landing_ref_name
185 185 default_f_path = '/'
186 186
187 187 commit_id = self.request.matchdict.get(
188 188 'commit_id', default_commit_id)
189 189 f_path = self._get_f_path(self.request.matchdict, default_f_path)
190 190 return commit_id, f_path
191 191
192 192 def _get_default_encoding(self, c):
193 193 enc_list = getattr(c, 'default_encodings', [])
194 194 return enc_list[0] if enc_list else 'UTF-8'
195 195
196 196 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
197 197 """
198 198 This is a safe way to get commit. If an error occurs it redirects to
199 199 tip with proper message
200 200
201 201 :param commit_id: id of commit to fetch
202 202 :param redirect_after: toggle redirection
203 203 """
204 204 _ = self.request.translate
205 205
206 206 try:
207 207 return self.rhodecode_vcs_repo.get_commit(commit_id)
208 208 except EmptyRepositoryError:
209 209 if not redirect_after:
210 210 return None
211 211
212 212 add_new = upload_new = ""
213 213 if h.HasRepoPermissionAny(
214 214 'repository.write', 'repository.admin')(self.db_repo_name):
215 215 _url = h.route_path(
216 216 'repo_files_add_file',
217 217 repo_name=self.db_repo_name, commit_id=0, f_path='')
218 218 add_new = h.link_to(
219 219 _('add a new file'), _url, class_="alert-link")
220 220
221 221 _url_upld = h.route_path(
222 222 'repo_files_upload_file',
223 223 repo_name=self.db_repo_name, commit_id=0, f_path='')
224 224 upload_new = h.link_to(
225 225 _('upload a new file'), _url_upld, class_="alert-link")
226 226
227 227 h.flash(h.literal(
228 228 _('There are no files yet. Click here to %s or %s.') % (add_new, upload_new)), category='warning')
229 229 raise HTTPFound(
230 230 h.route_path('repo_summary', repo_name=self.db_repo_name))
231 231
232 232 except (CommitDoesNotExistError, LookupError) as e:
233 233 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
234 234 h.flash(msg, category='error')
235 235 raise HTTPNotFound()
236 236 except RepositoryError as e:
237 237 h.flash(h.escape(safe_str(e)), category='error')
238 238 raise HTTPNotFound()
239 239
240 240 def _get_filenode_or_redirect(self, commit_obj, path, pre_load=None):
241 241 """
242 242 Returns file_node, if error occurs or given path is directory,
243 243 it'll redirect to top level path
244 244 """
245 245 _ = self.request.translate
246 246
247 247 try:
248 248 file_node = commit_obj.get_node(path, pre_load=pre_load)
249 249 if file_node.is_dir():
250 250 raise RepositoryError('The given path is a directory')
251 251 except CommitDoesNotExistError:
252 252 log.exception('No such commit exists for this repository')
253 253 h.flash(_('No such commit exists for this repository'), category='error')
254 254 raise HTTPNotFound()
255 255 except RepositoryError as e:
256 256 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
257 257 h.flash(h.escape(safe_str(e)), category='error')
258 258 raise HTTPNotFound()
259 259
260 260 return file_node
261 261
262 262 def _is_valid_head(self, commit_id, repo, landing_ref):
263 263 branch_name = sha_commit_id = ''
264 264 is_head = False
265 265 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
266 266
267 267 for _branch_name, branch_commit_id in repo.branches.items():
268 268 # simple case we pass in branch name, it's a HEAD
269 269 if commit_id == _branch_name:
270 270 is_head = True
271 271 branch_name = _branch_name
272 272 sha_commit_id = branch_commit_id
273 273 break
274 274 # case when we pass in full sha commit_id, which is a head
275 275 elif commit_id == branch_commit_id:
276 276 is_head = True
277 277 branch_name = _branch_name
278 278 sha_commit_id = branch_commit_id
279 279 break
280 280
281 281 if h.is_svn(repo) and not repo.is_empty():
282 282 # Note: Subversion only has one head.
283 283 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
284 284 is_head = True
285 285 return branch_name, sha_commit_id, is_head
286 286
287 287 # checked branches, means we only need to try to get the branch/commit_sha
288 288 if repo.is_empty():
289 289 is_head = True
290 290 branch_name = landing_ref
291 291 sha_commit_id = EmptyCommit().raw_id
292 292 else:
293 293 commit = repo.get_commit(commit_id=commit_id)
294 294 if commit:
295 295 branch_name = commit.branch
296 296 sha_commit_id = commit.raw_id
297 297
298 298 return branch_name, sha_commit_id, is_head
299 299
300 300 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
301 301
302 302 repo_id = self.db_repo.repo_id
303 303 force_recache = self.get_recache_flag()
304 304
305 305 cache_seconds = safe_int(
306 306 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
307 307 cache_on = not force_recache and cache_seconds > 0
308 308 log.debug(
309 309 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
310 310 'with caching: %s[TTL: %ss]' % (
311 311 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
312 312
313 313 cache_namespace_uid = f'repo.{rc_cache.FILE_TREE_CACHE_VER}.{repo_id}'
314 314 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
315 315
316 316 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
317 317 def compute_file_tree(_name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
318 318 log.debug('Generating cached file tree at for repo_id: %s, %s, %s',
319 319 _repo_id, _commit_id, _f_path)
320 320
321 321 c.full_load = _full_load
322 322 return render(
323 323 'rhodecode:templates/files/files_browser_tree.mako',
324 324 self._get_template_context(c), self.request, _at_rev)
325 325
326 326 return compute_file_tree(
327 327 self.db_repo.repo_name_hash, self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
328 328
329 329 def create_pure_path(self, *parts):
330 330 # Split paths and sanitize them, removing any ../ etc
331 331 sanitized_path = [
332 332 x for x in pathlib.PurePath(*parts).parts
333 333 if x not in ['.', '..']]
334 334
335 335 pure_path = pathlib.PurePath(*sanitized_path)
336 336 return pure_path
337 337
338 338 def _is_lf_enabled(self, target_repo):
339 339 lf_enabled = False
340 340
341 341 lf_key_for_vcs_map = {
342 342 'hg': 'extensions_largefiles',
343 343 'git': 'vcs_git_lfs_enabled'
344 344 }
345 345
346 346 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
347 347
348 348 if lf_key_for_vcs:
349 349 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
350 350
351 351 return lf_enabled
352 352
353 353 @LoginRequired()
354 354 @HasRepoPermissionAnyDecorator(
355 355 'repository.read', 'repository.write', 'repository.admin')
356 356 def repo_archivefile(self):
357 357 # archive cache config
358 358 from rhodecode import CONFIG
359 359 _ = self.request.translate
360 360 self.load_default_context()
361 361 default_at_path = '/'
362 362 fname = self.request.matchdict['fname']
363 363 subrepos = self.request.GET.get('subrepos') == 'true'
364 364 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
365 365 at_path = self.request.GET.get('at_path') or default_at_path
366 366
367 367 if not self.db_repo.enable_downloads:
368 368 return Response(_('Downloads disabled'))
369 369
370 370 try:
371 371 commit_id, ext, fileformat, content_type = \
372 372 _get_archive_spec(fname)
373 373 except ValueError:
374 374 return Response(_('Unknown archive type for: `{}`').format(
375 375 h.escape(fname)))
376 376
377 377 try:
378 378 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
379 379 except CommitDoesNotExistError:
380 380 return Response(_('Unknown commit_id {}').format(
381 381 h.escape(commit_id)))
382 382 except EmptyRepositoryError:
383 383 return Response(_('Empty repository'))
384 384
385 385 # we used a ref, or a shorter version, lets redirect client ot use explicit hash
386 386 if commit_id != commit.raw_id:
387 387 fname=f'{commit.raw_id}{ext}'
388 388 raise HTTPFound(self.request.current_route_path(fname=fname))
389 389
390 390 try:
391 391 at_path = commit.get_node(at_path).path or default_at_path
392 392 except Exception:
393 393 return Response(_('No node at path {} for this repository').format(h.escape(at_path)))
394 394
395 395 path_sha = get_path_sha(at_path)
396 396
397 397 # used for cache etc, consistent unique archive name
398 398 archive_name_key = get_archive_name(
399 399 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
400 400 path_sha=path_sha, with_hash=True)
401 401
402 402 if not with_hash:
403 403 path_sha = ''
404 404
405 405 # what end client gets served
406 406 response_archive_name = get_archive_name(
407 407 self.db_repo.repo_id, self.db_repo_name, commit_sha=commit.short_id, ext=ext, subrepos=subrepos,
408 408 path_sha=path_sha, with_hash=with_hash)
409 409
410 410 # remove extension from our archive directory name
411 411 archive_dir_name = response_archive_name[:-len(ext)]
412 412
413 413 archive_cache_disable = self.request.GET.get('no_cache')
414 414
415 415 d_cache = get_archival_cache_store(config=CONFIG)
416 416
417 417 # NOTE: we get the config to pass to a call to lazy-init the SAME type of cache on vcsserver
418 418 d_cache_conf = get_archival_config(config=CONFIG)
419 419
420 420 reentrant_lock_key = archive_name_key + '.lock'
421 421 with ReentrantLock(d_cache, reentrant_lock_key):
422 422 # This is also a cache key
423 423 use_cached_archive = False
424 424 if archive_name_key in d_cache and not archive_cache_disable:
425 425 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
426 426 use_cached_archive = True
427 427 log.debug('Found cached archive as key=%s tag=%s, serving archive from cache reader=%s',
428 428 archive_name_key, tag, reader.name)
429 429 else:
430 430 reader = None
431 431 log.debug('Archive with key=%s is not yet cached, creating one now...', archive_name_key)
432 432
433 433 # generate new archive, as previous was not found in the cache
434 434 if not reader:
435 435
436 436 try:
437 437 commit.archive_repo(archive_name_key, archive_dir_name=archive_dir_name,
438 438 kind=fileformat, subrepos=subrepos,
439 439 archive_at_path=at_path, cache_config=d_cache_conf)
440 440 except ImproperArchiveTypeError:
441 441 return _('Unknown archive type')
442 442
443 443 reader, tag = d_cache.get(archive_name_key, read=True, tag=True, retry=True)
444 444
445 445 if not reader:
446 446 raise ValueError('archive cache reader is empty, failed to fetch file from distributed archive cache')
447 447
448 448 def archive_iterator(_reader, block_size: int = 4096*512):
449 449 # 4096 * 64 = 64KB
450 450 while 1:
451 451 data = _reader.read(block_size)
452 452 if not data:
453 453 break
454 454 yield data
455 455
456 456 response = Response(app_iter=archive_iterator(reader))
457 457 response.content_disposition = f'attachment; filename={response_archive_name}'
458 458 response.content_type = str(content_type)
459 459
460 460 try:
461 461 return response
462 462 finally:
463 463 # store download action
464 464 audit_logger.store_web(
465 465 'repo.archive.download', action_data={
466 466 'user_agent': self.request.user_agent,
467 467 'archive_name': archive_name_key,
468 468 'archive_spec': fname,
469 469 'archive_cached': use_cached_archive},
470 470 user=self._rhodecode_user,
471 471 repo=self.db_repo,
472 472 commit=True
473 473 )
474 474
475 475 def _get_file_node(self, commit_id, f_path):
476 476 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
477 477 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
478 478 try:
479 479 node = commit.get_node(f_path)
480 480 if node.is_dir():
481 481 raise NodeError(f'{node} path is a {type(node)} not a file')
482 482 except NodeDoesNotExistError:
483 483 commit = EmptyCommit(
484 484 commit_id=commit_id,
485 485 idx=commit.idx,
486 486 repo=commit.repository,
487 487 alias=commit.repository.alias,
488 488 message=commit.message,
489 489 author=commit.author,
490 490 date=commit.date)
491 491 node = FileNode(safe_bytes(f_path), b'', commit=commit)
492 492 else:
493 493 commit = EmptyCommit(
494 494 repo=self.rhodecode_vcs_repo,
495 495 alias=self.rhodecode_vcs_repo.alias)
496 496 node = FileNode(safe_bytes(f_path), b'', commit=commit)
497 497 return node
498 498
499 499 @LoginRequired()
500 500 @HasRepoPermissionAnyDecorator(
501 501 'repository.read', 'repository.write', 'repository.admin')
502 502 def repo_files_diff(self):
503 503 c = self.load_default_context()
504 504 f_path = self._get_f_path(self.request.matchdict)
505 505 diff1 = self.request.GET.get('diff1', '')
506 506 diff2 = self.request.GET.get('diff2', '')
507 507
508 508 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
509 509
510 510 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
511 511 line_context = self.request.GET.get('context', 3)
512 512
513 513 if not any((diff1, diff2)):
514 514 h.flash(
515 515 'Need query parameter "diff1" or "diff2" to generate a diff.',
516 516 category='error')
517 517 raise HTTPBadRequest()
518 518
519 519 c.action = self.request.GET.get('diff')
520 520 if c.action not in ['download', 'raw']:
521 521 compare_url = h.route_path(
522 522 'repo_compare',
523 523 repo_name=self.db_repo_name,
524 524 source_ref_type='rev',
525 525 source_ref=diff1,
526 526 target_repo=self.db_repo_name,
527 527 target_ref_type='rev',
528 528 target_ref=diff2,
529 529 _query=dict(f_path=f_path))
530 530 # redirect to new view if we render diff
531 531 raise HTTPFound(compare_url)
532 532
533 533 try:
534 534 node1 = self._get_file_node(diff1, path1)
535 535 node2 = self._get_file_node(diff2, f_path)
536 536 except (RepositoryError, NodeError):
537 537 log.exception("Exception while trying to get node from repository")
538 538 raise HTTPFound(
539 539 h.route_path('repo_files', repo_name=self.db_repo_name,
540 540 commit_id='tip', f_path=f_path))
541 541
542 542 if all(isinstance(node.commit, EmptyCommit)
543 543 for node in (node1, node2)):
544 544 raise HTTPNotFound()
545 545
546 546 c.commit_1 = node1.commit
547 547 c.commit_2 = node2.commit
548 548
549 549 if c.action == 'download':
550 550 _diff = diffs.get_gitdiff(node1, node2,
551 551 ignore_whitespace=ignore_whitespace,
552 552 context=line_context)
553 553 # NOTE: this was using diff_format='gitdiff'
554 554 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
555 555
556 556 response = Response(self.path_filter.get_raw_patch(diff))
557 557 response.content_type = 'text/plain'
558 558 response.content_disposition = (
559 559 f'attachment; filename={f_path}_{diff1}_vs_{diff2}.diff'
560 560 )
561 561 charset = self._get_default_encoding(c)
562 562 if charset:
563 563 response.charset = charset
564 564 return response
565 565
566 566 elif c.action == 'raw':
567 567 _diff = diffs.get_gitdiff(node1, node2,
568 568 ignore_whitespace=ignore_whitespace,
569 569 context=line_context)
570 570 # NOTE: this was using diff_format='gitdiff'
571 571 diff = diffs.DiffProcessor(_diff, diff_format='newdiff')
572 572
573 573 response = Response(self.path_filter.get_raw_patch(diff))
574 574 response.content_type = 'text/plain'
575 575 charset = self._get_default_encoding(c)
576 576 if charset:
577 577 response.charset = charset
578 578 return response
579 579
580 580 # in case we ever end up here
581 581 raise HTTPNotFound()
582 582
583 583 @LoginRequired()
584 584 @HasRepoPermissionAnyDecorator(
585 585 'repository.read', 'repository.write', 'repository.admin')
586 586 def repo_files_diff_2way_redirect(self):
587 587 """
588 588 Kept only to make OLD links work
589 589 """
590 590 f_path = self._get_f_path_unchecked(self.request.matchdict)
591 591 diff1 = self.request.GET.get('diff1', '')
592 592 diff2 = self.request.GET.get('diff2', '')
593 593
594 594 if not any((diff1, diff2)):
595 595 h.flash(
596 596 'Need query parameter "diff1" or "diff2" to generate a diff.',
597 597 category='error')
598 598 raise HTTPBadRequest()
599 599
600 600 compare_url = h.route_path(
601 601 'repo_compare',
602 602 repo_name=self.db_repo_name,
603 603 source_ref_type='rev',
604 604 source_ref=diff1,
605 605 target_ref_type='rev',
606 606 target_ref=diff2,
607 607 _query=dict(f_path=f_path, diffmode='sideside',
608 608 target_repo=self.db_repo_name,))
609 609 raise HTTPFound(compare_url)
610 610
611 611 @LoginRequired()
612 612 def repo_files_default_commit_redirect(self):
613 613 """
614 614 Special page that redirects to the landing page of files based on the default
615 615 commit for repository
616 616 """
617 617 c = self.load_default_context()
618 618 ref_name = c.rhodecode_db_repo.landing_ref_name
619 619 landing_url = h.repo_files_by_ref_url(
620 620 c.rhodecode_db_repo.repo_name,
621 621 c.rhodecode_db_repo.repo_type,
622 622 f_path='',
623 623 ref_name=ref_name,
624 624 commit_id='tip',
625 625 query=dict(at=ref_name)
626 626 )
627 627
628 628 raise HTTPFound(landing_url)
629 629
630 630 @LoginRequired()
631 631 @HasRepoPermissionAnyDecorator(
632 632 'repository.read', 'repository.write', 'repository.admin')
633 633 def repo_files(self):
634 634 c = self.load_default_context()
635 635
636 636 view_name = getattr(self.request.matched_route, 'name', None)
637 637
638 638 c.annotate = view_name == 'repo_files:annotated'
639 639 # default is false, but .rst/.md files later are auto rendered, we can
640 640 # overwrite auto rendering by setting this GET flag
641 641 c.renderer = view_name == 'repo_files:rendered' or not self.request.GET.get('no-render', False)
642 642
643 643 commit_id, f_path = self._get_commit_and_path()
644 644
645 645 c.commit = self._get_commit_or_redirect(commit_id)
646 646 c.branch = self.request.GET.get('branch', None)
647 647 c.f_path = f_path
648 648 at_rev = self.request.GET.get('at')
649 649
650 # prev link
651 try:
652 prev_commit = c.commit.prev(c.branch)
653 c.prev_commit = prev_commit
654 c.url_prev = h.route_path(
655 'repo_files', repo_name=self.db_repo_name,
656 commit_id=prev_commit.raw_id, f_path=f_path)
657 if c.branch:
658 c.url_prev += '?branch=%s' % c.branch
659 except (CommitDoesNotExistError, VCSError):
660 c.url_prev = '#'
661 c.prev_commit = EmptyCommit()
662
663 # next link
664 try:
665 next_commit = c.commit.next(c.branch)
666 c.next_commit = next_commit
667 c.url_next = h.route_path(
668 'repo_files', repo_name=self.db_repo_name,
669 commit_id=next_commit.raw_id, f_path=f_path)
670 if c.branch:
671 c.url_next += '?branch=%s' % c.branch
672 except (CommitDoesNotExistError, VCSError):
673 c.url_next = '#'
674 c.next_commit = EmptyCommit()
675
676 650 # files or dirs
677 651 try:
678 652 c.file = c.commit.get_node(f_path, pre_load=['is_binary', 'size', 'data'])
679 653
680 654 c.file_author = True
681 655 c.file_tree = ''
682 656
657 # prev link
658 try:
659 prev_commit = c.commit.prev(c.branch)
660 c.prev_commit = prev_commit
661 c.url_prev = h.route_path(
662 'repo_files', repo_name=self.db_repo_name,
663 commit_id=prev_commit.raw_id, f_path=f_path)
664 if c.branch:
665 c.url_prev += '?branch=%s' % c.branch
666 except (CommitDoesNotExistError, VCSError):
667 c.url_prev = '#'
668 c.prev_commit = EmptyCommit()
669
670 # next link
671 try:
672 next_commit = c.commit.next(c.branch)
673 c.next_commit = next_commit
674 c.url_next = h.route_path(
675 'repo_files', repo_name=self.db_repo_name,
676 commit_id=next_commit.raw_id, f_path=f_path)
677 if c.branch:
678 c.url_next += '?branch=%s' % c.branch
679 except (CommitDoesNotExistError, VCSError):
680 c.url_next = '#'
681 c.next_commit = EmptyCommit()
682
683 683 # load file content
684 684 if c.file.is_file():
685
685 686 c.lf_node = {}
686 687
687 688 has_lf_enabled = self._is_lf_enabled(self.db_repo)
688 689 if has_lf_enabled:
689 690 c.lf_node = c.file.get_largefile_node()
690 691
691 692 c.file_source_page = 'true'
692 693 c.file_last_commit = c.file.last_commit
693 694
694 695 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
695 696
696 697 if not (c.file_size_too_big or c.file.is_binary):
697 698 if c.annotate: # annotation has precedence over renderer
698 699 c.annotated_lines = filenode_as_annotated_lines_tokens(
699 700 c.file
700 701 )
701 702 else:
702 703 c.renderer = (
703 704 c.renderer and h.renderer_from_filename(c.file.path)
704 705 )
705 706 if not c.renderer:
706 707 c.lines = filenode_as_lines_tokens(c.file)
707 708
708 709 _branch_name, _sha_commit_id, is_head = \
709 710 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
710 711 landing_ref=self.db_repo.landing_ref_name)
711 712 c.on_branch_head = is_head
712 713
713 714 branch = c.commit.branch if (
714 715 c.commit.branch and '/' not in c.commit.branch) else None
715 716 c.branch_or_raw_id = branch or c.commit.raw_id
716 717 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
717 718
718 719 author = c.file_last_commit.author
719 720 c.authors = [[
720 721 h.email(author),
721 722 h.person(author, 'username_or_name_or_email'),
722 723 1
723 724 ]]
724 725
725 726 else: # load tree content at path
726 727 c.file_source_page = 'false'
727 728 c.authors = []
728 729 # this loads a simple tree without metadata to speed things up
729 730 # later via ajax we call repo_nodetree_full and fetch whole
730 731 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
731 732
732 733 c.readme_data, c.readme_file = \
733 734 self._get_readme_data(self.db_repo, c.visual.default_renderer,
734 735 c.commit.raw_id, f_path)
735 736
736 737 except RepositoryError as e:
737 738 h.flash(h.escape(safe_str(e)), category='error')
738 739 raise HTTPNotFound()
739 740
740 741 if self.request.environ.get('HTTP_X_PJAX'):
741 742 html = render('rhodecode:templates/files/files_pjax.mako',
742 743 self._get_template_context(c), self.request)
743 744 else:
744 745 html = render('rhodecode:templates/files/files.mako',
745 746 self._get_template_context(c), self.request)
746 747 return Response(html)
747 748
748 749 @HasRepoPermissionAnyDecorator(
749 750 'repository.read', 'repository.write', 'repository.admin')
750 751 def repo_files_annotated_previous(self):
751 752 self.load_default_context()
752 753
753 754 commit_id, f_path = self._get_commit_and_path()
754 755 commit = self._get_commit_or_redirect(commit_id)
755 756 prev_commit_id = commit.raw_id
756 757 line_anchor = self.request.GET.get('line_anchor')
757 758 is_file = False
758 759 try:
759 760 _file = commit.get_node(f_path)
760 761 is_file = _file.is_file()
761 762 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
762 763 pass
763 764
764 765 if is_file:
765 766 history = commit.get_path_history(f_path)
766 767 prev_commit_id = history[1].raw_id \
767 768 if len(history) > 1 else prev_commit_id
768 769 prev_url = h.route_path(
769 770 'repo_files:annotated', repo_name=self.db_repo_name,
770 771 commit_id=prev_commit_id, f_path=f_path,
771 772 _anchor=f'L{line_anchor}')
772 773
773 774 raise HTTPFound(prev_url)
774 775
775 776 @LoginRequired()
776 777 @HasRepoPermissionAnyDecorator(
777 778 'repository.read', 'repository.write', 'repository.admin')
778 779 def repo_nodetree_full(self):
779 780 """
780 781 Returns rendered html of file tree that contains commit date,
781 782 author, commit_id for the specified combination of
782 783 repo, commit_id and file path
783 784 """
784 785 c = self.load_default_context()
785 786
786 787 commit_id, f_path = self._get_commit_and_path()
787 788 commit = self._get_commit_or_redirect(commit_id)
788 789 try:
789 790 dir_node = commit.get_node(f_path)
790 791 except RepositoryError as e:
791 792 return Response(f'error: {h.escape(safe_str(e))}')
792 793
793 794 if dir_node.is_file():
794 795 return Response('')
795 796
796 797 c.file = dir_node
797 798 c.commit = commit
798 799 at_rev = self.request.GET.get('at')
799 800
800 801 html = self._get_tree_at_commit(
801 802 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
802 803
803 804 return Response(html)
804 805
805 806 def _get_attachement_headers(self, f_path):
806 807 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
807 808 safe_path = f_name.replace('"', '\\"')
808 809 encoded_path = urllib.parse.quote(f_name)
809 810
810 811 return "attachment; " \
811 812 "filename=\"{}\"; " \
812 813 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
813 814
814 815 @LoginRequired()
815 816 @HasRepoPermissionAnyDecorator(
816 817 'repository.read', 'repository.write', 'repository.admin')
817 818 def repo_file_raw(self):
818 819 """
819 820 Action for show as raw, some mimetypes are "rendered",
820 821 those include images, icons.
821 822 """
822 823 c = self.load_default_context()
823 824
824 825 commit_id, f_path = self._get_commit_and_path()
825 826 commit = self._get_commit_or_redirect(commit_id)
826 827 file_node = self._get_filenode_or_redirect(commit, f_path)
827 828
828 829 raw_mimetype_mapping = {
829 830 # map original mimetype to a mimetype used for "show as raw"
830 831 # you can also provide a content-disposition to override the
831 832 # default "attachment" disposition.
832 833 # orig_type: (new_type, new_dispo)
833 834
834 835 # show images inline:
835 836 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
836 837 # for example render an SVG with javascript inside or even render
837 838 # HTML.
838 839 'image/x-icon': ('image/x-icon', 'inline'),
839 840 'image/png': ('image/png', 'inline'),
840 841 'image/gif': ('image/gif', 'inline'),
841 842 'image/jpeg': ('image/jpeg', 'inline'),
842 843 'application/pdf': ('application/pdf', 'inline'),
843 844 }
844 845
845 846 mimetype = file_node.mimetype
846 847 try:
847 848 mimetype, disposition = raw_mimetype_mapping[mimetype]
848 849 except KeyError:
849 850 # we don't know anything special about this, handle it safely
850 851 if file_node.is_binary:
851 852 # do same as download raw for binary files
852 853 mimetype, disposition = 'application/octet-stream', 'attachment'
853 854 else:
854 855 # do not just use the original mimetype, but force text/plain,
855 856 # otherwise it would serve text/html and that might be unsafe.
856 857 # Note: underlying vcs library fakes text/plain mimetype if the
857 858 # mimetype can not be determined and it thinks it is not
858 859 # binary.This might lead to erroneous text display in some
859 860 # cases, but helps in other cases, like with text files
860 861 # without extension.
861 862 mimetype, disposition = 'text/plain', 'inline'
862 863
863 864 if disposition == 'attachment':
864 865 disposition = self._get_attachement_headers(f_path)
865 866
866 867 stream_content = file_node.stream_bytes()
867 868
868 869 response = Response(app_iter=stream_content)
869 870 response.content_disposition = disposition
870 871 response.content_type = mimetype
871 872
872 873 charset = self._get_default_encoding(c)
873 874 if charset:
874 875 response.charset = charset
875 876
876 877 return response
877 878
878 879 @LoginRequired()
879 880 @HasRepoPermissionAnyDecorator(
880 881 'repository.read', 'repository.write', 'repository.admin')
881 882 def repo_file_download(self):
882 883 c = self.load_default_context()
883 884
884 885 commit_id, f_path = self._get_commit_and_path()
885 886 commit = self._get_commit_or_redirect(commit_id)
886 887 file_node = self._get_filenode_or_redirect(commit, f_path)
887 888
888 889 if self.request.GET.get('lf'):
889 890 # only if lf get flag is passed, we download this file
890 891 # as LFS/Largefile
891 892 lf_node = file_node.get_largefile_node()
892 893 if lf_node:
893 894 # overwrite our pointer with the REAL large-file
894 895 file_node = lf_node
895 896
896 897 disposition = self._get_attachement_headers(f_path)
897 898
898 899 stream_content = file_node.stream_bytes()
899 900
900 901 response = Response(app_iter=stream_content)
901 902 response.content_disposition = disposition
902 903 response.content_type = file_node.mimetype
903 904
904 905 charset = self._get_default_encoding(c)
905 906 if charset:
906 907 response.charset = charset
907 908
908 909 return response
909 910
910 911 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
911 912
912 913 cache_seconds = safe_int(
913 914 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
914 915 cache_on = cache_seconds > 0
915 916 log.debug(
916 917 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
917 918 'with caching: %s[TTL: %ss]' % (
918 919 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
919 920
920 921 cache_namespace_uid = f'repo.{repo_id}'
921 922 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
922 923
923 924 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
924 925 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
925 926 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
926 927 _repo_id, commit_id, f_path)
927 928 try:
928 929 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
929 930 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
930 931 log.exception(safe_str(e))
931 932 h.flash(h.escape(safe_str(e)), category='error')
932 933 raise HTTPFound(h.route_path(
933 934 'repo_files', repo_name=self.db_repo_name,
934 935 commit_id='tip', f_path='/'))
935 936
936 937 return _d + _f
937 938
938 939 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
939 940 commit_id, f_path)
940 941 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
941 942
942 943 @LoginRequired()
943 944 @HasRepoPermissionAnyDecorator(
944 945 'repository.read', 'repository.write', 'repository.admin')
945 946 def repo_nodelist(self):
946 947 self.load_default_context()
947 948
948 949 commit_id, f_path = self._get_commit_and_path()
949 950 commit = self._get_commit_or_redirect(commit_id)
950 951
951 952 metadata = self._get_nodelist_at_commit(
952 953 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
953 954 return {'nodes': [x for x in metadata]}
954 955
955 956 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
956 957 items = []
957 958 for name, commit_id in branches_or_tags.items():
958 959 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
959 960 items.append((sym_ref, name, ref_type))
960 961 return items
961 962
962 963 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
963 964 return commit_id
964 965
965 966 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
966 967 return commit_id
967 968
968 969 # NOTE(dan): old code we used in "diff" mode compare
969 970 new_f_path = vcspath.join(name, f_path)
970 971 return f'{new_f_path}@{commit_id}'
971 972
972 973 def _get_node_history(self, commit_obj, f_path, commits=None):
973 974 """
974 975 get commit history for given node
975 976
976 977 :param commit_obj: commit to calculate history
977 978 :param f_path: path for node to calculate history for
978 979 :param commits: if passed don't calculate history and take
979 980 commits defined in this list
980 981 """
981 982 _ = self.request.translate
982 983
983 984 # calculate history based on tip
984 985 tip = self.rhodecode_vcs_repo.get_commit()
985 986 if commits is None:
986 987 pre_load = ["author", "branch"]
987 988 try:
988 989 commits = tip.get_path_history(f_path, pre_load=pre_load)
989 990 except (NodeDoesNotExistError, CommitError):
990 991 # this node is not present at tip!
991 992 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
992 993
993 994 history = []
994 995 commits_group = ([], _("Changesets"))
995 996 for commit in commits:
996 997 branch = ' (%s)' % commit.branch if commit.branch else ''
997 998 n_desc = f'r{commit.idx}:{commit.short_id}{branch}'
998 999 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
999 1000 history.append(commits_group)
1000 1001
1001 1002 symbolic_reference = self._symbolic_reference
1002 1003
1003 1004 if self.rhodecode_vcs_repo.alias == 'svn':
1004 1005 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1005 1006 f_path, self.rhodecode_vcs_repo)
1006 1007 if adjusted_f_path != f_path:
1007 1008 log.debug(
1008 1009 'Recognized svn tag or branch in file "%s", using svn '
1009 1010 'specific symbolic references', f_path)
1010 1011 f_path = adjusted_f_path
1011 1012 symbolic_reference = self._symbolic_reference_svn
1012 1013
1013 1014 branches = self._create_references(
1014 1015 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1015 1016 branches_group = (branches, _("Branches"))
1016 1017
1017 1018 tags = self._create_references(
1018 1019 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1019 1020 tags_group = (tags, _("Tags"))
1020 1021
1021 1022 history.append(branches_group)
1022 1023 history.append(tags_group)
1023 1024
1024 1025 return history, commits
1025 1026
1026 1027 @LoginRequired()
1027 1028 @HasRepoPermissionAnyDecorator(
1028 1029 'repository.read', 'repository.write', 'repository.admin')
1029 1030 def repo_file_history(self):
1030 1031 self.load_default_context()
1031 1032
1032 1033 commit_id, f_path = self._get_commit_and_path()
1033 1034 commit = self._get_commit_or_redirect(commit_id)
1034 1035 file_node = self._get_filenode_or_redirect(commit, f_path)
1035 1036
1036 1037 if file_node.is_file():
1037 1038 file_history, _hist = self._get_node_history(commit, f_path)
1038 1039
1039 1040 res = []
1040 1041 for section_items, section in file_history:
1041 1042 items = []
1042 1043 for obj_id, obj_text, obj_type in section_items:
1043 1044 at_rev = ''
1044 1045 if obj_type in ['branch', 'bookmark', 'tag']:
1045 1046 at_rev = obj_text
1046 1047 entry = {
1047 1048 'id': obj_id,
1048 1049 'text': obj_text,
1049 1050 'type': obj_type,
1050 1051 'at_rev': at_rev
1051 1052 }
1052 1053
1053 1054 items.append(entry)
1054 1055
1055 1056 res.append({
1056 1057 'text': section,
1057 1058 'children': items
1058 1059 })
1059 1060
1060 1061 data = {
1061 1062 'more': False,
1062 1063 'results': res
1063 1064 }
1064 1065 return data
1065 1066
1066 1067 log.warning('Cannot fetch history for directory')
1067 1068 raise HTTPBadRequest()
1068 1069
1069 1070 @LoginRequired()
1070 1071 @HasRepoPermissionAnyDecorator(
1071 1072 'repository.read', 'repository.write', 'repository.admin')
1072 1073 def repo_file_authors(self):
1073 1074 c = self.load_default_context()
1074 1075
1075 1076 commit_id, f_path = self._get_commit_and_path()
1076 1077 commit = self._get_commit_or_redirect(commit_id)
1077 1078 file_node = self._get_filenode_or_redirect(commit, f_path)
1078 1079
1079 1080 if not file_node.is_file():
1080 1081 raise HTTPBadRequest()
1081 1082
1082 1083 c.file_last_commit = file_node.last_commit
1083 1084 if self.request.GET.get('annotate') == '1':
1084 1085 # use _hist from annotation if annotation mode is on
1085 1086 commit_ids = {x[1] for x in file_node.annotate}
1086 1087 _hist = (
1087 1088 self.rhodecode_vcs_repo.get_commit(commit_id)
1088 1089 for commit_id in commit_ids)
1089 1090 else:
1090 1091 _f_history, _hist = self._get_node_history(commit, f_path)
1091 1092 c.file_author = False
1092 1093
1093 1094 unique = collections.OrderedDict()
1094 1095 for commit in _hist:
1095 1096 author = commit.author
1096 1097 if author not in unique:
1097 1098 unique[commit.author] = [
1098 1099 h.email(author),
1099 1100 h.person(author, 'username_or_name_or_email'),
1100 1101 1 # counter
1101 1102 ]
1102 1103
1103 1104 else:
1104 1105 # increase counter
1105 1106 unique[commit.author][2] += 1
1106 1107
1107 1108 c.authors = [val for val in unique.values()]
1108 1109
1109 1110 return self._get_template_context(c)
1110 1111
1111 1112 @LoginRequired()
1112 1113 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1113 1114 def repo_files_check_head(self):
1114 1115 self.load_default_context()
1115 1116
1116 1117 commit_id, f_path = self._get_commit_and_path()
1117 1118 _branch_name, _sha_commit_id, is_head = \
1118 1119 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1119 1120 landing_ref=self.db_repo.landing_ref_name)
1120 1121
1121 1122 new_path = self.request.POST.get('path')
1122 1123 operation = self.request.POST.get('operation')
1123 1124 path_exist = ''
1124 1125
1125 1126 if new_path and operation in ['create', 'upload']:
1126 1127 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1127 1128 try:
1128 1129 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1129 1130 # NOTE(dan): construct whole path without leading /
1130 1131 file_node = commit_obj.get_node(new_f_path)
1131 1132 if file_node is not None:
1132 1133 path_exist = new_f_path
1133 1134 except EmptyRepositoryError:
1134 1135 pass
1135 1136 except Exception:
1136 1137 pass
1137 1138
1138 1139 return {
1139 1140 'branch': _branch_name,
1140 1141 'sha': _sha_commit_id,
1141 1142 'is_head': is_head,
1142 1143 'path_exists': path_exist
1143 1144 }
1144 1145
1145 1146 @LoginRequired()
1146 1147 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1147 1148 def repo_files_remove_file(self):
1148 1149 _ = self.request.translate
1149 1150 c = self.load_default_context()
1150 1151 commit_id, f_path = self._get_commit_and_path()
1151 1152
1152 1153 self._ensure_not_locked()
1153 1154 _branch_name, _sha_commit_id, is_head = \
1154 1155 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1155 1156 landing_ref=self.db_repo.landing_ref_name)
1156 1157
1157 1158 self.forbid_non_head(is_head, f_path)
1158 1159 self.check_branch_permission(_branch_name)
1159 1160
1160 1161 c.commit = self._get_commit_or_redirect(commit_id)
1161 1162 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1162 1163
1163 1164 c.default_message = _(
1164 1165 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1165 1166 c.f_path = f_path
1166 1167
1167 1168 return self._get_template_context(c)
1168 1169
1169 1170 @LoginRequired()
1170 1171 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1171 1172 @CSRFRequired()
1172 1173 def repo_files_delete_file(self):
1173 1174 _ = self.request.translate
1174 1175
1175 1176 c = self.load_default_context()
1176 1177 commit_id, f_path = self._get_commit_and_path()
1177 1178
1178 1179 self._ensure_not_locked()
1179 1180 _branch_name, _sha_commit_id, is_head = \
1180 1181 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1181 1182 landing_ref=self.db_repo.landing_ref_name)
1182 1183
1183 1184 self.forbid_non_head(is_head, f_path)
1184 1185 self.check_branch_permission(_branch_name)
1185 1186
1186 1187 c.commit = self._get_commit_or_redirect(commit_id)
1187 1188 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1188 1189
1189 1190 c.default_message = _(
1190 1191 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1191 1192 c.f_path = f_path
1192 1193 node_path = f_path
1193 1194 author = self._rhodecode_db_user.full_contact
1194 1195 message = self.request.POST.get('message') or c.default_message
1195 1196 try:
1196 1197 nodes = {
1197 1198 safe_bytes(node_path): {
1198 1199 'content': b''
1199 1200 }
1200 1201 }
1201 1202 ScmModel().delete_nodes(
1202 1203 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1203 1204 message=message,
1204 1205 nodes=nodes,
1205 1206 parent_commit=c.commit,
1206 1207 author=author,
1207 1208 )
1208 1209
1209 1210 h.flash(
1210 1211 _('Successfully deleted file `{}`').format(
1211 1212 h.escape(f_path)), category='success')
1212 1213 except Exception:
1213 1214 log.exception('Error during commit operation')
1214 1215 h.flash(_('Error occurred during commit'), category='error')
1215 1216 raise HTTPFound(
1216 1217 h.route_path('repo_commit', repo_name=self.db_repo_name,
1217 1218 commit_id='tip'))
1218 1219
1219 1220 @LoginRequired()
1220 1221 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1221 1222 def repo_files_edit_file(self):
1222 1223 _ = self.request.translate
1223 1224 c = self.load_default_context()
1224 1225 commit_id, f_path = self._get_commit_and_path()
1225 1226
1226 1227 self._ensure_not_locked()
1227 1228 _branch_name, _sha_commit_id, is_head = \
1228 1229 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1229 1230 landing_ref=self.db_repo.landing_ref_name)
1230 1231
1231 1232 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1232 1233 self.check_branch_permission(_branch_name, commit_id=commit_id)
1233 1234
1234 1235 c.commit = self._get_commit_or_redirect(commit_id)
1235 1236 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1236 1237
1237 1238 if c.file.is_binary:
1238 1239 files_url = h.route_path(
1239 1240 'repo_files',
1240 1241 repo_name=self.db_repo_name,
1241 1242 commit_id=c.commit.raw_id, f_path=f_path)
1242 1243 raise HTTPFound(files_url)
1243 1244
1244 1245 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1245 1246 c.f_path = f_path
1246 1247
1247 1248 return self._get_template_context(c)
1248 1249
1249 1250 @LoginRequired()
1250 1251 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1251 1252 @CSRFRequired()
1252 1253 def repo_files_update_file(self):
1253 1254 _ = self.request.translate
1254 1255 c = self.load_default_context()
1255 1256 commit_id, f_path = self._get_commit_and_path()
1256 1257
1257 1258 self._ensure_not_locked()
1258 1259
1259 1260 c.commit = self._get_commit_or_redirect(commit_id)
1260 1261 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1261 1262
1262 1263 if c.file.is_binary:
1263 1264 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1264 1265 commit_id=c.commit.raw_id, f_path=f_path))
1265 1266
1266 1267 _branch_name, _sha_commit_id, is_head = \
1267 1268 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1268 1269 landing_ref=self.db_repo.landing_ref_name)
1269 1270
1270 1271 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1271 1272 self.check_branch_permission(_branch_name, commit_id=commit_id)
1272 1273
1273 1274 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1274 1275 c.f_path = f_path
1275 1276
1276 1277 old_content = c.file.str_content
1277 1278 sl = old_content.splitlines(1)
1278 1279 first_line = sl[0] if sl else ''
1279 1280
1280 1281 r_post = self.request.POST
1281 1282 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1282 1283 line_ending_mode = detect_mode(first_line, 0)
1283 1284 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1284 1285
1285 1286 message = r_post.get('message') or c.default_message
1286 1287
1287 1288 org_node_path = c.file.str_path
1288 1289 filename = r_post['filename']
1289 1290
1290 1291 root_path = c.file.dir_path
1291 1292 pure_path = self.create_pure_path(root_path, filename)
1292 1293 node_path = pure_path.as_posix()
1293 1294
1294 1295 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1295 1296 commit_id=commit_id)
1296 1297 if content == old_content and node_path == org_node_path:
1297 1298 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1298 1299 category='warning')
1299 1300 raise HTTPFound(default_redirect_url)
1300 1301
1301 1302 try:
1302 1303 mapping = {
1303 1304 c.file.bytes_path: {
1304 1305 'org_filename': org_node_path,
1305 1306 'filename': safe_bytes(node_path),
1306 1307 'content': safe_bytes(content),
1307 1308 'lexer': '',
1308 1309 'op': 'mod',
1309 1310 'mode': c.file.mode
1310 1311 }
1311 1312 }
1312 1313
1313 1314 commit = ScmModel().update_nodes(
1314 1315 user=self._rhodecode_db_user.user_id,
1315 1316 repo=self.db_repo,
1316 1317 message=message,
1317 1318 nodes=mapping,
1318 1319 parent_commit=c.commit,
1319 1320 )
1320 1321
1321 1322 h.flash(_('Successfully committed changes to file `{}`').format(
1322 1323 h.escape(f_path)), category='success')
1323 1324 default_redirect_url = h.route_path(
1324 1325 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1325 1326
1326 1327 except Exception:
1327 1328 log.exception('Error occurred during commit')
1328 1329 h.flash(_('Error occurred during commit'), category='error')
1329 1330
1330 1331 raise HTTPFound(default_redirect_url)
1331 1332
1332 1333 @LoginRequired()
1333 1334 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1334 1335 def repo_files_add_file(self):
1335 1336 _ = self.request.translate
1336 1337 c = self.load_default_context()
1337 1338 commit_id, f_path = self._get_commit_and_path()
1338 1339
1339 1340 self._ensure_not_locked()
1340 1341
1341 1342 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1342 1343 if c.commit is None:
1343 1344 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1344 1345
1345 1346 if self.rhodecode_vcs_repo.is_empty():
1346 1347 # for empty repository we cannot check for current branch, we rely on
1347 1348 # c.commit.branch instead
1348 1349 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1349 1350 else:
1350 1351 _branch_name, _sha_commit_id, is_head = \
1351 1352 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1352 1353 landing_ref=self.db_repo.landing_ref_name)
1353 1354
1354 1355 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1355 1356 self.check_branch_permission(_branch_name, commit_id=commit_id)
1356 1357
1357 1358 c.default_message = (_('Added file via RhodeCode Enterprise'))
1358 1359 c.f_path = f_path.lstrip('/') # ensure not relative path
1359 1360
1360 1361 return self._get_template_context(c)
1361 1362
1362 1363 @LoginRequired()
1363 1364 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1364 1365 @CSRFRequired()
1365 1366 def repo_files_create_file(self):
1366 1367 _ = self.request.translate
1367 1368 c = self.load_default_context()
1368 1369 commit_id, f_path = self._get_commit_and_path()
1369 1370
1370 1371 self._ensure_not_locked()
1371 1372
1372 1373 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1373 1374 if c.commit is None:
1374 1375 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1375 1376
1376 1377 # calculate redirect URL
1377 1378 if self.rhodecode_vcs_repo.is_empty():
1378 1379 default_redirect_url = h.route_path(
1379 1380 'repo_summary', repo_name=self.db_repo_name)
1380 1381 else:
1381 1382 default_redirect_url = h.route_path(
1382 1383 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1383 1384
1384 1385 if self.rhodecode_vcs_repo.is_empty():
1385 1386 # for empty repository we cannot check for current branch, we rely on
1386 1387 # c.commit.branch instead
1387 1388 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1388 1389 else:
1389 1390 _branch_name, _sha_commit_id, is_head = \
1390 1391 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1391 1392 landing_ref=self.db_repo.landing_ref_name)
1392 1393
1393 1394 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1394 1395 self.check_branch_permission(_branch_name, commit_id=commit_id)
1395 1396
1396 1397 c.default_message = (_('Added file via RhodeCode Enterprise'))
1397 1398 c.f_path = f_path
1398 1399
1399 1400 r_post = self.request.POST
1400 1401 message = r_post.get('message') or c.default_message
1401 1402 filename = r_post.get('filename')
1402 1403 unix_mode = 0
1403 1404
1404 1405 if not filename:
1405 1406 # If there's no commit, redirect to repo summary
1406 1407 if type(c.commit) is EmptyCommit:
1407 1408 redirect_url = h.route_path(
1408 1409 'repo_summary', repo_name=self.db_repo_name)
1409 1410 else:
1410 1411 redirect_url = default_redirect_url
1411 1412 h.flash(_('No filename specified'), category='warning')
1412 1413 raise HTTPFound(redirect_url)
1413 1414
1414 1415 root_path = f_path
1415 1416 pure_path = self.create_pure_path(root_path, filename)
1416 1417 node_path = pure_path.as_posix().lstrip('/')
1417 1418
1418 1419 author = self._rhodecode_db_user.full_contact
1419 1420 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1420 1421 nodes = {
1421 1422 safe_bytes(node_path): {
1422 1423 'content': safe_bytes(content)
1423 1424 }
1424 1425 }
1425 1426
1426 1427 try:
1427 1428
1428 1429 commit = ScmModel().create_nodes(
1429 1430 user=self._rhodecode_db_user.user_id,
1430 1431 repo=self.db_repo,
1431 1432 message=message,
1432 1433 nodes=nodes,
1433 1434 parent_commit=c.commit,
1434 1435 author=author,
1435 1436 )
1436 1437
1437 1438 h.flash(_('Successfully committed new file `{}`').format(
1438 1439 h.escape(node_path)), category='success')
1439 1440
1440 1441 default_redirect_url = h.route_path(
1441 1442 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1442 1443
1443 1444 except NonRelativePathError:
1444 1445 log.exception('Non Relative path found')
1445 1446 h.flash(_('The location specified must be a relative path and must not '
1446 1447 'contain .. in the path'), category='warning')
1447 1448 raise HTTPFound(default_redirect_url)
1448 1449 except (NodeError, NodeAlreadyExistsError) as e:
1449 1450 h.flash(h.escape(safe_str(e)), category='error')
1450 1451 except Exception:
1451 1452 log.exception('Error occurred during commit')
1452 1453 h.flash(_('Error occurred during commit'), category='error')
1453 1454
1454 1455 raise HTTPFound(default_redirect_url)
1455 1456
1456 1457 @LoginRequired()
1457 1458 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1458 1459 @CSRFRequired()
1459 1460 def repo_files_upload_file(self):
1460 1461 _ = self.request.translate
1461 1462 c = self.load_default_context()
1462 1463 commit_id, f_path = self._get_commit_and_path()
1463 1464
1464 1465 self._ensure_not_locked()
1465 1466
1466 1467 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1467 1468 if c.commit is None:
1468 1469 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1469 1470
1470 1471 # calculate redirect URL
1471 1472 if self.rhodecode_vcs_repo.is_empty():
1472 1473 default_redirect_url = h.route_path(
1473 1474 'repo_summary', repo_name=self.db_repo_name)
1474 1475 else:
1475 1476 default_redirect_url = h.route_path(
1476 1477 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1477 1478
1478 1479 if self.rhodecode_vcs_repo.is_empty():
1479 1480 # for empty repository we cannot check for current branch, we rely on
1480 1481 # c.commit.branch instead
1481 1482 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1482 1483 else:
1483 1484 _branch_name, _sha_commit_id, is_head = \
1484 1485 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1485 1486 landing_ref=self.db_repo.landing_ref_name)
1486 1487
1487 1488 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1488 1489 if error:
1489 1490 return {
1490 1491 'error': error,
1491 1492 'redirect_url': default_redirect_url
1492 1493 }
1493 1494 error = self.check_branch_permission(_branch_name, json_mode=True)
1494 1495 if error:
1495 1496 return {
1496 1497 'error': error,
1497 1498 'redirect_url': default_redirect_url
1498 1499 }
1499 1500
1500 1501 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1501 1502 c.f_path = f_path
1502 1503
1503 1504 r_post = self.request.POST
1504 1505
1505 1506 message = c.default_message
1506 1507 user_message = r_post.getall('message')
1507 1508 if isinstance(user_message, list) and user_message:
1508 1509 # we take the first from duplicated results if it's not empty
1509 1510 message = user_message[0] if user_message[0] else message
1510 1511
1511 1512 nodes = {}
1512 1513
1513 1514 for file_obj in r_post.getall('files_upload') or []:
1514 1515 content = file_obj.file
1515 1516 filename = file_obj.filename
1516 1517
1517 1518 root_path = f_path
1518 1519 pure_path = self.create_pure_path(root_path, filename)
1519 1520 node_path = pure_path.as_posix().lstrip('/')
1520 1521
1521 1522 nodes[safe_bytes(node_path)] = {
1522 1523 'content': content
1523 1524 }
1524 1525
1525 1526 if not nodes:
1526 1527 error = 'missing files'
1527 1528 return {
1528 1529 'error': error,
1529 1530 'redirect_url': default_redirect_url
1530 1531 }
1531 1532
1532 1533 author = self._rhodecode_db_user.full_contact
1533 1534
1534 1535 try:
1535 1536 commit = ScmModel().create_nodes(
1536 1537 user=self._rhodecode_db_user.user_id,
1537 1538 repo=self.db_repo,
1538 1539 message=message,
1539 1540 nodes=nodes,
1540 1541 parent_commit=c.commit,
1541 1542 author=author,
1542 1543 )
1543 1544 if len(nodes) == 1:
1544 1545 flash_message = _('Successfully committed {} new files').format(len(nodes))
1545 1546 else:
1546 1547 flash_message = _('Successfully committed 1 new file')
1547 1548
1548 1549 h.flash(flash_message, category='success')
1549 1550
1550 1551 default_redirect_url = h.route_path(
1551 1552 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1552 1553
1553 1554 except NonRelativePathError:
1554 1555 log.exception('Non Relative path found')
1555 1556 error = _('The location specified must be a relative path and must not '
1556 1557 'contain .. in the path')
1557 1558 h.flash(error, category='warning')
1558 1559
1559 1560 return {
1560 1561 'error': error,
1561 1562 'redirect_url': default_redirect_url
1562 1563 }
1563 1564 except (NodeError, NodeAlreadyExistsError) as e:
1564 1565 error = h.escape(e)
1565 1566 h.flash(error, category='error')
1566 1567
1567 1568 return {
1568 1569 'error': error,
1569 1570 'redirect_url': default_redirect_url
1570 1571 }
1571 1572 except Exception:
1572 1573 log.exception('Error occurred during commit')
1573 1574 error = _('Error occurred during commit')
1574 1575 h.flash(error, category='error')
1575 1576 return {
1576 1577 'error': error,
1577 1578 'redirect_url': default_redirect_url
1578 1579 }
1579 1580
1580 1581 return {
1581 1582 'error': None,
1582 1583 'redirect_url': default_redirect_url
1583 1584 }
@@ -1,1198 +1,1203 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import os
20 20 import re
21 21 import shutil
22 22 import time
23 23 import logging
24 24 import traceback
25 25 import datetime
26 26
27 27 from pyramid.threadlocal import get_current_request
28 28 from sqlalchemy.orm import aliased
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 from rhodecode import events
32 32 from rhodecode.lib.auth import HasUserGroupPermissionAny
33 33 from rhodecode.lib.caching_query import FromCache
34 34 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
35 35 from rhodecode.lib import hooks_base
36 36 from rhodecode.lib.user_log_filter import user_log_filter
37 37 from rhodecode.lib.utils import make_db_config
38 38 from rhodecode.lib.utils2 import (
39 39 safe_str, remove_prefix, obfuscate_url_pw,
40 40 get_current_rhodecode_user, safe_int, action_logger_generic)
41 41 from rhodecode.lib.vcs.backends import get_backend
42 from rhodecode.lib.vcs.nodes import NodeKind
42 43 from rhodecode.model import BaseModel
43 44 from rhodecode.model.db import (
44 45 _hash_key, func, case, joinedload, or_, in_filter_generator,
45 46 Session, Repository, UserRepoToPerm, UserGroupRepoToPerm,
46 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
47 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
48 49 from rhodecode.model.permission import PermissionModel
49 50 from rhodecode.model.settings import VcsSettingsModel
50 51
51 52 log = logging.getLogger(__name__)
52 53
53 54
54 55 class RepoModel(BaseModel):
55 56
56 57 cls = Repository
57 58
58 59 def _get_user_group(self, users_group):
59 60 return self._get_instance(UserGroup, users_group,
60 61 callback=UserGroup.get_by_group_name)
61 62
62 63 def _get_repo_group(self, repo_group):
63 64 return self._get_instance(RepoGroup, repo_group,
64 65 callback=RepoGroup.get_by_group_name)
65 66
66 67 def _create_default_perms(self, repository, private):
67 68 # create default permission
68 69 default = 'repository.read'
69 70 def_user = User.get_default_user()
70 71 for p in def_user.user_perms:
71 72 if p.permission.permission_name.startswith('repository.'):
72 73 default = p.permission.permission_name
73 74 break
74 75
75 76 default_perm = 'repository.none' if private else default
76 77
77 78 repo_to_perm = UserRepoToPerm()
78 79 repo_to_perm.permission = Permission.get_by_key(default_perm)
79 80
80 81 repo_to_perm.repository = repository
81 82 repo_to_perm.user = def_user
82 83
83 84 return repo_to_perm
84 85
85 86 @LazyProperty
86 87 def repos_path(self):
87 88 """
88 89 Gets the repositories root path from database
89 90 """
90 91 settings_model = VcsSettingsModel(sa=self.sa)
91 92 return settings_model.get_repos_location()
92 93
93 94 def get(self, repo_id):
94 95 repo = self.sa.query(Repository) \
95 96 .filter(Repository.repo_id == repo_id)
96 97
97 98 return repo.scalar()
98 99
99 100 def get_repo(self, repository):
100 101 return self._get_repo(repository)
101 102
102 103 def get_by_repo_name(self, repo_name, cache=False):
103 104 repo = self.sa.query(Repository) \
104 105 .filter(Repository.repo_name == repo_name)
105 106
106 107 if cache:
107 108 name_key = _hash_key(repo_name)
108 109 repo = repo.options(
109 110 FromCache("sql_cache_short", f"get_repo_{name_key}"))
110 111 return repo.scalar()
111 112
112 113 def _extract_id_from_repo_name(self, repo_name):
113 114 if repo_name.startswith('/'):
114 115 repo_name = repo_name.lstrip('/')
115 116 by_id_match = re.match(r'^_(\d+)', repo_name)
116 117 if by_id_match:
117 118 return by_id_match.groups()[0]
118 119
119 120 def get_repo_by_id(self, repo_name):
120 121 """
121 122 Extracts repo_name by id from special urls.
122 123 Example url is _11/repo_name
123 124
124 125 :param repo_name:
125 126 :return: repo object if matched else None
126 127 """
127 128 _repo_id = None
128 129 try:
129 130 _repo_id = self._extract_id_from_repo_name(repo_name)
130 131 if _repo_id:
131 132 return self.get(_repo_id)
132 133 except Exception:
133 134 log.exception('Failed to extract repo_name from URL')
134 135 if _repo_id:
135 136 Session().rollback()
136 137
137 138 return None
138 139
139 140 def get_repos_for_root(self, root, traverse=False):
140 141 if traverse:
141 142 like_expression = u'{}%'.format(safe_str(root))
142 143 repos = Repository.query().filter(
143 144 Repository.repo_name.like(like_expression)).all()
144 145 else:
145 146 if root and not isinstance(root, RepoGroup):
146 147 raise ValueError(
147 148 'Root must be an instance '
148 149 'of RepoGroup, got:{} instead'.format(type(root)))
149 150 repos = Repository.query().filter(Repository.group == root).all()
150 151 return repos
151 152
152 153 def get_url(self, repo, request=None, permalink=False):
153 154 if not request:
154 155 request = get_current_request()
155 156
156 157 if not request:
157 158 return
158 159
159 160 if permalink:
160 161 return request.route_url(
161 162 'repo_summary', repo_name='_{}'.format(safe_str(repo.repo_id)))
162 163 else:
163 164 return request.route_url(
164 165 'repo_summary', repo_name=safe_str(repo.repo_name))
165 166
166 167 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
167 168 if not request:
168 169 request = get_current_request()
169 170
170 171 if not request:
171 172 return
172 173
173 174 if permalink:
174 175 return request.route_url(
175 176 'repo_commit', repo_name=safe_str(repo.repo_id),
176 177 commit_id=commit_id)
177 178
178 179 else:
179 180 return request.route_url(
180 181 'repo_commit', repo_name=safe_str(repo.repo_name),
181 182 commit_id=commit_id)
182 183
183 184 def get_repo_log(self, repo, filter_term):
184 185 repo_log = UserLog.query()\
185 186 .filter(or_(UserLog.repository_id == repo.repo_id,
186 187 UserLog.repository_name == repo.repo_name))\
187 188 .options(joinedload(UserLog.user))\
188 189 .options(joinedload(UserLog.repository))\
189 190 .order_by(UserLog.action_date.desc())
190 191
191 192 repo_log = user_log_filter(repo_log, filter_term)
192 193 return repo_log
193 194
194 195 @classmethod
195 196 def update_commit_cache(cls, repositories=None):
196 197 if not repositories:
197 198 repositories = Repository.getAll()
198 199 for repo in repositories:
199 200 repo.update_commit_cache()
200 201
201 202 def get_repos_as_dict(self, repo_list=None, admin=False,
202 203 super_user_actions=False, short_name=None):
203 204
204 205 _render = get_current_request().get_partial_renderer(
205 206 'rhodecode:templates/data_table/_dt_elements.mako')
206 207 c = _render.get_call_context()
207 208 h = _render.get_helpers()
208 209
209 210 def quick_menu(repo_name):
210 211 return _render('quick_menu', repo_name)
211 212
212 213 def repo_lnk(name, rtype, rstate, private, archived, fork_repo_name):
213 214 if short_name is not None:
214 215 short_name_var = short_name
215 216 else:
216 217 short_name_var = not admin
217 218 return _render('repo_name', name, rtype, rstate, private, archived, fork_repo_name,
218 219 short_name=short_name_var, admin=False)
219 220
220 221 def last_change(last_change):
221 222 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
222 223 ts = time.time()
223 224 utc_offset = (datetime.datetime.fromtimestamp(ts)
224 225 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
225 226 last_change = last_change + datetime.timedelta(seconds=utc_offset)
226 227
227 228 return _render("last_change", last_change)
228 229
229 230 def rss_lnk(repo_name):
230 231 return _render("rss", repo_name)
231 232
232 233 def atom_lnk(repo_name):
233 234 return _render("atom", repo_name)
234 235
235 236 def last_rev(repo_name, cs_cache):
236 237 return _render('revision', repo_name, cs_cache.get('revision'),
237 238 cs_cache.get('raw_id'), cs_cache.get('author'),
238 239 cs_cache.get('message'), cs_cache.get('date'))
239 240
240 241 def desc(desc):
241 242 return _render('repo_desc', desc, c.visual.stylify_metatags)
242 243
243 244 def state(repo_state):
244 245 return _render("repo_state", repo_state)
245 246
246 247 def repo_actions(repo_name):
247 248 return _render('repo_actions', repo_name, super_user_actions)
248 249
249 250 def user_profile(username):
250 251 return _render('user_profile', username)
251 252
252 253 repos_data = []
253 254 for repo in repo_list:
254 255 # NOTE(marcink): because we use only raw column we need to load it like that
255 256 changeset_cache = Repository._load_changeset_cache(
256 257 repo.repo_id, repo._changeset_cache)
257 258
258 259 row = {
259 260 "menu": quick_menu(repo.repo_name),
260 261
261 262 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
262 263 repo.private, repo.archived, repo.fork_repo_name),
263 264
264 265 "desc": desc(h.escape(repo.description)),
265 266
266 267 "last_change": last_change(repo.updated_on),
267 268
268 269 "last_changeset": last_rev(repo.repo_name, changeset_cache),
269 270 "last_changeset_raw": changeset_cache.get('revision'),
270 271
271 272 "owner": user_profile(repo.owner_username),
272 273
273 274 "state": state(repo.repo_state),
274 275 "rss": rss_lnk(repo.repo_name),
275 276 "atom": atom_lnk(repo.repo_name),
276 277 }
277 278 if admin:
278 279 row.update({
279 280 "action": repo_actions(repo.repo_name),
280 281 })
281 282 repos_data.append(row)
282 283
283 284 return repos_data
284 285
285 286 def get_repos_data_table(
286 287 self, draw, start, limit,
287 288 search_q, order_by, order_dir,
288 289 auth_user, repo_group_id):
289 290 from rhodecode.model.scm import RepoList
290 291
291 292 _perms = ['repository.read', 'repository.write', 'repository.admin']
292 293
293 294 repos = Repository.query() \
294 295 .filter(Repository.group_id == repo_group_id) \
295 296 .all()
296 297 auth_repo_list = RepoList(
297 298 repos, perm_set=_perms,
298 299 extra_kwargs=dict(user=auth_user))
299 300
300 301 allowed_ids = [-1]
301 302 for repo in auth_repo_list:
302 303 allowed_ids.append(repo.repo_id)
303 304
304 305 repos_data_total_count = Repository.query() \
305 306 .filter(Repository.group_id == repo_group_id) \
306 307 .filter(or_(
307 308 # generate multiple IN to fix limitation problems
308 309 *in_filter_generator(Repository.repo_id, allowed_ids))
309 310 ) \
310 311 .count()
311 312
312 313 RepoFork = aliased(Repository)
313 314 OwnerUser = aliased(User)
314 315 base_q = Session.query(
315 316 Repository.repo_id,
316 317 Repository.repo_name,
317 318 Repository.description,
318 319 Repository.repo_type,
319 320 Repository.repo_state,
320 321 Repository.private,
321 322 Repository.archived,
322 323 Repository.updated_on,
323 324 Repository._changeset_cache,
324 325 RepoFork.repo_name.label('fork_repo_name'),
325 326 OwnerUser.username.label('owner_username'),
326 327 ) \
327 328 .filter(Repository.group_id == repo_group_id) \
328 329 .filter(or_(
329 330 # generate multiple IN to fix limitation problems
330 331 *in_filter_generator(Repository.repo_id, allowed_ids))
331 332 ) \
332 333 .outerjoin(RepoFork, Repository.fork_id == RepoFork.repo_id) \
333 334 .join(OwnerUser, Repository.user_id == OwnerUser.user_id)
334 335
335 336 repos_data_total_filtered_count = base_q.count()
336 337
337 338 sort_defined = False
338 339 if order_by == 'repo_name':
339 340 sort_col = func.lower(Repository.repo_name)
340 341 sort_defined = True
341 342 elif order_by == 'user_username':
342 343 sort_col = User.username
343 344 else:
344 345 sort_col = getattr(Repository, order_by, None)
345 346
346 347 if sort_defined or sort_col:
347 348 if order_dir == 'asc':
348 349 sort_col = sort_col.asc()
349 350 else:
350 351 sort_col = sort_col.desc()
351 352
352 353 base_q = base_q.order_by(sort_col)
353 354 base_q = base_q.offset(start).limit(limit)
354 355
355 356 repos_list = base_q.all()
356 357
357 358 repos_data = RepoModel().get_repos_as_dict(
358 359 repo_list=repos_list, admin=False)
359 360
360 361 data = ({
361 362 'draw': draw,
362 363 'data': repos_data,
363 364 'recordsTotal': repos_data_total_count,
364 365 'recordsFiltered': repos_data_total_filtered_count,
365 366 })
366 367 return data
367 368
368 369 def _get_defaults(self, repo_name):
369 370 """
370 371 Gets information about repository, and returns a dict for
371 372 usage in forms
372 373
373 374 :param repo_name:
374 375 """
375 376
376 377 repo_info = Repository.get_by_repo_name(repo_name)
377 378
378 379 if repo_info is None:
379 380 return None
380 381
381 382 defaults = repo_info.get_dict()
382 383 defaults['repo_name'] = repo_info.just_name
383 384
384 385 groups = repo_info.groups_with_parents
385 386 parent_group = groups[-1] if groups else None
386 387
387 388 # we use -1 as this is how in HTML, we mark an empty group
388 389 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
389 390
390 391 keys_to_process = (
391 392 {'k': 'repo_type', 'strip': False},
392 393 {'k': 'repo_enable_downloads', 'strip': True},
393 394 {'k': 'repo_description', 'strip': True},
394 395 {'k': 'repo_enable_locking', 'strip': True},
395 396 {'k': 'repo_landing_rev', 'strip': True},
396 397 {'k': 'clone_uri', 'strip': False},
397 398 {'k': 'push_uri', 'strip': False},
398 399 {'k': 'repo_private', 'strip': True},
399 400 {'k': 'repo_enable_statistics', 'strip': True}
400 401 )
401 402
402 403 for item in keys_to_process:
403 404 attr = item['k']
404 405 if item['strip']:
405 406 attr = remove_prefix(item['k'], 'repo_')
406 407
407 408 val = defaults[attr]
408 409 if item['k'] == 'repo_landing_rev':
409 410 val = ':'.join(defaults[attr])
410 411 defaults[item['k']] = val
411 412 if item['k'] == 'clone_uri':
412 413 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
413 414 if item['k'] == 'push_uri':
414 415 defaults['push_uri_hidden'] = repo_info.push_uri_hidden
415 416
416 417 # fill owner
417 418 if repo_info.user:
418 419 defaults.update({'user': repo_info.user.username})
419 420 else:
420 421 replacement_user = User.get_first_super_admin().username
421 422 defaults.update({'user': replacement_user})
422 423
423 424 return defaults
424 425
425 426 def update(self, repo, **kwargs):
426 427 try:
427 428 cur_repo = self._get_repo(repo)
428 429 source_repo_name = cur_repo.repo_name
429 430
430 431 affected_user_ids = []
431 432 if 'user' in kwargs:
432 433 old_owner_id = cur_repo.user.user_id
433 434 new_owner = User.get_by_username(kwargs['user'])
434 435 cur_repo.user = new_owner
435 436
436 437 if old_owner_id != new_owner.user_id:
437 438 affected_user_ids = [new_owner.user_id, old_owner_id]
438 439
439 440 if 'repo_group' in kwargs:
440 441 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
441 442 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
442 443
443 444 update_keys = [
444 445 (1, 'repo_description'),
445 446 (1, 'repo_landing_rev'),
446 447 (1, 'repo_private'),
447 448 (1, 'repo_enable_downloads'),
448 449 (1, 'repo_enable_locking'),
449 450 (1, 'repo_enable_statistics'),
450 451 (0, 'clone_uri'),
451 452 (0, 'push_uri'),
452 453 (0, 'fork_id')
453 454 ]
454 455 for strip, k in update_keys:
455 456 if k in kwargs:
456 457 val = kwargs[k]
457 458 if strip:
458 459 k = remove_prefix(k, 'repo_')
459 460
460 461 setattr(cur_repo, k, val)
461 462
462 463 new_name = cur_repo.get_new_name(kwargs['repo_name'])
463 464 cur_repo.repo_name = new_name
464 465
465 466 # if private flag is set, reset default permission to NONE
466 467 if kwargs.get('repo_private'):
467 468 EMPTY_PERM = 'repository.none'
468 469 RepoModel().grant_user_permission(
469 470 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
470 471 )
471 472 if kwargs.get('repo_landing_rev'):
472 473 landing_rev_val = kwargs['repo_landing_rev']
473 474 RepoModel().set_landing_rev(cur_repo, landing_rev_val)
474 475
475 476 # handle extra fields
476 477 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX), kwargs):
477 478 k = RepositoryField.un_prefix_key(field)
478 479 ex_field = RepositoryField.get_by_key_name(
479 480 key=k, repo=cur_repo)
480 481 if ex_field:
481 482 ex_field.field_value = kwargs[field]
482 483 self.sa.add(ex_field)
483 484
484 485 self.sa.add(cur_repo)
485 486
486 487 if source_repo_name != new_name:
487 488 # rename repository
488 489 self._rename_filesystem_repo(
489 490 old=source_repo_name, new=new_name)
490 491
491 492 if affected_user_ids:
492 493 PermissionModel().trigger_permission_flush(affected_user_ids)
493 494
494 495 return cur_repo
495 496 except Exception:
496 497 log.error(traceback.format_exc())
497 498 raise
498 499
499 500 def _create_repo(self, repo_name, repo_type, description, owner,
500 501 private=False, clone_uri=None, repo_group=None,
501 502 landing_rev=None, fork_of=None,
502 503 copy_fork_permissions=False, enable_statistics=False,
503 504 enable_locking=False, enable_downloads=False,
504 505 copy_group_permissions=False,
505 506 state=Repository.STATE_PENDING):
506 507 """
507 508 Create repository inside database with PENDING state, this should be
508 509 only executed by create() repo. With exception of importing existing
509 510 repos
510 511 """
511 512 from rhodecode.model.scm import ScmModel
512 513
513 514 owner = self._get_user(owner)
514 515 fork_of = self._get_repo(fork_of)
515 516 repo_group = self._get_repo_group(safe_int(repo_group))
516 517 default_landing_ref, _lbl = ScmModel.backend_landing_ref(repo_type)
517 518 landing_rev = landing_rev or default_landing_ref
518 519
519 520 try:
520 521 repo_name = safe_str(repo_name)
521 522 description = safe_str(description)
522 523 # repo name is just a name of repository
523 524 # while repo_name_full is a full qualified name that is combined
524 525 # with name and path of group
525 526 repo_name_full = repo_name
526 527 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
527 528
528 529 new_repo = Repository()
529 530 new_repo.repo_state = state
530 531 new_repo.enable_statistics = False
531 532 new_repo.repo_name = repo_name_full
532 533 new_repo.repo_type = repo_type
533 534 new_repo.user = owner
534 535 new_repo.group = repo_group
535 536 new_repo.description = description or repo_name
536 537 new_repo.private = private
537 538 new_repo.archived = False
538 539 new_repo.clone_uri = clone_uri
539 540 new_repo.landing_rev = landing_rev
540 541
541 542 new_repo.enable_statistics = enable_statistics
542 543 new_repo.enable_locking = enable_locking
543 544 new_repo.enable_downloads = enable_downloads
544 545
545 546 if repo_group:
546 547 new_repo.enable_locking = repo_group.enable_locking
547 548
548 549 if fork_of:
549 550 parent_repo = fork_of
550 551 new_repo.fork = parent_repo
551 552
552 553 events.trigger(events.RepoPreCreateEvent(new_repo))
553 554
554 555 self.sa.add(new_repo)
555 556
556 557 EMPTY_PERM = 'repository.none'
557 558 if fork_of and copy_fork_permissions:
558 559 repo = fork_of
559 560 user_perms = UserRepoToPerm.query() \
560 561 .filter(UserRepoToPerm.repository == repo).all()
561 562 group_perms = UserGroupRepoToPerm.query() \
562 563 .filter(UserGroupRepoToPerm.repository == repo).all()
563 564
564 565 for perm in user_perms:
565 566 UserRepoToPerm.create(
566 567 perm.user, new_repo, perm.permission)
567 568
568 569 for perm in group_perms:
569 570 UserGroupRepoToPerm.create(
570 571 perm.users_group, new_repo, perm.permission)
571 572 # in case we copy permissions and also set this repo to private
572 573 # override the default user permission to make it a private repo
573 574 if private:
574 575 RepoModel(self.sa).grant_user_permission(
575 576 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
576 577
577 578 elif repo_group and copy_group_permissions:
578 579 user_perms = UserRepoGroupToPerm.query() \
579 580 .filter(UserRepoGroupToPerm.group == repo_group).all()
580 581
581 582 group_perms = UserGroupRepoGroupToPerm.query() \
582 583 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
583 584
584 585 for perm in user_perms:
585 586 perm_name = perm.permission.permission_name.replace(
586 587 'group.', 'repository.')
587 588 perm_obj = Permission.get_by_key(perm_name)
588 589 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
589 590
590 591 for perm in group_perms:
591 592 perm_name = perm.permission.permission_name.replace(
592 593 'group.', 'repository.')
593 594 perm_obj = Permission.get_by_key(perm_name)
594 595 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
595 596
596 597 if private:
597 598 RepoModel(self.sa).grant_user_permission(
598 599 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
599 600
600 601 else:
601 602 perm_obj = self._create_default_perms(new_repo, private)
602 603 self.sa.add(perm_obj)
603 604
604 605 # now automatically start following this repository as owner
605 606 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id)
606 607
607 608 # we need to flush here, in order to check if database won't
608 609 # throw any exceptions, create filesystem dirs at the very end
609 610 self.sa.flush()
610 611 events.trigger(events.RepoCreateEvent(new_repo))
611 612 return new_repo
612 613
613 614 except Exception:
614 615 log.error(traceback.format_exc())
615 616 raise
616 617
617 618 def create(self, form_data, cur_user):
618 619 """
619 620 Create repository using celery tasks
620 621
621 622 :param form_data:
622 623 :param cur_user:
623 624 """
624 625 from rhodecode.lib.celerylib import tasks, run_task
625 626 return run_task(tasks.create_repo, form_data, cur_user)
626 627
627 628 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
628 629 perm_deletions=None, check_perms=True,
629 630 cur_user=None):
630 631 if not perm_additions:
631 632 perm_additions = []
632 633 if not perm_updates:
633 634 perm_updates = []
634 635 if not perm_deletions:
635 636 perm_deletions = []
636 637
637 638 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
638 639
639 640 changes = {
640 641 'added': [],
641 642 'updated': [],
642 643 'deleted': [],
643 644 'default_user_changed': None
644 645 }
645 646
646 647 repo = self._get_repo(repo)
647 648
648 649 # update permissions
649 650 for member_id, perm, member_type in perm_updates:
650 651 member_id = int(member_id)
651 652 if member_type == 'user':
652 653 member_name = User.get(member_id).username
653 654 if member_name == User.DEFAULT_USER:
654 655 # NOTE(dan): detect if we changed permissions for default user
655 656 perm_obj = self.sa.query(UserRepoToPerm) \
656 657 .filter(UserRepoToPerm.user_id == member_id) \
657 658 .filter(UserRepoToPerm.repository == repo) \
658 659 .scalar()
659 660 if perm_obj and perm_obj.permission.permission_name != perm:
660 661 changes['default_user_changed'] = True
661 662
662 663 # this updates also current one if found
663 664 self.grant_user_permission(
664 665 repo=repo, user=member_id, perm=perm)
665 666 elif member_type == 'user_group':
666 667 # check if we have permissions to alter this usergroup
667 668 member_name = UserGroup.get(member_id).users_group_name
668 669 if not check_perms or HasUserGroupPermissionAny(
669 670 *req_perms)(member_name, user=cur_user):
670 671 self.grant_user_group_permission(
671 672 repo=repo, group_name=member_id, perm=perm)
672 673 else:
673 674 raise ValueError("member_type must be 'user' or 'user_group' "
674 675 "got {} instead".format(member_type))
675 676 changes['updated'].append({'type': member_type, 'id': member_id,
676 677 'name': member_name, 'new_perm': perm})
677 678
678 679 # set new permissions
679 680 for member_id, perm, member_type in perm_additions:
680 681 member_id = int(member_id)
681 682 if member_type == 'user':
682 683 member_name = User.get(member_id).username
683 684 self.grant_user_permission(
684 685 repo=repo, user=member_id, perm=perm)
685 686 elif member_type == 'user_group':
686 687 # check if we have permissions to alter this usergroup
687 688 member_name = UserGroup.get(member_id).users_group_name
688 689 if not check_perms or HasUserGroupPermissionAny(
689 690 *req_perms)(member_name, user=cur_user):
690 691 self.grant_user_group_permission(
691 692 repo=repo, group_name=member_id, perm=perm)
692 693 else:
693 694 raise ValueError("member_type must be 'user' or 'user_group' "
694 695 "got {} instead".format(member_type))
695 696
696 697 changes['added'].append({'type': member_type, 'id': member_id,
697 698 'name': member_name, 'new_perm': perm})
698 699 # delete permissions
699 700 for member_id, perm, member_type in perm_deletions:
700 701 member_id = int(member_id)
701 702 if member_type == 'user':
702 703 member_name = User.get(member_id).username
703 704 self.revoke_user_permission(repo=repo, user=member_id)
704 705 elif member_type == 'user_group':
705 706 # check if we have permissions to alter this usergroup
706 707 member_name = UserGroup.get(member_id).users_group_name
707 708 if not check_perms or HasUserGroupPermissionAny(
708 709 *req_perms)(member_name, user=cur_user):
709 710 self.revoke_user_group_permission(
710 711 repo=repo, group_name=member_id)
711 712 else:
712 713 raise ValueError("member_type must be 'user' or 'user_group' "
713 714 "got {} instead".format(member_type))
714 715
715 716 changes['deleted'].append({'type': member_type, 'id': member_id,
716 717 'name': member_name, 'new_perm': perm})
717 718 return changes
718 719
719 720 def create_fork(self, form_data, cur_user):
720 721 """
721 722 Simple wrapper into executing celery task for fork creation
722 723
723 724 :param form_data:
724 725 :param cur_user:
725 726 """
726 727 from rhodecode.lib.celerylib import tasks, run_task
727 728 return run_task(tasks.create_repo_fork, form_data, cur_user)
728 729
729 730 def archive(self, repo):
730 731 """
731 732 Archive given repository. Set archive flag.
732 733
733 734 :param repo:
734 735 """
735 736 repo = self._get_repo(repo)
736 737 if repo:
737 738
738 739 try:
739 740 repo.archived = True
740 741 self.sa.add(repo)
741 742 self.sa.commit()
742 743 except Exception:
743 744 log.error(traceback.format_exc())
744 745 raise
745 746
746 747 def delete(self, repo, forks=None, pull_requests=None, fs_remove=True, cur_user=None):
747 748 """
748 749 Delete given repository, forks parameter defines what do do with
749 750 attached forks. Throws AttachedForksError if deleted repo has attached
750 751 forks
751 752
752 753 :param repo:
753 754 :param forks: str 'delete' or 'detach'
754 755 :param pull_requests: str 'delete' or None
755 756 :param fs_remove: remove(archive) repo from filesystem
756 757 """
757 758 if not cur_user:
758 759 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
759 760 repo = self._get_repo(repo)
760 761 if repo:
761 762 if forks == 'detach':
762 763 for r in repo.forks:
763 764 r.fork = None
764 765 self.sa.add(r)
765 766 elif forks == 'delete':
766 767 for r in repo.forks:
767 768 self.delete(r, forks='delete')
768 769 elif [f for f in repo.forks]:
769 770 raise AttachedForksError()
770 771
771 772 # check for pull requests
772 773 pr_sources = repo.pull_requests_source
773 774 pr_targets = repo.pull_requests_target
774 775 if pull_requests != 'delete' and (pr_sources or pr_targets):
775 776 raise AttachedPullRequestsError()
776 777
777 778 old_repo_dict = repo.get_dict()
778 779 events.trigger(events.RepoPreDeleteEvent(repo))
779 780 try:
780 781 self.sa.delete(repo)
781 782 if fs_remove:
782 783 self._delete_filesystem_repo(repo)
783 784 else:
784 785 log.debug('skipping removal from filesystem')
785 786 old_repo_dict.update({
786 787 'deleted_by': cur_user,
787 788 'deleted_on': time.time(),
788 789 })
789 790 hooks_base.delete_repository(**old_repo_dict)
790 791 events.trigger(events.RepoDeleteEvent(repo))
791 792 except Exception:
792 793 log.error(traceback.format_exc())
793 794 raise
794 795
795 796 def grant_user_permission(self, repo, user, perm):
796 797 """
797 798 Grant permission for user on given repository, or update existing one
798 799 if found
799 800
800 801 :param repo: Instance of Repository, repository_id, or repository name
801 802 :param user: Instance of User, user_id or username
802 803 :param perm: Instance of Permission, or permission_name
803 804 """
804 805 user = self._get_user(user)
805 806 repo = self._get_repo(repo)
806 807 permission = self._get_perm(perm)
807 808
808 809 # check if we have that permission already
809 810 obj = self.sa.query(UserRepoToPerm) \
810 811 .filter(UserRepoToPerm.user == user) \
811 812 .filter(UserRepoToPerm.repository == repo) \
812 813 .scalar()
813 814 if obj is None:
814 815 # create new !
815 816 obj = UserRepoToPerm()
816 817 obj.repository = repo
817 818 obj.user = user
818 819 obj.permission = permission
819 820 self.sa.add(obj)
820 821 log.debug('Granted perm %s to %s on %s', perm, user, repo)
821 822 action_logger_generic(
822 823 'granted permission: {} to user: {} on repo: {}'.format(
823 824 perm, user, repo), namespace='security.repo')
824 825 return obj
825 826
826 827 def revoke_user_permission(self, repo, user):
827 828 """
828 829 Revoke permission for user on given repository
829 830
830 831 :param repo: Instance of Repository, repository_id, or repository name
831 832 :param user: Instance of User, user_id or username
832 833 """
833 834
834 835 user = self._get_user(user)
835 836 repo = self._get_repo(repo)
836 837
837 838 obj = self.sa.query(UserRepoToPerm) \
838 839 .filter(UserRepoToPerm.repository == repo) \
839 840 .filter(UserRepoToPerm.user == user) \
840 841 .scalar()
841 842 if obj:
842 843 self.sa.delete(obj)
843 844 log.debug('Revoked perm on %s on %s', repo, user)
844 845 action_logger_generic(
845 846 'revoked permission from user: {} on repo: {}'.format(
846 847 user, repo), namespace='security.repo')
847 848
848 849 def grant_user_group_permission(self, repo, group_name, perm):
849 850 """
850 851 Grant permission for user group on given repository, or update
851 852 existing one if found
852 853
853 854 :param repo: Instance of Repository, repository_id, or repository name
854 855 :param group_name: Instance of UserGroup, users_group_id,
855 856 or user group name
856 857 :param perm: Instance of Permission, or permission_name
857 858 """
858 859 repo = self._get_repo(repo)
859 860 group_name = self._get_user_group(group_name)
860 861 permission = self._get_perm(perm)
861 862
862 863 # check if we have that permission already
863 864 obj = self.sa.query(UserGroupRepoToPerm) \
864 865 .filter(UserGroupRepoToPerm.users_group == group_name) \
865 866 .filter(UserGroupRepoToPerm.repository == repo) \
866 867 .scalar()
867 868
868 869 if obj is None:
869 870 # create new
870 871 obj = UserGroupRepoToPerm()
871 872
872 873 obj.repository = repo
873 874 obj.users_group = group_name
874 875 obj.permission = permission
875 876 self.sa.add(obj)
876 877 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
877 878 action_logger_generic(
878 879 'granted permission: {} to usergroup: {} on repo: {}'.format(
879 880 perm, group_name, repo), namespace='security.repo')
880 881
881 882 return obj
882 883
883 884 def revoke_user_group_permission(self, repo, group_name):
884 885 """
885 886 Revoke permission for user group on given repository
886 887
887 888 :param repo: Instance of Repository, repository_id, or repository name
888 889 :param group_name: Instance of UserGroup, users_group_id,
889 890 or user group name
890 891 """
891 892 repo = self._get_repo(repo)
892 893 group_name = self._get_user_group(group_name)
893 894
894 895 obj = self.sa.query(UserGroupRepoToPerm) \
895 896 .filter(UserGroupRepoToPerm.repository == repo) \
896 897 .filter(UserGroupRepoToPerm.users_group == group_name) \
897 898 .scalar()
898 899 if obj:
899 900 self.sa.delete(obj)
900 901 log.debug('Revoked perm to %s on %s', repo, group_name)
901 902 action_logger_generic(
902 903 'revoked permission from usergroup: {} on repo: {}'.format(
903 904 group_name, repo), namespace='security.repo')
904 905
905 906 def delete_stats(self, repo_name):
906 907 """
907 908 removes stats for given repo
908 909
909 910 :param repo_name:
910 911 """
911 912 repo = self._get_repo(repo_name)
912 913 try:
913 914 obj = self.sa.query(Statistics) \
914 915 .filter(Statistics.repository == repo).scalar()
915 916 if obj:
916 917 self.sa.delete(obj)
917 918 except Exception:
918 919 log.error(traceback.format_exc())
919 920 raise
920 921
921 922 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
922 923 field_type='str', field_desc=''):
923 924
924 925 repo = self._get_repo(repo_name)
925 926
926 927 new_field = RepositoryField()
927 928 new_field.repository = repo
928 929 new_field.field_key = field_key
929 930 new_field.field_type = field_type # python type
930 931 new_field.field_value = field_value
931 932 new_field.field_desc = field_desc
932 933 new_field.field_label = field_label
933 934 self.sa.add(new_field)
934 935 return new_field
935 936
936 937 def delete_repo_field(self, repo_name, field_key):
937 938 repo = self._get_repo(repo_name)
938 939 field = RepositoryField.get_by_key_name(field_key, repo)
939 940 if field:
940 941 self.sa.delete(field)
941 942
942 943 def set_landing_rev(self, repo, landing_rev_name):
943 944 if landing_rev_name.startswith('branch:'):
944 945 landing_rev_name = landing_rev_name.split('branch:')[-1]
945 946 scm_instance = repo.scm_instance()
946 947 if scm_instance:
947 948 return scm_instance._remote.set_head_ref(landing_rev_name)
948 949
949 950 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
950 951 clone_uri=None, repo_store_location=None,
951 952 use_global_config=False, install_hooks=True):
952 953 """
953 954 makes repository on filesystem. It's group aware means it'll create
954 955 a repository within a group, and alter the paths accordingly of
955 956 group location
956 957
957 958 :param repo_name:
958 959 :param alias:
959 960 :param parent:
960 961 :param clone_uri:
961 962 :param repo_store_location:
962 963 """
963 964 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
964 965 from rhodecode.model.scm import ScmModel
965 966
966 967 if Repository.NAME_SEP in repo_name:
967 968 raise ValueError(
968 969 'repo_name must not contain groups got `%s`' % repo_name)
969 970
970 971 if isinstance(repo_group, RepoGroup):
971 972 new_parent_path = os.sep.join(repo_group.full_path_splitted)
972 973 else:
973 974 new_parent_path = repo_group or ''
974 975
975 976 if repo_store_location:
976 977 _paths = [repo_store_location]
977 978 else:
978 979 _paths = [self.repos_path, new_parent_path, repo_name]
979 980 # we need to make it str for mercurial
980 981 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
981 982
982 983 # check if this path is not a repository
983 984 if is_valid_repo(repo_path, self.repos_path):
984 985 raise Exception(f'This path {repo_path} is a valid repository')
985 986
986 987 # check if this path is a group
987 988 if is_valid_repo_group(repo_path, self.repos_path):
988 989 raise Exception(f'This path {repo_path} is a valid group')
989 990
990 991 log.info('creating repo %s in %s from url: `%s`',
991 992 repo_name, safe_str(repo_path),
992 993 obfuscate_url_pw(clone_uri))
993 994
994 995 backend = get_backend(repo_type)
995 996
996 997 config_repo = None if use_global_config else repo_name
997 998 if config_repo and new_parent_path:
998 999 config_repo = Repository.NAME_SEP.join(
999 1000 (new_parent_path, config_repo))
1000 1001 config = make_db_config(clear_session=False, repo=config_repo)
1001 1002 config.set('extensions', 'largefiles', '')
1002 1003
1003 1004 # patch and reset hooks section of UI config to not run any
1004 1005 # hooks on creating remote repo
1005 1006 config.clear_section('hooks')
1006 1007
1007 1008 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
1008 1009 if repo_type == 'git':
1009 1010 repo = backend(
1010 1011 repo_path, config=config, create=True, src_url=clone_uri, bare=True,
1011 1012 with_wire={"cache": False})
1012 1013 else:
1013 1014 repo = backend(
1014 1015 repo_path, config=config, create=True, src_url=clone_uri,
1015 1016 with_wire={"cache": False})
1016 1017
1017 1018 if install_hooks:
1018 1019 repo.install_hooks()
1019 1020
1020 1021 log.debug('Created repo %s with %s backend',
1021 1022 safe_str(repo_name), safe_str(repo_type))
1022 1023 return repo
1023 1024
1024 1025 def _rename_filesystem_repo(self, old, new):
1025 1026 """
1026 1027 renames repository on filesystem
1027 1028
1028 1029 :param old: old name
1029 1030 :param new: new name
1030 1031 """
1031 1032 log.info('renaming repo from %s to %s', old, new)
1032 1033
1033 1034 old_path = os.path.join(self.repos_path, old)
1034 1035 new_path = os.path.join(self.repos_path, new)
1035 1036 if os.path.isdir(new_path):
1036 1037 raise Exception(
1037 1038 'Was trying to rename to already existing dir %s' % new_path
1038 1039 )
1039 1040 shutil.move(old_path, new_path)
1040 1041
1041 1042 def _delete_filesystem_repo(self, repo):
1042 1043 """
1043 1044 removes repo from filesystem, the removal is actually made by
1044 1045 added rm__ prefix into dir, and rename internal .hg/.git dirs so this
1045 1046 repository is no longer valid for rhodecode, can be undeleted later on
1046 1047 by reverting the renames on this repository
1047 1048
1048 1049 :param repo: repo object
1049 1050 """
1050 1051 rm_path = os.path.join(self.repos_path, repo.repo_name)
1051 1052 repo_group = repo.group
1052 1053 log.info("delete_filesystem_repo: removing repository %s", rm_path)
1053 1054 # disable hg/git internal that it doesn't get detected as repo
1054 1055 alias = repo.repo_type
1055 1056
1056 1057 config = make_db_config(clear_session=False)
1057 1058 config.set('extensions', 'largefiles', '')
1058 1059 bare = getattr(repo.scm_instance(config=config), 'bare', False)
1059 1060
1060 1061 # skip this for bare git repos
1061 1062 if not bare:
1062 1063 # disable VCS repo
1063 1064 vcs_path = os.path.join(rm_path, '.%s' % alias)
1064 1065 if os.path.exists(vcs_path):
1065 1066 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
1066 1067
1067 1068 _now = datetime.datetime.now()
1068 1069 _ms = str(_now.microsecond).rjust(6, '0')
1069 1070 _d = 'rm__{}__{}'.format(_now.strftime('%Y%m%d_%H%M%S_' + _ms),
1070 1071 repo.just_name)
1071 1072 if repo_group:
1072 1073 # if repository is in group, prefix the removal path with the group
1073 1074 args = repo_group.full_path_splitted + [_d]
1074 1075 _d = os.path.join(*args)
1075 1076
1076 1077 if os.path.isdir(rm_path):
1077 1078 shutil.move(rm_path, os.path.join(self.repos_path, _d))
1078 1079
1079 1080 # finally cleanup diff-cache if it exists
1080 1081 cached_diffs_dir = repo.cached_diffs_dir
1081 1082 if os.path.isdir(cached_diffs_dir):
1082 1083 shutil.rmtree(cached_diffs_dir)
1083 1084
1084 1085
1085 1086 class ReadmeFinder:
1086 1087 """
1087 1088 Utility which knows how to find a readme for a specific commit.
1088 1089
1089 1090 The main idea is that this is a configurable algorithm. When creating an
1090 1091 instance you can define parameters, currently only the `default_renderer`.
1091 1092 Based on this configuration the method :meth:`search` behaves slightly
1092 1093 different.
1093 1094 """
1094 1095
1095 1096 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
1096 1097 path_re = re.compile(r'^docs?', re.IGNORECASE)
1097 1098
1098 1099 default_priorities = {
1099 1100 None: 0,
1100 1101 '.rst': 1,
1101 1102 '.md': 1,
1102 1103 '.rest': 2,
1103 1104 '.mkdn': 2,
1104 1105 '.text': 2,
1105 1106 '.txt': 3,
1106 1107 '.mdown': 3,
1107 1108 '.markdown': 4,
1108 1109 }
1109 1110
1110 1111 path_priority = {
1111 1112 'doc': 0,
1112 1113 'docs': 1,
1113 1114 }
1114 1115
1115 1116 FALLBACK_PRIORITY = 99
1116 1117
1117 1118 RENDERER_TO_EXTENSION = {
1118 1119 'rst': ['.rst', '.rest'],
1119 1120 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
1120 1121 }
1121 1122
1122 1123 def __init__(self, default_renderer=None):
1123 1124 self._default_renderer = default_renderer
1124 1125 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
1125 1126 default_renderer, [])
1126 1127
1127 1128 def search(self, commit, path='/'):
1128 1129 """
1129 1130 Find a readme in the given `commit`.
1130 1131 """
1132 # firstly, check the PATH type if it is actually a DIR
1133 if commit.get_node(path).kind != NodeKind.DIR:
1134 return None
1135
1131 1136 nodes = commit.get_nodes(path)
1132 1137 matches = self._match_readmes(nodes)
1133 1138 matches = self._sort_according_to_priority(matches)
1134 1139 if matches:
1135 1140 return matches[0].node
1136 1141
1137 1142 paths = self._match_paths(nodes)
1138 1143 paths = self._sort_paths_according_to_priority(paths)
1139 1144 for path in paths:
1140 1145 match = self.search(commit, path=path)
1141 1146 if match:
1142 1147 return match
1143 1148
1144 1149 return None
1145 1150
1146 1151 def _match_readmes(self, nodes):
1147 1152 for node in nodes:
1148 1153 if not node.is_file():
1149 1154 continue
1150 1155 path = node.path.rsplit('/', 1)[-1]
1151 1156 match = self.readme_re.match(path)
1152 1157 if match:
1153 1158 extension = match.group(1)
1154 1159 yield ReadmeMatch(node, match, self._priority(extension))
1155 1160
1156 1161 def _match_paths(self, nodes):
1157 1162 for node in nodes:
1158 1163 if not node.is_dir():
1159 1164 continue
1160 1165 match = self.path_re.match(node.path)
1161 1166 if match:
1162 1167 yield node.path
1163 1168
1164 1169 def _priority(self, extension):
1165 1170 renderer_priority = (
1166 1171 0 if extension in self._renderer_extensions else 1)
1167 1172 extension_priority = self.default_priorities.get(
1168 1173 extension, self.FALLBACK_PRIORITY)
1169 1174 return (renderer_priority, extension_priority)
1170 1175
1171 1176 def _sort_according_to_priority(self, matches):
1172 1177
1173 1178 def priority_and_path(match):
1174 1179 return (match.priority, match.path)
1175 1180
1176 1181 return sorted(matches, key=priority_and_path)
1177 1182
1178 1183 def _sort_paths_according_to_priority(self, paths):
1179 1184
1180 1185 def priority_and_path(path):
1181 1186 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1182 1187
1183 1188 return sorted(paths, key=priority_and_path)
1184 1189
1185 1190
1186 1191 class ReadmeMatch:
1187 1192
1188 1193 def __init__(self, node, match, priority):
1189 1194 self.node = node
1190 1195 self._match = match
1191 1196 self.priority = priority
1192 1197
1193 1198 @property
1194 1199 def path(self):
1195 1200 return self.node.path
1196 1201
1197 1202 def __repr__(self):
1198 1203 return f'<ReadmeMatch {self.path} priority={self.priority}'
@@ -1,278 +1,281 b''
1 1 <%text>
2 2 <div style="display: none">
3 3
4 4 <script>
5 5 var CG = new ColorGenerator();
6 6 </script>
7 7
8 8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
9 9
10 10 <%
11 11 if (size > 16) {
12 12 var gravatar_class = 'gravatar gravatar-large';
13 13 } else {
14 14 var gravatar_class = 'gravatar';
15 15 }
16 16
17 17 if (tooltip) {
18 18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
19 19 }
20 20
21 21 var data_hovercard_alt = username;
22 22
23 23 %>
24 24
25 25 <%
26 26 if (show_disabled) {
27 27 var user_cls = 'user user-disabled';
28 28 } else {
29 29 var user_cls = 'user';
30 30 }
31 31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
32 32 %>
33 33
34 34 <div class="rc-user">
35 35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
36 36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
37 37 </div>
38 38
39 39 </script>
40 40
41 41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 42 <%
43 43 if (create) {
44 44 var edit_visibility = 'visible';
45 45 } else {
46 46 var edit_visibility = 'hidden';
47 47 }
48 48
49 49 if (member.user_group && member.user_group.vote_rule) {
50 50 var reviewGroup = '<i class="icon-user-group"></i>';
51 51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
52 52 } else {
53 53 var reviewGroup = null;
54 54 var reviewGroupColor = 'transparent';
55 55 }
56 56 var rule_show = rule_show || false;
57 57
58 58 if (rule_show) {
59 59 var rule_visibility = 'table-cell';
60 60 } else {
61 61 var rule_visibility = 'none';
62 62 }
63 63
64 64 %>
65 65
66 66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67 67
68 68 <% if (create) { %>
69 69 <td style="width: 1px"></td>
70 70 <% } else { %>
71 71 <td style="width: 20px">
72 72 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
73 73 <i class="icon-eye" style="color: #0ac878"></i>
74 74 </div>
75 75 <% if (role === 'reviewer') { %>
76 76 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
77 77 <i class="icon-circle review-status-<%= review_status %>"></i>
78 78 </div>
79 79 <% } else if (role === 'observer') { %>
80 80 <div class="tooltip" title="Observer without voting right.">
81 81 <i class="icon-circle-thin"></i>
82 82 </div>
83 83 <% } %>
84 84 </td>
85 85 <% } %>
86 86
87 87
88 88 <% if (mandatory) { %>
89 89 <td style="text-align: right;width: 10px;">
90 90 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
91 91 <i class="icon-lock"></i>
92 92 </div>
93 93 </td>
94 94
95 95 <% } else { %>
96 96 <td style="text-align: right;width: 10px;">
97 97 <% if (allowed_to_update) { %>
98 98 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
99 99 <i class="icon-remove" style="color: #e85e4d;"></i>
100 100 </div>
101 101 <% } %>
102 102 </td>
103 103 <% } %>
104 104
105 105 <td>
106 106 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
107 107 <%-
108 108 renderTemplate('gravatarWithUser', {
109 109 'size': 16,
110 110 'show_disabled': false,
111 111 'tooltip': true,
112 112 'username': member.username,
113 113 'user_id': member.user_id,
114 114 'user_link': member.user_link,
115 115 'gravatar_url': member.gravatar_link
116 116 })
117 117 %>
118 118 </div>
119 119 <% if (reviewGroup !== null) { %>
120 120 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
121 121 <%- reviewGroup %>
122 122 </span>
123 123 <% } %>
124 124 </td>
125 125
126 126 </tr>
127 127
128 128 <tr id="reviewer_<%= member.user_id %>_rules">
129 129 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
130 130 <input type="hidden" name="__start__" value="reviewer:mapping">
131 131
132 132 <%if (member.user_group && member.user_group.vote_rule) { %>
133 133 <div class="reviewer_reason">
134 134
135 135 <%if (member.user_group.vote_rule == -1) {%>
136 136 - group votes required: ALL
137 137 <%} else {%>
138 138 - group votes required: <%= member.user_group.vote_rule %>
139 139 <%}%>
140 140 </div>
141 141 <%} %>
142 142
143 143 <input type="hidden" name="__start__" value="reasons:sequence">
144 144 <% for (var i = 0; i < reasons.length; i++) { %>
145 145 <% var reason = reasons[i] %>
146 146 <div class="reviewer_reason">- <%= reason %></div>
147 147 <input type="hidden" name="reason" value="<%= reason %>">
148 148 <% } %>
149 149 <input type="hidden" name="__end__" value="reasons:sequence">
150 150
151 151 <input type="hidden" name="__start__" value="rules:sequence">
152 152 <% for (var i = 0; i < member.rules.length; i++) { %>
153 153 <% var rule = member.rules[i] %>
154 154 <input type="hidden" name="rule_id" value="<%= rule %>">
155 155 <% } %>
156 156 <input type="hidden" name="__end__" value="rules:sequence">
157 157
158 158 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
159 159 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
160 160 <input type="hidden" name="role" value="<%= role %>"/>
161 161
162 162 <input type="hidden" name="__end__" value="reviewer:mapping">
163 163 </td>
164 164 </tr>
165 165
166 166 </script>
167 167
168 168 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
169 169
170 170 <%
171 171 if (size > 16) {
172 172 var gravatar_class = 'gravatar gravatar-large';
173 173 } else {
174 174 var gravatar_class = 'gravatar';
175 175 }
176 176
177 177 %>
178 178
179 179 <%
180 180 if (show_disabled) {
181 181 var user_cls = 'user user-disabled';
182 182 } else {
183 183 var user_cls = 'user';
184 184 }
185 185
186 186 %>
187 187
188 188 <div style='line-height: 20px'>
189 189 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
190 190 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
191 191 </div>
192 192
193 193 </script>
194 194
195 195
196 196 <script id="ejs_sideBarCommentHovercard" type="text/template" class="ejsTemplate">
197 197
198 198 <div>
199 199
200 200 <% if (is_todo) { %>
201 201 <% if (inline) { %>
202 202 <strong>Inline</strong> TODO (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
203 203 <% if (version_info) { %>
204 204 <%= version_info %>
205 205 <% } %>
206 206 <br/>
207 207 File: <code><%- file_name -%></code>
208 208 <% } else { %>
209 209 <% if (review_status) { %>
210 210 <i class="icon-circle review-status-<%= review_status %>"></i>
211 211 <% } %>
212 212 <strong>General</strong> TODO (<code>#<%- comment_id -%></code>)
213 213 <% if (version_info) { %>
214 214 <%= version_info %>
215 215 <% } %>
216 216 <% } %>
217 217 <% } else { %>
218 218 <% if (inline) { %>
219 219 <strong>Inline</strong> comment (<code>#<%- comment_id -%></code>) on line: <%= line_no %>
220 220 <% if (version_info) { %>
221 221 <%= version_info %>
222 222 <% } %>
223 223 <br/>
224 224 File: <code><%= file_name -%></code>
225 225 <% } else { %>
226 226 <% if (review_status) { %>
227 227 <i class="icon-circle review-status-<%= review_status %>"></i>
228 228 <% } %>
229 229 <strong>General</strong> comment (<code>#<%- comment_id -%></code>)
230 230 <% if (version_info) { %>
231 231 <%= version_info %>
232 232 <% } %>
233 233 <% } %>
234 234 <% } %>
235 235 <br/>
236 236 Created:
237 237 <time class="timeago" title="<%= created_on %>" datetime="<%= datetime %>"><%= $.timeago(datetime) %></time>
238 238
239 239 <% if (is_todo) { %>
240 240 <div style="text-align: left; padding-top: 5px">
241 241 <a class="btn btn-sm" href="#resolveTodo<%- comment_id -%>" onclick="Rhodecode.comments.resolveTodo(this, '<%- comment_id -%>'); return false">
242 242 <strong>Resolve TODO</strong>
243 243 </a>
244 244 </div>
245 245 <% } %>
246 246
247 247 </div>
248 248
249 249 </script>
250 250
251 251 <script id="ejs_commentHelpHovercard" type="text/template" class="ejsTemplate">
252 252
253 253 <div>
254 254 Use <strong>@username</strong> mention syntax to send direct notification to this RhodeCode user.<br/>
255 255 Typing / starts autocomplete for certain action, e.g set review status, or comment type. <br/>
256 256 <br/>
257 257 Use <strong>Cmd/ctrl+enter</strong> to submit comment, or <strong>Shift+Cmd/ctrl+enter</strong> to submit a draft.<br/>
258 258 <br/>
259 259 <strong>Draft comments</strong> are private to the author, and trigger no notification to others.<br/>
260 260 They are permanent until deleted, or converted to regular comments.<br/>
261 261 <br/>
262 262 <br/>
263 263 </div>
264 264
265 265 </script>
266 266
267
267 <script id="ejs_submoduleHovercard" type="text/template" class="ejsTemplate">
268 <strong>Submodule Node</strong><br/>
269 <pre><%= submodule_url %></pre>
270 </script>
268 271
269 272 ##// END OF EJS Templates
270 273 </div>
271 274
272 275
273 276 <script>
274 277 // registers the templates into global cache
275 278 registerTemplates();
276 279 </script>
277 280
278 281 </%text>
@@ -1,98 +1,112 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 <%doc>
4 Please note the content of this file is cached, so changes here might not be reflected when editing.
5 add ?no-cache=true into the file url to disable caches.
6
7 e.g
8 http://docker-dev:10020/ipython/files/master/IPython/frontend/html/notebook/static?no-cache=1
9
10 </%doc>
3 11 <%
4 12 at_ref = request.GET.get('at')
5 13 if at_ref:
6 14 query={'at': at_ref}
7 15 default_landing_ref = at_ref or c.rhodecode_db_repo.landing_ref_name
8 16 else:
9 17 query=None
10 18 default_landing_ref = c.commit.raw_id
11 19 %>
12 20 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
13 21 <table class="code-browser rctable table-bordered">
14 22 <thead>
15 23 <tr>
16 24 <th>${_('Name')}</th>
17 25 <th>${_('Size')}</th>
18 26 <th>${_('Modified')}</th>
19 27 <th>${_('Last Commit')}</th>
20 28 <th>${_('Author')}</th>
21 29 </tr>
22 30 </thead>
23 31
24 32 <tbody id="tbody">
25 33 <tr>
26 34 <td colspan="5">
27 35 ${h.files_breadcrumbs(c.repo_name, c.rhodecode_db_repo.repo_type, c.commit.raw_id, c.file.path, c.rhodecode_db_repo.landing_ref_name, request.GET.get('at'), limit_items=True)}
28 36 </td>
29 37 </tr>
30 38
31 39 <% has_files = False %>
32 % for cnt,node in enumerate(c.file):
33 <% has_files = True %>
34 <tr class="parity${(cnt % 2)}">
35 <td class="td-componentname">
36 % if node.is_submodule():
37 <span class="submodule-dir">
38 % if node.url.startswith('http://') or node.url.startswith('https://'):
39 <a href="${node.url}">
40 <i class="icon-directory browser-dir"></i>${node.name}
40 % if not c.file.is_submodule():
41 % for cnt, node in enumerate(c.file):
42 <% has_files = True %>
43 <tr class="parity${(cnt % 2)}">
44 <td class="td-componentname">
45 % if node.is_submodule():
46 <span class="submodule-dir">
47 % if node.url.startswith('http://') or node.url.startswith('https://'):
48 <a href="${node.url}">
49 <i class="icon-directory browser-dir"></i><span class="tooltip-hovercard" data-hovercard-alt="${node.url}" data-hovercard-url="javascript:renderTemplate('submoduleHovercard', {'submodule_url':'${node.url}'})">${node.name}</span>
50 </a>
51 % else:
52 <i class="icon-directory browser-dir"></i><span class="tooltip-hovercard" data-hovercard-alt="${node.url}" data-hovercard-url="javascript:renderTemplate('submoduleHovercard', {'submodule_url':'${node.url}'})">${node.name}</span>
53 % endif
54 </span>
55 % else:
56 <a href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path=h.safe_str(node.path), ref_name=default_landing_ref, commit_id=c.commit.raw_id, query=query)}">
57 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
41 58 </a>
42 % else:
43 <i class="icon-directory browser-dir"></i>${node.name}
44 59 % endif
45 </span>
46 % else:
47 <a href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path=h.safe_str(node.path), ref_name=default_landing_ref, commit_id=c.commit.raw_id, query=query)}">
48 <i class="${('icon-file-text browser-file' if node.is_file() else 'icon-directory browser-dir')}"></i>${node.name}
49 </a>
50 % endif
51 </td>
52 %if node.is_file():
53 <td class="td-size" data-attr-name="size">
54 % if c.full_load:
55 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
56 % else:
57 ${_('Loading ...')}
58 % endif
59 </td>
60 <td class="td-time" data-attr-name="modified_at">
61 % if c.full_load:
62 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
63 % endif
64 </td>
65 <td class="td-hash" data-attr-name="commit_id">
66 % if c.full_load:
67 <div class="tooltip-hovercard" data-hovercard-alt="${node.last_commit.message}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=node.last_commit.raw_id)}">
68 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
69 </div>
70 % endif
71 </td>
72 <td class="td-user" data-attr-name="author">
73 % if c.full_load:
74 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
75 % endif
76 </td>
77 %else:
78 <td></td>
79 <td></td>
80 <td></td>
81 <td></td>
82 %endif
83 </tr>
84 % endfor
60 </td>
61 %if node.is_file():
62 <td class="td-size" data-attr-name="size">
63 % if c.full_load:
64 <span data-size="${node.size}">${h.format_byte_size_binary(node.size)}</span>
65 % else:
66 ${_('Loading ...')}
67 % endif
68 </td>
69 <td class="td-time" data-attr-name="modified_at">
70 % if c.full_load:
71 <span data-date="${node.last_commit.date}">${h.age_component(node.last_commit.date)}</span>
72 % endif
73 </td>
74 <td class="td-hash" data-attr-name="commit_id">
75 % if c.full_load:
76 <div class="tooltip-hovercard" data-hovercard-alt="${node.last_commit.message}" data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=c.repo_name, commit_id=node.last_commit.raw_id)}">
77 <pre data-commit-id="${node.last_commit.raw_id}">r${node.last_commit.idx}:${node.last_commit.short_id}</pre>
78 </div>
79 % endif
80 </td>
81 <td class="td-user" data-attr-name="author">
82 % if c.full_load:
83 <span data-author="${node.last_commit.author}">${h.gravatar_with_user(request, node.last_commit.author, tooltip=True)|n}</span>
84 % endif
85 </td>
86 %else:
87 <td></td>
88 <td></td>
89 <td></td>
90 <td></td>
91 %endif
92 </tr>
93 % endfor
94 % endif
85 95
86 96 % if not has_files:
87 97 <tr>
88 98 <td colspan="5">
89 99 ##empty-dir mostly SVN
90 &nbsp;
100
101 ## submodule if we somehow endup
102 % if c.file.is_submodule():
103 <span class="submodule-dir">Submodule ${h.escape(c.file.name)}</span>
104 %endif
91 105 </td>
92 106 </tr>
93 107 % endif
94 108
95 109 </tbody>
96 110 <tbody id="tbody_filtered"></tbody>
97 111 </table>
98 112 </div>
@@ -1,34 +1,32 b''
1 1 <%def name="title(*args)">
2 2 ${_('{} Files').format(c.repo_name)}
3 3 %if hasattr(c,'file'):
4 4 &middot; ${(h.safe_str(c.file.path) or '\\')}
5 5 %endif
6 6
7 7 %if c.rhodecode_name:
8 8 &middot; ${h.branding(c.rhodecode_name)}
9 9 %endif
10 10 </%def>
11 11
12 12 <div>
13 13
14 14 <div class="summary-detail">
15 15 <div class="summary-detail-header">
16 16
17 17 </div><!--end summary-detail-header-->
18
19 % if c.file.is_submodule():
20 <span class="submodule-dir">Submodule ${h.escape(c.file.name)}</span>
21 % elif c.file.is_dir():
18 % if c.file.is_dir() or c.file.is_submodule():
22 19 <%include file='files_tree_header.mako'/>
23 20 % else:
24 21 <%include file='files_source_header.mako'/>
25 22 % endif
26 23
27 24 </div> <!--end summary-detail-->
28 % if c.file.is_dir():
25
26 % if c.file.is_dir() or c.file.is_submodule():
29 27 <%include file='files_browser.mako'/>
30 28 % else:
31 29 <%include file='files_source.mako'/>
32 30 % endif
33 31
34 32 </div>
General Comments 0
You need to be logged in to leave comments. Login now