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