##// END OF EJS Templates
files: added branch permissions checks into web edit operations.
marcink -
r2978:7cabaaed default
parent child Browse files
Show More
@@ -1,1297 +1,1385 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
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 29 from pyramid.view import view_config
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 from rhodecode.controllers.utils import parse_path_ref
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.exceptions import NonRelativePathError
40 40 from rhodecode.lib.codeblocks import (
41 41 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
42 42 from rhodecode.lib.utils2 import (
43 43 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
46 46 from rhodecode.lib.vcs import path as vcspath
47 47 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 48 from rhodecode.lib.vcs.conf import settings
49 49 from rhodecode.lib.vcs.nodes import FileNode
50 50 from rhodecode.lib.vcs.exceptions import (
51 51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 53 NodeDoesNotExistError, CommitError, NodeError)
54 54
55 55 from rhodecode.model.scm import ScmModel
56 56 from rhodecode.model.db import Repository
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 class RepoFilesView(RepoAppView):
62 62
63 63 @staticmethod
64 64 def adjust_file_path_for_svn(f_path, repo):
65 65 """
66 66 Computes the relative path of `f_path`.
67 67
68 68 This is mainly based on prefix matching of the recognized tags and
69 69 branches in the underlying repository.
70 70 """
71 71 tags_and_branches = itertools.chain(
72 72 repo.branches.iterkeys(),
73 73 repo.tags.iterkeys())
74 74 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
75 75
76 76 for name in tags_and_branches:
77 77 if f_path.startswith('{}/'.format(name)):
78 78 f_path = vcspath.relpath(f_path, name)
79 79 break
80 80 return f_path
81 81
82 82 def load_default_context(self):
83 83 c = self._get_local_tmpl_context(include_app_defaults=True)
84 84 c.rhodecode_repo = self.rhodecode_vcs_repo
85 85 return c
86 86
87 87 def _ensure_not_locked(self):
88 88 _ = self.request.translate
89 89
90 90 repo = self.db_repo
91 91 if repo.enable_locking and repo.locked[0]:
92 92 h.flash(_('This repository has been locked by %s on %s')
93 93 % (h.person_by_id(repo.locked[0]),
94 94 h.format_date(h.time_to_datetime(repo.locked[1]))),
95 95 'warning')
96 96 files_url = h.route_path(
97 97 'repo_files:default_path',
98 98 repo_name=self.db_repo_name, commit_id='tip')
99 99 raise HTTPFound(files_url)
100 100
101 def check_branch_permission(self, branch_name):
102 _ = self.request.translate
103
104 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
105 self.db_repo_name, branch_name)
106 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
107 h.flash(
108 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
109 'warning')
110 files_url = h.route_path(
111 'repo_files:default_path',
112 repo_name=self.db_repo_name, commit_id='tip')
113 raise HTTPFound(files_url)
114
101 115 def _get_commit_and_path(self):
102 116 default_commit_id = self.db_repo.landing_rev[1]
103 117 default_f_path = '/'
104 118
105 119 commit_id = self.request.matchdict.get(
106 120 'commit_id', default_commit_id)
107 121 f_path = self._get_f_path(self.request.matchdict, default_f_path)
108 122 return commit_id, f_path
109 123
110 124 def _get_default_encoding(self, c):
111 125 enc_list = getattr(c, 'default_encodings', [])
112 126 return enc_list[0] if enc_list else 'UTF-8'
113 127
114 128 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
115 129 """
116 130 This is a safe way to get commit. If an error occurs it redirects to
117 131 tip with proper message
118 132
119 133 :param commit_id: id of commit to fetch
120 134 :param redirect_after: toggle redirection
121 135 """
122 136 _ = self.request.translate
123 137
124 138 try:
125 139 return self.rhodecode_vcs_repo.get_commit(commit_id)
126 140 except EmptyRepositoryError:
127 141 if not redirect_after:
128 142 return None
129 143
130 144 _url = h.route_path(
131 145 'repo_files_add_file',
132 146 repo_name=self.db_repo_name, commit_id=0, f_path='',
133 147 _anchor='edit')
134 148
135 149 if h.HasRepoPermissionAny(
136 150 'repository.write', 'repository.admin')(self.db_repo_name):
137 151 add_new = h.link_to(
138 152 _('Click here to add a new file.'), _url, class_="alert-link")
139 153 else:
140 154 add_new = ""
141 155
142 156 h.flash(h.literal(
143 157 _('There are no files yet. %s') % add_new), category='warning')
144 158 raise HTTPFound(
145 159 h.route_path('repo_summary', repo_name=self.db_repo_name))
146 160
147 161 except (CommitDoesNotExistError, LookupError):
148 162 msg = _('No such commit exists for this repository')
149 163 h.flash(msg, category='error')
150 164 raise HTTPNotFound()
151 165 except RepositoryError as e:
152 166 h.flash(safe_str(h.escape(e)), category='error')
153 167 raise HTTPNotFound()
154 168
155 169 def _get_filenode_or_redirect(self, commit_obj, path):
156 170 """
157 171 Returns file_node, if error occurs or given path is directory,
158 172 it'll redirect to top level path
159 173 """
160 174 _ = self.request.translate
161 175
162 176 try:
163 177 file_node = commit_obj.get_node(path)
164 178 if file_node.is_dir():
165 179 raise RepositoryError('The given path is a directory')
166 180 except CommitDoesNotExistError:
167 181 log.exception('No such commit exists for this repository')
168 182 h.flash(_('No such commit exists for this repository'), category='error')
169 183 raise HTTPNotFound()
170 184 except RepositoryError as e:
171 185 log.warning('Repository error while fetching '
172 186 'filenode `%s`. Err:%s', path, e)
173 187 h.flash(safe_str(h.escape(e)), category='error')
174 188 raise HTTPNotFound()
175 189
176 190 return file_node
177 191
178 192 def _is_valid_head(self, commit_id, repo):
179 # check if commit is a branch identifier- basically we cannot
180 # create multiple heads via file editing
181 valid_heads = repo.branches.keys() + repo.branches.values()
193 branch_name = sha_commit_id = ''
194 is_head = False
182 195
183 196 if h.is_svn(repo) and not repo.is_empty():
184 # Note: Subversion only has one head, we add it here in case there
185 # is no branch matched.
186 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
197 # Note: Subversion only has one head.
198 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
199 is_head = True
200 return branch_name, sha_commit_id, is_head
187 201
188 # check if commit is a branch name or branch hash
189 return commit_id in valid_heads
202 for _branch_name, branch_commit_id in repo.branches.items():
203 # simple case we pass in branch name, it's a HEAD
204 if commit_id == _branch_name:
205 is_head = True
206 branch_name = _branch_name
207 sha_commit_id = branch_commit_id
208 break
209 # case when we pass in full sha commit_id, which is a head
210 elif commit_id == branch_commit_id:
211 is_head = True
212 branch_name = _branch_name
213 sha_commit_id = branch_commit_id
214 break
215
216 # checked branches, means we only need to try to get the branch/commit_sha
217 if not repo.is_empty:
218 commit = repo.get_commit(commit_id=commit_id)
219 if commit:
220 branch_name = commit.branch
221 sha_commit_id = commit.raw_id
222
223 return branch_name, sha_commit_id, is_head
190 224
191 225 def _get_tree_at_commit(
192 226 self, c, commit_id, f_path, full_load=False):
193 227
194 228 repo_id = self.db_repo.repo_id
195 229
196 230 cache_seconds = safe_int(
197 231 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
198 232 cache_on = cache_seconds > 0
199 233 log.debug(
200 234 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
201 235 'with caching: %s[TTL: %ss]' % (
202 236 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
203 237
204 238 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
205 239 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
206 240
207 241 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
208 242 condition=cache_on)
209 243 def compute_file_tree(repo_id, commit_id, f_path, full_load):
210 244 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
211 245 repo_id, commit_id, f_path)
212 246
213 247 c.full_load = full_load
214 248 return render(
215 249 'rhodecode:templates/files/files_browser_tree.mako',
216 250 self._get_template_context(c), self.request)
217 251
218 252 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
219 253
220 254 def _get_archive_spec(self, fname):
221 255 log.debug('Detecting archive spec for: `%s`', fname)
222 256
223 257 fileformat = None
224 258 ext = None
225 259 content_type = None
226 260 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
227 261 content_type, extension = ext_data
228 262
229 263 if fname.endswith(extension):
230 264 fileformat = a_type
231 265 log.debug('archive is of type: %s', fileformat)
232 266 ext = extension
233 267 break
234 268
235 269 if not fileformat:
236 270 raise ValueError()
237 271
238 272 # left over part of whole fname is the commit
239 273 commit_id = fname[:-len(ext)]
240 274
241 275 return commit_id, ext, fileformat, content_type
242 276
243 277 @LoginRequired()
244 278 @HasRepoPermissionAnyDecorator(
245 279 'repository.read', 'repository.write', 'repository.admin')
246 280 @view_config(
247 281 route_name='repo_archivefile', request_method='GET',
248 282 renderer=None)
249 283 def repo_archivefile(self):
250 284 # archive cache config
251 285 from rhodecode import CONFIG
252 286 _ = self.request.translate
253 287 self.load_default_context()
254 288
255 289 fname = self.request.matchdict['fname']
256 290 subrepos = self.request.GET.get('subrepos') == 'true'
257 291
258 292 if not self.db_repo.enable_downloads:
259 293 return Response(_('Downloads disabled'))
260 294
261 295 try:
262 296 commit_id, ext, fileformat, content_type = \
263 297 self._get_archive_spec(fname)
264 298 except ValueError:
265 299 return Response(_('Unknown archive type for: `{}`').format(
266 300 h.escape(fname)))
267 301
268 302 try:
269 303 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
270 304 except CommitDoesNotExistError:
271 305 return Response(_('Unknown commit_id {}').format(
272 306 h.escape(commit_id)))
273 307 except EmptyRepositoryError:
274 308 return Response(_('Empty repository'))
275 309
276 310 archive_name = '%s-%s%s%s' % (
277 311 safe_str(self.db_repo_name.replace('/', '_')),
278 312 '-sub' if subrepos else '',
279 313 safe_str(commit.short_id), ext)
280 314
281 315 use_cached_archive = False
282 316 archive_cache_enabled = CONFIG.get(
283 317 'archive_cache_dir') and not self.request.GET.get('no_cache')
318 cached_archive_path = None
284 319
285 320 if archive_cache_enabled:
286 321 # check if we it's ok to write
287 322 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 323 os.makedirs(CONFIG['archive_cache_dir'])
289 324 cached_archive_path = os.path.join(
290 325 CONFIG['archive_cache_dir'], archive_name)
291 326 if os.path.isfile(cached_archive_path):
292 327 log.debug('Found cached archive in %s', cached_archive_path)
293 328 fd, archive = None, cached_archive_path
294 329 use_cached_archive = True
295 330 else:
296 331 log.debug('Archive %s is not yet cached', archive_name)
297 332
298 333 if not use_cached_archive:
299 334 # generate new archive
300 335 fd, archive = tempfile.mkstemp()
301 336 log.debug('Creating new temp archive in %s', archive)
302 337 try:
303 338 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 339 except ImproperArchiveTypeError:
305 340 return _('Unknown archive type')
306 341 if archive_cache_enabled:
307 342 # if we generated the archive and we have cache enabled
308 343 # let's use this for future
309 344 log.debug('Storing new archive in %s', cached_archive_path)
310 345 shutil.move(archive, cached_archive_path)
311 346 archive = cached_archive_path
312 347
313 348 # store download action
314 349 audit_logger.store_web(
315 350 'repo.archive.download', action_data={
316 351 'user_agent': self.request.user_agent,
317 352 'archive_name': archive_name,
318 353 'archive_spec': fname,
319 354 'archive_cached': use_cached_archive},
320 355 user=self._rhodecode_user,
321 356 repo=self.db_repo,
322 357 commit=True
323 358 )
324 359
325 def get_chunked_archive(archive):
326 with open(archive, 'rb') as stream:
360 def get_chunked_archive(archive_path):
361 with open(archive_path, 'rb') as stream:
327 362 while True:
328 363 data = stream.read(16 * 1024)
329 364 if not data:
330 365 if fd: # fd means we used temporary file
331 366 os.close(fd)
332 367 if not archive_cache_enabled:
333 log.debug('Destroying temp archive %s', archive)
334 os.remove(archive)
368 log.debug('Destroying temp archive %s', archive_path)
369 os.remove(archive_path)
335 370 break
336 371 yield data
337 372
338 373 response = Response(app_iter=get_chunked_archive(archive))
339 374 response.content_disposition = str(
340 375 'attachment; filename=%s' % archive_name)
341 376 response.content_type = str(content_type)
342 377
343 378 return response
344 379
345 380 def _get_file_node(self, commit_id, f_path):
346 381 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 382 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 383 try:
349 384 node = commit.get_node(f_path)
350 385 if node.is_dir():
351 386 raise NodeError('%s path is a %s not a file'
352 387 % (node, type(node)))
353 388 except NodeDoesNotExistError:
354 389 commit = EmptyCommit(
355 390 commit_id=commit_id,
356 391 idx=commit.idx,
357 392 repo=commit.repository,
358 393 alias=commit.repository.alias,
359 394 message=commit.message,
360 395 author=commit.author,
361 396 date=commit.date)
362 397 node = FileNode(f_path, '', commit=commit)
363 398 else:
364 399 commit = EmptyCommit(
365 400 repo=self.rhodecode_vcs_repo,
366 401 alias=self.rhodecode_vcs_repo.alias)
367 402 node = FileNode(f_path, '', commit=commit)
368 403 return node
369 404
370 405 @LoginRequired()
371 406 @HasRepoPermissionAnyDecorator(
372 407 'repository.read', 'repository.write', 'repository.admin')
373 408 @view_config(
374 409 route_name='repo_files_diff', request_method='GET',
375 410 renderer=None)
376 411 def repo_files_diff(self):
377 412 c = self.load_default_context()
378 413 f_path = self._get_f_path(self.request.matchdict)
379 414 diff1 = self.request.GET.get('diff1', '')
380 415 diff2 = self.request.GET.get('diff2', '')
381 416
382 417 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383 418
384 419 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 420 line_context = self.request.GET.get('context', 3)
386 421
387 422 if not any((diff1, diff2)):
388 423 h.flash(
389 424 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 425 category='error')
391 426 raise HTTPBadRequest()
392 427
393 428 c.action = self.request.GET.get('diff')
394 429 if c.action not in ['download', 'raw']:
395 430 compare_url = h.route_path(
396 431 'repo_compare',
397 432 repo_name=self.db_repo_name,
398 433 source_ref_type='rev',
399 434 source_ref=diff1,
400 435 target_repo=self.db_repo_name,
401 436 target_ref_type='rev',
402 437 target_ref=diff2,
403 438 _query=dict(f_path=f_path))
404 439 # redirect to new view if we render diff
405 440 raise HTTPFound(compare_url)
406 441
407 442 try:
408 443 node1 = self._get_file_node(diff1, path1)
409 444 node2 = self._get_file_node(diff2, f_path)
410 445 except (RepositoryError, NodeError):
411 446 log.exception("Exception while trying to get node from repository")
412 447 raise HTTPFound(
413 448 h.route_path('repo_files', repo_name=self.db_repo_name,
414 449 commit_id='tip', f_path=f_path))
415 450
416 451 if all(isinstance(node.commit, EmptyCommit)
417 452 for node in (node1, node2)):
418 453 raise HTTPNotFound()
419 454
420 455 c.commit_1 = node1.commit
421 456 c.commit_2 = node2.commit
422 457
423 458 if c.action == 'download':
424 459 _diff = diffs.get_gitdiff(node1, node2,
425 460 ignore_whitespace=ignore_whitespace,
426 461 context=line_context)
427 462 diff = diffs.DiffProcessor(_diff, format='gitdiff')
428 463
429 464 response = Response(self.path_filter.get_raw_patch(diff))
430 465 response.content_type = 'text/plain'
431 466 response.content_disposition = (
432 467 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
433 468 )
434 469 charset = self._get_default_encoding(c)
435 470 if charset:
436 471 response.charset = charset
437 472 return response
438 473
439 474 elif c.action == 'raw':
440 475 _diff = diffs.get_gitdiff(node1, node2,
441 476 ignore_whitespace=ignore_whitespace,
442 477 context=line_context)
443 478 diff = diffs.DiffProcessor(_diff, format='gitdiff')
444 479
445 480 response = Response(self.path_filter.get_raw_patch(diff))
446 481 response.content_type = 'text/plain'
447 482 charset = self._get_default_encoding(c)
448 483 if charset:
449 484 response.charset = charset
450 485 return response
451 486
452 487 # in case we ever end up here
453 488 raise HTTPNotFound()
454 489
455 490 @LoginRequired()
456 491 @HasRepoPermissionAnyDecorator(
457 492 'repository.read', 'repository.write', 'repository.admin')
458 493 @view_config(
459 494 route_name='repo_files_diff_2way_redirect', request_method='GET',
460 495 renderer=None)
461 496 def repo_files_diff_2way_redirect(self):
462 497 """
463 498 Kept only to make OLD links work
464 499 """
465 500 f_path = self._get_f_path_unchecked(self.request.matchdict)
466 501 diff1 = self.request.GET.get('diff1', '')
467 502 diff2 = self.request.GET.get('diff2', '')
468 503
469 504 if not any((diff1, diff2)):
470 505 h.flash(
471 506 'Need query parameter "diff1" or "diff2" to generate a diff.',
472 507 category='error')
473 508 raise HTTPBadRequest()
474 509
475 510 compare_url = h.route_path(
476 511 'repo_compare',
477 512 repo_name=self.db_repo_name,
478 513 source_ref_type='rev',
479 514 source_ref=diff1,
480 515 target_ref_type='rev',
481 516 target_ref=diff2,
482 517 _query=dict(f_path=f_path, diffmode='sideside',
483 518 target_repo=self.db_repo_name,))
484 519 raise HTTPFound(compare_url)
485 520
486 521 @LoginRequired()
487 522 @HasRepoPermissionAnyDecorator(
488 523 'repository.read', 'repository.write', 'repository.admin')
489 524 @view_config(
490 525 route_name='repo_files', request_method='GET',
491 526 renderer=None)
492 527 @view_config(
493 528 route_name='repo_files:default_path', request_method='GET',
494 529 renderer=None)
495 530 @view_config(
496 531 route_name='repo_files:default_commit', request_method='GET',
497 532 renderer=None)
498 533 @view_config(
499 534 route_name='repo_files:rendered', request_method='GET',
500 535 renderer=None)
501 536 @view_config(
502 537 route_name='repo_files:annotated', request_method='GET',
503 538 renderer=None)
504 539 def repo_files(self):
505 540 c = self.load_default_context()
506 541
507 542 view_name = getattr(self.request.matched_route, 'name', None)
508 543
509 544 c.annotate = view_name == 'repo_files:annotated'
510 545 # default is false, but .rst/.md files later are auto rendered, we can
511 546 # overwrite auto rendering by setting this GET flag
512 547 c.renderer = view_name == 'repo_files:rendered' or \
513 548 not self.request.GET.get('no-render', False)
514 549
515 550 # redirect to given commit_id from form if given
516 551 get_commit_id = self.request.GET.get('at_rev', None)
517 552 if get_commit_id:
518 553 self._get_commit_or_redirect(get_commit_id)
519 554
520 555 commit_id, f_path = self._get_commit_and_path()
521 556 c.commit = self._get_commit_or_redirect(commit_id)
522 557 c.branch = self.request.GET.get('branch', None)
523 558 c.f_path = f_path
524 559
525 560 # prev link
526 561 try:
527 562 prev_commit = c.commit.prev(c.branch)
528 563 c.prev_commit = prev_commit
529 564 c.url_prev = h.route_path(
530 565 'repo_files', repo_name=self.db_repo_name,
531 566 commit_id=prev_commit.raw_id, f_path=f_path)
532 567 if c.branch:
533 568 c.url_prev += '?branch=%s' % c.branch
534 569 except (CommitDoesNotExistError, VCSError):
535 570 c.url_prev = '#'
536 571 c.prev_commit = EmptyCommit()
537 572
538 573 # next link
539 574 try:
540 575 next_commit = c.commit.next(c.branch)
541 576 c.next_commit = next_commit
542 577 c.url_next = h.route_path(
543 578 'repo_files', repo_name=self.db_repo_name,
544 579 commit_id=next_commit.raw_id, f_path=f_path)
545 580 if c.branch:
546 581 c.url_next += '?branch=%s' % c.branch
547 582 except (CommitDoesNotExistError, VCSError):
548 583 c.url_next = '#'
549 584 c.next_commit = EmptyCommit()
550 585
551 586 # files or dirs
552 587 try:
553 588 c.file = c.commit.get_node(f_path)
554 589 c.file_author = True
555 590 c.file_tree = ''
556 591
557 592 # load file content
558 593 if c.file.is_file():
559 594 c.lf_node = c.file.get_largefile_node()
560 595
561 596 c.file_source_page = 'true'
562 597 c.file_last_commit = c.file.last_commit
563 598 if c.file.size < c.visual.cut_off_limit_diff:
564 599 if c.annotate: # annotation has precedence over renderer
565 600 c.annotated_lines = filenode_as_annotated_lines_tokens(
566 601 c.file
567 602 )
568 603 else:
569 604 c.renderer = (
570 605 c.renderer and h.renderer_from_filename(c.file.path)
571 606 )
572 607 if not c.renderer:
573 608 c.lines = filenode_as_lines_tokens(c.file)
574 609
575 c.on_branch_head = self._is_valid_head(
610 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
576 611 commit_id, self.rhodecode_vcs_repo)
612 c.on_branch_head = is_head
577 613
578 614 branch = c.commit.branch if (
579 615 c.commit.branch and '/' not in c.commit.branch) else None
580 616 c.branch_or_raw_id = branch or c.commit.raw_id
581 617 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
582 618
583 619 author = c.file_last_commit.author
584 620 c.authors = [[
585 621 h.email(author),
586 622 h.person(author, 'username_or_name_or_email'),
587 623 1
588 624 ]]
589 625
590 626 else: # load tree content at path
591 627 c.file_source_page = 'false'
592 628 c.authors = []
593 629 # this loads a simple tree without metadata to speed things up
594 630 # later via ajax we call repo_nodetree_full and fetch whole
595 631 c.file_tree = self._get_tree_at_commit(
596 632 c, c.commit.raw_id, f_path)
597 633
598 634 except RepositoryError as e:
599 635 h.flash(safe_str(h.escape(e)), category='error')
600 636 raise HTTPNotFound()
601 637
602 638 if self.request.environ.get('HTTP_X_PJAX'):
603 639 html = render('rhodecode:templates/files/files_pjax.mako',
604 640 self._get_template_context(c), self.request)
605 641 else:
606 642 html = render('rhodecode:templates/files/files.mako',
607 643 self._get_template_context(c), self.request)
608 644 return Response(html)
609 645
610 646 @HasRepoPermissionAnyDecorator(
611 647 'repository.read', 'repository.write', 'repository.admin')
612 648 @view_config(
613 649 route_name='repo_files:annotated_previous', request_method='GET',
614 650 renderer=None)
615 651 def repo_files_annotated_previous(self):
616 652 self.load_default_context()
617 653
618 654 commit_id, f_path = self._get_commit_and_path()
619 655 commit = self._get_commit_or_redirect(commit_id)
620 656 prev_commit_id = commit.raw_id
621 657 line_anchor = self.request.GET.get('line_anchor')
622 658 is_file = False
623 659 try:
624 660 _file = commit.get_node(f_path)
625 661 is_file = _file.is_file()
626 662 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
627 663 pass
628 664
629 665 if is_file:
630 666 history = commit.get_file_history(f_path)
631 667 prev_commit_id = history[1].raw_id \
632 668 if len(history) > 1 else prev_commit_id
633 669 prev_url = h.route_path(
634 670 'repo_files:annotated', repo_name=self.db_repo_name,
635 671 commit_id=prev_commit_id, f_path=f_path,
636 672 _anchor='L{}'.format(line_anchor))
637 673
638 674 raise HTTPFound(prev_url)
639 675
640 676 @LoginRequired()
641 677 @HasRepoPermissionAnyDecorator(
642 678 'repository.read', 'repository.write', 'repository.admin')
643 679 @view_config(
644 680 route_name='repo_nodetree_full', request_method='GET',
645 681 renderer=None, xhr=True)
646 682 @view_config(
647 683 route_name='repo_nodetree_full:default_path', request_method='GET',
648 684 renderer=None, xhr=True)
649 685 def repo_nodetree_full(self):
650 686 """
651 687 Returns rendered html of file tree that contains commit date,
652 688 author, commit_id for the specified combination of
653 689 repo, commit_id and file path
654 690 """
655 691 c = self.load_default_context()
656 692
657 693 commit_id, f_path = self._get_commit_and_path()
658 694 commit = self._get_commit_or_redirect(commit_id)
659 695 try:
660 696 dir_node = commit.get_node(f_path)
661 697 except RepositoryError as e:
662 698 return Response('error: {}'.format(h.escape(safe_str(e))))
663 699
664 700 if dir_node.is_file():
665 701 return Response('')
666 702
667 703 c.file = dir_node
668 704 c.commit = commit
669 705
670 706 html = self._get_tree_at_commit(
671 707 c, commit.raw_id, dir_node.path, full_load=True)
672 708
673 709 return Response(html)
674 710
675 711 def _get_attachement_disposition(self, f_path):
676 712 return 'attachment; filename=%s' % \
677 713 safe_str(f_path.split(Repository.NAME_SEP)[-1])
678 714
679 715 @LoginRequired()
680 716 @HasRepoPermissionAnyDecorator(
681 717 'repository.read', 'repository.write', 'repository.admin')
682 718 @view_config(
683 719 route_name='repo_file_raw', request_method='GET',
684 720 renderer=None)
685 721 def repo_file_raw(self):
686 722 """
687 723 Action for show as raw, some mimetypes are "rendered",
688 724 those include images, icons.
689 725 """
690 726 c = self.load_default_context()
691 727
692 728 commit_id, f_path = self._get_commit_and_path()
693 729 commit = self._get_commit_or_redirect(commit_id)
694 730 file_node = self._get_filenode_or_redirect(commit, f_path)
695 731
696 732 raw_mimetype_mapping = {
697 733 # map original mimetype to a mimetype used for "show as raw"
698 734 # you can also provide a content-disposition to override the
699 735 # default "attachment" disposition.
700 736 # orig_type: (new_type, new_dispo)
701 737
702 738 # show images inline:
703 739 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
704 740 # for example render an SVG with javascript inside or even render
705 741 # HTML.
706 742 'image/x-icon': ('image/x-icon', 'inline'),
707 743 'image/png': ('image/png', 'inline'),
708 744 'image/gif': ('image/gif', 'inline'),
709 745 'image/jpeg': ('image/jpeg', 'inline'),
710 746 'application/pdf': ('application/pdf', 'inline'),
711 747 }
712 748
713 749 mimetype = file_node.mimetype
714 750 try:
715 751 mimetype, disposition = raw_mimetype_mapping[mimetype]
716 752 except KeyError:
717 753 # we don't know anything special about this, handle it safely
718 754 if file_node.is_binary:
719 755 # do same as download raw for binary files
720 756 mimetype, disposition = 'application/octet-stream', 'attachment'
721 757 else:
722 758 # do not just use the original mimetype, but force text/plain,
723 759 # otherwise it would serve text/html and that might be unsafe.
724 760 # Note: underlying vcs library fakes text/plain mimetype if the
725 761 # mimetype can not be determined and it thinks it is not
726 762 # binary.This might lead to erroneous text display in some
727 763 # cases, but helps in other cases, like with text files
728 764 # without extension.
729 765 mimetype, disposition = 'text/plain', 'inline'
730 766
731 767 if disposition == 'attachment':
732 768 disposition = self._get_attachement_disposition(f_path)
733 769
734 770 def stream_node():
735 771 yield file_node.raw_bytes
736 772
737 773 response = Response(app_iter=stream_node())
738 774 response.content_disposition = disposition
739 775 response.content_type = mimetype
740 776
741 777 charset = self._get_default_encoding(c)
742 778 if charset:
743 779 response.charset = charset
744 780
745 781 return response
746 782
747 783 @LoginRequired()
748 784 @HasRepoPermissionAnyDecorator(
749 785 'repository.read', 'repository.write', 'repository.admin')
750 786 @view_config(
751 787 route_name='repo_file_download', request_method='GET',
752 788 renderer=None)
753 789 @view_config(
754 790 route_name='repo_file_download:legacy', request_method='GET',
755 791 renderer=None)
756 792 def repo_file_download(self):
757 793 c = self.load_default_context()
758 794
759 795 commit_id, f_path = self._get_commit_and_path()
760 796 commit = self._get_commit_or_redirect(commit_id)
761 797 file_node = self._get_filenode_or_redirect(commit, f_path)
762 798
763 799 if self.request.GET.get('lf'):
764 800 # only if lf get flag is passed, we download this file
765 801 # as LFS/Largefile
766 802 lf_node = file_node.get_largefile_node()
767 803 if lf_node:
768 804 # overwrite our pointer with the REAL large-file
769 805 file_node = lf_node
770 806
771 807 disposition = self._get_attachement_disposition(f_path)
772 808
773 809 def stream_node():
774 810 yield file_node.raw_bytes
775 811
776 812 response = Response(app_iter=stream_node())
777 813 response.content_disposition = disposition
778 814 response.content_type = file_node.mimetype
779 815
780 816 charset = self._get_default_encoding(c)
781 817 if charset:
782 818 response.charset = charset
783 819
784 820 return response
785 821
786 822 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
787 823
788 824 cache_seconds = safe_int(
789 825 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
790 826 cache_on = cache_seconds > 0
791 827 log.debug(
792 828 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
793 829 'with caching: %s[TTL: %ss]' % (
794 830 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
795 831
796 832 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
797 833 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
798 834
799 835 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
800 836 condition=cache_on)
801 837 def compute_file_search(repo_id, commit_id, f_path):
802 838 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
803 839 repo_id, commit_id, f_path)
804 840 try:
805 841 _d, _f = ScmModel().get_nodes(
806 842 repo_name, commit_id, f_path, flat=False)
807 843 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
808 844 log.exception(safe_str(e))
809 845 h.flash(safe_str(h.escape(e)), category='error')
810 846 raise HTTPFound(h.route_path(
811 847 'repo_files', repo_name=self.db_repo_name,
812 848 commit_id='tip', f_path='/'))
813 849 return _d + _f
814 850
815 851 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
816 852
817 853 @LoginRequired()
818 854 @HasRepoPermissionAnyDecorator(
819 855 'repository.read', 'repository.write', 'repository.admin')
820 856 @view_config(
821 857 route_name='repo_files_nodelist', request_method='GET',
822 858 renderer='json_ext', xhr=True)
823 859 def repo_nodelist(self):
824 860 self.load_default_context()
825 861
826 862 commit_id, f_path = self._get_commit_and_path()
827 863 commit = self._get_commit_or_redirect(commit_id)
828 864
829 865 metadata = self._get_nodelist_at_commit(
830 866 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
831 867 return {'nodes': metadata}
832 868
833 869 def _create_references(
834 870 self, branches_or_tags, symbolic_reference, f_path):
835 871 items = []
836 872 for name, commit_id in branches_or_tags.items():
837 873 sym_ref = symbolic_reference(commit_id, name, f_path)
838 874 items.append((sym_ref, name))
839 875 return items
840 876
841 877 def _symbolic_reference(self, commit_id, name, f_path):
842 878 return commit_id
843 879
844 880 def _symbolic_reference_svn(self, commit_id, name, f_path):
845 881 new_f_path = vcspath.join(name, f_path)
846 882 return u'%s@%s' % (new_f_path, commit_id)
847 883
848 884 def _get_node_history(self, commit_obj, f_path, commits=None):
849 885 """
850 886 get commit history for given node
851 887
852 888 :param commit_obj: commit to calculate history
853 889 :param f_path: path for node to calculate history for
854 890 :param commits: if passed don't calculate history and take
855 891 commits defined in this list
856 892 """
857 893 _ = self.request.translate
858 894
859 895 # calculate history based on tip
860 896 tip = self.rhodecode_vcs_repo.get_commit()
861 897 if commits is None:
862 898 pre_load = ["author", "branch"]
863 899 try:
864 900 commits = tip.get_file_history(f_path, pre_load=pre_load)
865 901 except (NodeDoesNotExistError, CommitError):
866 902 # this node is not present at tip!
867 903 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
868 904
869 905 history = []
870 906 commits_group = ([], _("Changesets"))
871 907 for commit in commits:
872 908 branch = ' (%s)' % commit.branch if commit.branch else ''
873 909 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
874 910 commits_group[0].append((commit.raw_id, n_desc,))
875 911 history.append(commits_group)
876 912
877 913 symbolic_reference = self._symbolic_reference
878 914
879 915 if self.rhodecode_vcs_repo.alias == 'svn':
880 916 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
881 917 f_path, self.rhodecode_vcs_repo)
882 918 if adjusted_f_path != f_path:
883 919 log.debug(
884 920 'Recognized svn tag or branch in file "%s", using svn '
885 921 'specific symbolic references', f_path)
886 922 f_path = adjusted_f_path
887 923 symbolic_reference = self._symbolic_reference_svn
888 924
889 925 branches = self._create_references(
890 926 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
891 927 branches_group = (branches, _("Branches"))
892 928
893 929 tags = self._create_references(
894 930 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
895 931 tags_group = (tags, _("Tags"))
896 932
897 933 history.append(branches_group)
898 934 history.append(tags_group)
899 935
900 936 return history, commits
901 937
902 938 @LoginRequired()
903 939 @HasRepoPermissionAnyDecorator(
904 940 'repository.read', 'repository.write', 'repository.admin')
905 941 @view_config(
906 942 route_name='repo_file_history', request_method='GET',
907 943 renderer='json_ext')
908 944 def repo_file_history(self):
909 945 self.load_default_context()
910 946
911 947 commit_id, f_path = self._get_commit_and_path()
912 948 commit = self._get_commit_or_redirect(commit_id)
913 949 file_node = self._get_filenode_or_redirect(commit, f_path)
914 950
915 951 if file_node.is_file():
916 952 file_history, _hist = self._get_node_history(commit, f_path)
917 953
918 954 res = []
919 955 for obj in file_history:
920 956 res.append({
921 957 'text': obj[1],
922 958 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
923 959 })
924 960
925 961 data = {
926 962 'more': False,
927 963 'results': res
928 964 }
929 965 return data
930 966
931 967 log.warning('Cannot fetch history for directory')
932 968 raise HTTPBadRequest()
933 969
934 970 @LoginRequired()
935 971 @HasRepoPermissionAnyDecorator(
936 972 'repository.read', 'repository.write', 'repository.admin')
937 973 @view_config(
938 974 route_name='repo_file_authors', request_method='GET',
939 975 renderer='rhodecode:templates/files/file_authors_box.mako')
940 976 def repo_file_authors(self):
941 977 c = self.load_default_context()
942 978
943 979 commit_id, f_path = self._get_commit_and_path()
944 980 commit = self._get_commit_or_redirect(commit_id)
945 981 file_node = self._get_filenode_or_redirect(commit, f_path)
946 982
947 983 if not file_node.is_file():
948 984 raise HTTPBadRequest()
949 985
950 986 c.file_last_commit = file_node.last_commit
951 987 if self.request.GET.get('annotate') == '1':
952 988 # use _hist from annotation if annotation mode is on
953 989 commit_ids = set(x[1] for x in file_node.annotate)
954 990 _hist = (
955 991 self.rhodecode_vcs_repo.get_commit(commit_id)
956 992 for commit_id in commit_ids)
957 993 else:
958 994 _f_history, _hist = self._get_node_history(commit, f_path)
959 995 c.file_author = False
960 996
961 997 unique = collections.OrderedDict()
962 998 for commit in _hist:
963 999 author = commit.author
964 1000 if author not in unique:
965 1001 unique[commit.author] = [
966 1002 h.email(author),
967 1003 h.person(author, 'username_or_name_or_email'),
968 1004 1 # counter
969 1005 ]
970 1006
971 1007 else:
972 1008 # increase counter
973 1009 unique[commit.author][2] += 1
974 1010
975 1011 c.authors = [val for val in unique.values()]
976 1012
977 1013 return self._get_template_context(c)
978 1014
979 1015 @LoginRequired()
980 1016 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
981 1017 @view_config(
982 1018 route_name='repo_files_remove_file', request_method='GET',
983 1019 renderer='rhodecode:templates/files/files_delete.mako')
984 1020 def repo_files_remove_file(self):
985 1021 _ = self.request.translate
986 1022 c = self.load_default_context()
987 1023 commit_id, f_path = self._get_commit_and_path()
988 1024
989 1025 self._ensure_not_locked()
1026 _branch_name, _sha_commit_id, is_head = \
1027 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
990 1028
991 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1029 if not is_head:
992 1030 h.flash(_('You can only delete files with commit '
993 'being a valid branch '), category='warning')
1031 'being a valid branch head.'), category='warning')
994 1032 raise HTTPFound(
995 1033 h.route_path('repo_files',
996 1034 repo_name=self.db_repo_name, commit_id='tip',
997 1035 f_path=f_path))
998 1036
1037 self.check_branch_permission(_branch_name)
999 1038 c.commit = self._get_commit_or_redirect(commit_id)
1000 1039 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1001 1040
1002 1041 c.default_message = _(
1003 1042 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1004 1043 c.f_path = f_path
1005 1044
1006 1045 return self._get_template_context(c)
1007 1046
1008 1047 @LoginRequired()
1009 1048 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1010 1049 @CSRFRequired()
1011 1050 @view_config(
1012 1051 route_name='repo_files_delete_file', request_method='POST',
1013 1052 renderer=None)
1014 1053 def repo_files_delete_file(self):
1015 1054 _ = self.request.translate
1016 1055
1017 1056 c = self.load_default_context()
1018 1057 commit_id, f_path = self._get_commit_and_path()
1019 1058
1020 1059 self._ensure_not_locked()
1060 _branch_name, _sha_commit_id, is_head = \
1061 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1021 1062
1022 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1063 if not is_head:
1023 1064 h.flash(_('You can only delete files with commit '
1024 'being a valid branch '), category='warning')
1065 'being a valid branch head.'), category='warning')
1025 1066 raise HTTPFound(
1026 1067 h.route_path('repo_files',
1027 1068 repo_name=self.db_repo_name, commit_id='tip',
1028 1069 f_path=f_path))
1070 self.check_branch_permission(_branch_name)
1029 1071
1030 1072 c.commit = self._get_commit_or_redirect(commit_id)
1031 1073 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1032 1074
1033 1075 c.default_message = _(
1034 1076 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1035 1077 c.f_path = f_path
1036 1078 node_path = f_path
1037 1079 author = self._rhodecode_db_user.full_contact
1038 1080 message = self.request.POST.get('message') or c.default_message
1039 1081 try:
1040 1082 nodes = {
1041 1083 node_path: {
1042 1084 'content': ''
1043 1085 }
1044 1086 }
1045 1087 ScmModel().delete_nodes(
1046 1088 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1047 1089 message=message,
1048 1090 nodes=nodes,
1049 1091 parent_commit=c.commit,
1050 1092 author=author,
1051 1093 )
1052 1094
1053 1095 h.flash(
1054 1096 _('Successfully deleted file `{}`').format(
1055 1097 h.escape(f_path)), category='success')
1056 1098 except Exception:
1057 1099 log.exception('Error during commit operation')
1058 1100 h.flash(_('Error occurred during commit'), category='error')
1059 1101 raise HTTPFound(
1060 1102 h.route_path('repo_commit', repo_name=self.db_repo_name,
1061 1103 commit_id='tip'))
1062 1104
1063 1105 @LoginRequired()
1064 1106 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1065 1107 @view_config(
1066 1108 route_name='repo_files_edit_file', request_method='GET',
1067 1109 renderer='rhodecode:templates/files/files_edit.mako')
1068 1110 def repo_files_edit_file(self):
1069 1111 _ = self.request.translate
1070 1112 c = self.load_default_context()
1071 1113 commit_id, f_path = self._get_commit_and_path()
1072 1114
1073 1115 self._ensure_not_locked()
1116 _branch_name, _sha_commit_id, is_head = \
1117 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1074 1118
1075 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1119 if not is_head:
1076 1120 h.flash(_('You can only edit files with commit '
1077 'being a valid branch '), category='warning')
1121 'being a valid branch head.'), category='warning')
1078 1122 raise HTTPFound(
1079 1123 h.route_path('repo_files',
1080 1124 repo_name=self.db_repo_name, commit_id='tip',
1081 1125 f_path=f_path))
1126 self.check_branch_permission(_branch_name)
1082 1127
1083 1128 c.commit = self._get_commit_or_redirect(commit_id)
1084 1129 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1085 1130
1086 1131 if c.file.is_binary:
1087 1132 files_url = h.route_path(
1088 1133 'repo_files',
1089 1134 repo_name=self.db_repo_name,
1090 1135 commit_id=c.commit.raw_id, f_path=f_path)
1091 1136 raise HTTPFound(files_url)
1092 1137
1093 1138 c.default_message = _(
1094 1139 'Edited file {} via RhodeCode Enterprise').format(f_path)
1095 1140 c.f_path = f_path
1096 1141
1097 1142 return self._get_template_context(c)
1098 1143
1099 1144 @LoginRequired()
1100 1145 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1101 1146 @CSRFRequired()
1102 1147 @view_config(
1103 1148 route_name='repo_files_update_file', request_method='POST',
1104 1149 renderer=None)
1105 1150 def repo_files_update_file(self):
1106 1151 _ = self.request.translate
1107 1152 c = self.load_default_context()
1108 1153 commit_id, f_path = self._get_commit_and_path()
1109 1154
1110 1155 self._ensure_not_locked()
1156 _branch_name, _sha_commit_id, is_head = \
1157 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1111 1158
1112 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1159 if not is_head:
1113 1160 h.flash(_('You can only edit files with commit '
1114 'being a valid branch '), category='warning')
1161 'being a valid branch head.'), category='warning')
1115 1162 raise HTTPFound(
1116 1163 h.route_path('repo_files',
1117 1164 repo_name=self.db_repo_name, commit_id='tip',
1118 1165 f_path=f_path))
1119 1166
1167 self.check_branch_permission(_branch_name)
1168
1120 1169 c.commit = self._get_commit_or_redirect(commit_id)
1121 1170 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1122 1171
1123 1172 if c.file.is_binary:
1124 1173 raise HTTPFound(
1125 1174 h.route_path('repo_files',
1126 1175 repo_name=self.db_repo_name,
1127 1176 commit_id=c.commit.raw_id,
1128 1177 f_path=f_path))
1129 1178
1130 1179 c.default_message = _(
1131 1180 'Edited file {} via RhodeCode Enterprise').format(f_path)
1132 1181 c.f_path = f_path
1133 1182 old_content = c.file.content
1134 1183 sl = old_content.splitlines(1)
1135 1184 first_line = sl[0] if sl else ''
1136 1185
1137 1186 r_post = self.request.POST
1138 1187 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1139 1188 mode = detect_mode(first_line, 0)
1140 1189 content = convert_line_endings(r_post.get('content', ''), mode)
1141 1190
1142 1191 message = r_post.get('message') or c.default_message
1143 1192 org_f_path = c.file.unicode_path
1144 1193 filename = r_post['filename']
1145 1194 org_filename = c.file.name
1146 1195
1147 1196 if content == old_content and filename == org_filename:
1148 1197 h.flash(_('No changes'), category='warning')
1149 1198 raise HTTPFound(
1150 1199 h.route_path('repo_commit', repo_name=self.db_repo_name,
1151 1200 commit_id='tip'))
1152 1201 try:
1153 1202 mapping = {
1154 1203 org_f_path: {
1155 1204 'org_filename': org_f_path,
1156 1205 'filename': os.path.join(c.file.dir_path, filename),
1157 1206 'content': content,
1158 1207 'lexer': '',
1159 1208 'op': 'mod',
1160 1209 }
1161 1210 }
1162 1211
1163 1212 ScmModel().update_nodes(
1164 1213 user=self._rhodecode_db_user.user_id,
1165 1214 repo=self.db_repo,
1166 1215 message=message,
1167 1216 nodes=mapping,
1168 1217 parent_commit=c.commit,
1169 1218 )
1170 1219
1171 1220 h.flash(
1172 1221 _('Successfully committed changes to file `{}`').format(
1173 1222 h.escape(f_path)), category='success')
1174 1223 except Exception:
1175 1224 log.exception('Error occurred during commit')
1176 1225 h.flash(_('Error occurred during commit'), category='error')
1177 1226 raise HTTPFound(
1178 1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1179 1228 commit_id='tip'))
1180 1229
1181 1230 @LoginRequired()
1182 1231 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1183 1232 @view_config(
1184 1233 route_name='repo_files_add_file', request_method='GET',
1185 1234 renderer='rhodecode:templates/files/files_add.mako')
1186 1235 def repo_files_add_file(self):
1187 1236 _ = self.request.translate
1188 1237 c = self.load_default_context()
1189 1238 commit_id, f_path = self._get_commit_and_path()
1190 1239
1191 1240 self._ensure_not_locked()
1192 1241
1193 1242 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1194 1243 if c.commit is None:
1195 1244 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1196 1245 c.default_message = (_('Added file via RhodeCode Enterprise'))
1197 1246 c.f_path = f_path.lstrip('/') # ensure not relative path
1198 1247
1248 if self.rhodecode_vcs_repo.is_empty:
1249 # for empty repository we cannot check for current branch, we rely on
1250 # c.commit.branch instead
1251 _branch_name = c.commit.branch
1252 is_head = True
1253 else:
1254 _branch_name, _sha_commit_id, is_head = \
1255 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1256
1257 if not is_head:
1258 h.flash(_('You can only add files with commit '
1259 'being a valid branch head.'), category='warning')
1260 raise HTTPFound(
1261 h.route_path('repo_files',
1262 repo_name=self.db_repo_name, commit_id='tip',
1263 f_path=f_path))
1264
1265 self.check_branch_permission(_branch_name)
1266
1199 1267 return self._get_template_context(c)
1200 1268
1201 1269 @LoginRequired()
1202 1270 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1203 1271 @CSRFRequired()
1204 1272 @view_config(
1205 1273 route_name='repo_files_create_file', request_method='POST',
1206 1274 renderer=None)
1207 1275 def repo_files_create_file(self):
1208 1276 _ = self.request.translate
1209 1277 c = self.load_default_context()
1210 1278 commit_id, f_path = self._get_commit_and_path()
1211 1279
1212 1280 self._ensure_not_locked()
1213 1281
1214 1282 r_post = self.request.POST
1215 1283
1216 1284 c.commit = self._get_commit_or_redirect(
1217 1285 commit_id, redirect_after=False)
1218 1286 if c.commit is None:
1219 1287 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1288
1289 if self.rhodecode_vcs_repo.is_empty:
1290 # for empty repository we cannot check for current branch, we rely on
1291 # c.commit.branch instead
1292 _branch_name = c.commit.branch
1293 is_head = True
1294 else:
1295 _branch_name, _sha_commit_id, is_head = \
1296 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1297
1298 if not is_head:
1299 h.flash(_('You can only add files with commit '
1300 'being a valid branch head.'), category='warning')
1301 raise HTTPFound(
1302 h.route_path('repo_files',
1303 repo_name=self.db_repo_name, commit_id='tip',
1304 f_path=f_path))
1305
1306 self.check_branch_permission(_branch_name)
1307
1220 1308 c.default_message = (_('Added file via RhodeCode Enterprise'))
1221 1309 c.f_path = f_path
1222 1310 unix_mode = 0
1223 1311 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1224 1312
1225 1313 message = r_post.get('message') or c.default_message
1226 1314 filename = r_post.get('filename')
1227 1315 location = r_post.get('location', '') # dir location
1228 1316 file_obj = r_post.get('upload_file', None)
1229 1317
1230 1318 if file_obj is not None and hasattr(file_obj, 'filename'):
1231 1319 filename = r_post.get('filename_upload')
1232 1320 content = file_obj.file
1233 1321
1234 1322 if hasattr(content, 'file'):
1235 1323 # non posix systems store real file under file attr
1236 1324 content = content.file
1237 1325
1238 1326 if self.rhodecode_vcs_repo.is_empty:
1239 1327 default_redirect_url = h.route_path(
1240 1328 'repo_summary', repo_name=self.db_repo_name)
1241 1329 else:
1242 1330 default_redirect_url = h.route_path(
1243 1331 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1244 1332
1245 1333 # If there's no commit, redirect to repo summary
1246 1334 if type(c.commit) is EmptyCommit:
1247 1335 redirect_url = h.route_path(
1248 1336 'repo_summary', repo_name=self.db_repo_name)
1249 1337 else:
1250 1338 redirect_url = default_redirect_url
1251 1339
1252 1340 if not filename:
1253 1341 h.flash(_('No filename'), category='warning')
1254 1342 raise HTTPFound(redirect_url)
1255 1343
1256 1344 # extract the location from filename,
1257 1345 # allows using foo/bar.txt syntax to create subdirectories
1258 1346 subdir_loc = filename.rsplit('/', 1)
1259 1347 if len(subdir_loc) == 2:
1260 1348 location = os.path.join(location, subdir_loc[0])
1261 1349
1262 1350 # strip all crap out of file, just leave the basename
1263 1351 filename = os.path.basename(filename)
1264 1352 node_path = os.path.join(location, filename)
1265 1353 author = self._rhodecode_db_user.full_contact
1266 1354
1267 1355 try:
1268 1356 nodes = {
1269 1357 node_path: {
1270 1358 'content': content
1271 1359 }
1272 1360 }
1273 1361 ScmModel().create_nodes(
1274 1362 user=self._rhodecode_db_user.user_id,
1275 1363 repo=self.db_repo,
1276 1364 message=message,
1277 1365 nodes=nodes,
1278 1366 parent_commit=c.commit,
1279 1367 author=author,
1280 1368 )
1281 1369
1282 1370 h.flash(
1283 1371 _('Successfully committed new file `{}`').format(
1284 1372 h.escape(node_path)), category='success')
1285 1373 except NonRelativePathError:
1286 1374 log.exception('Non Relative path found')
1287 1375 h.flash(_(
1288 1376 'The location specified must be a relative path and must not '
1289 1377 'contain .. in the path'), category='warning')
1290 1378 raise HTTPFound(default_redirect_url)
1291 1379 except (NodeError, NodeAlreadyExistsError) as e:
1292 1380 h.flash(_(h.escape(e)), category='error')
1293 1381 except Exception:
1294 1382 log.exception('Error occurred during commit')
1295 1383 h.flash(_('Error occurred during commit'), category='error')
1296 1384
1297 1385 raise HTTPFound(default_redirect_url)
@@ -1,238 +1,238 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Files Add') % c.repo_name}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="menu_bar_nav()">
11 11 ${self.menu_items(active='repositories')}
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 ${_('Add new file')} @ ${h.show_id(c.commit)}
15 ${_('Add new file')} @ ${h.show_id(c.commit)} ${_('Branch')}: ${c.commit.branch}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_subnav()">
19 19 ${self.repo_menu(active='files')}
20 20 </%def>
21 21
22 22 <%def name="main()">
23 23 <div class="box">
24 24 <div class="title">
25 25 ${self.repo_page_title(c.rhodecode_db_repo)}
26 26 </div>
27 27 <div class="edit-file-title">
28 28 ${self.breadcrumbs()}
29 29 </div>
30 30 ${h.secure_form(h.route_path('repo_files_create_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path), id='eform', enctype="multipart/form-data", class_="form-horizontal", request=request)}
31 31 <div class="edit-file-fieldset">
32 32 <div class="fieldset">
33 33 <div id="destination-label" class="left-label">
34 34 ${_('Path')}:
35 35 </div>
36 36 <div class="right-content">
37 37 <div id="specify-custom-path-container">
38 38 <span id="path-breadcrumbs">${h.files_breadcrumbs(c.repo_name,c.commit.raw_id,c.f_path)}</span>
39 39 <a class="custom-path-link" id="specify-custom-path" href="#">${_('Specify Custom Path')}</a>
40 40 </div>
41 41 <div id="remove-custom-path-container" style="display: none;">
42 42 ${c.repo_name}/
43 43 <input type="input-small" value="${c.f_path}" size="46" name="location" id="location">
44 44 <a class="custom-path-link" id="remove-custom-path" href="#">${_('Remove Custom Path')}</a>
45 45 </div>
46 46 </div>
47 47 </div>
48 48 <div id="filename_container" class="fieldset">
49 49 <div class="filename-label left-label">
50 50 ${_('Filename')}:
51 51 </div>
52 52 <div class="right-content">
53 53 <input class="input-small" type="text" value="" size="46" name="filename" id="filename">
54 54 <p>${_('or')} <a id="upload_file_enable" href="#">${_('Upload File')}</a></p>
55 55 </div>
56 56 </div>
57 57 <div id="upload_file_container" class="fieldset" style="display: none;">
58 58 <div class="filename-label left-label">
59 59 ${_('Filename')}:
60 60 </div>
61 61 <div class="right-content">
62 62 <input class="input-small" type="text" value="" size="46" name="filename_upload" id="filename_upload" placeholder="${_('No file selected')}">
63 63 </div>
64 64 <div class="filename-label left-label file-upload-label">
65 65 ${_('Upload file')}:
66 66 </div>
67 67 <div class="right-content file-upload-input">
68 68 <label for="upload_file" class="btn btn-default">Browse</label>
69 69
70 70 <input type="file" name="upload_file" id="upload_file">
71 71 <p>${_('or')} <a id="file_enable" href="#">${_('Create New File')}</a></p>
72 72 </div>
73 73 </div>
74 74 </div>
75 75 <div class="table">
76 76 <div id="files_data">
77 77 <div id="codeblock" class="codeblock">
78 78 <div class="code-header form" id="set_mode_header">
79 79 <div class="fields">
80 80 ${h.dropdownmenu('set_mode','plain',[('plain',_('plain'))],enable_filter=True)}
81 81 <label for="line_wrap">${_('line wraps')}</label>
82 82 ${h.dropdownmenu('line_wrap', 'off', [('on', _('on')), ('off', _('off')),])}
83 83
84 84 <div id="render_preview" class="btn btn-small preview hidden" >${_('Preview')}</div>
85 85 </div>
86 86 </div>
87 87 <div id="editor_container">
88 88 <pre id="editor_pre"></pre>
89 89 <textarea id="editor" name="content" ></textarea>
90 90 <div id="editor_preview"></div>
91 91 </div>
92 92 </div>
93 93 </div>
94 94 </div>
95 95
96 96 <div class="edit-file-fieldset">
97 97 <div class="fieldset">
98 98 <div id="commit-message-label" class="commit-message-label left-label">
99 99 ${_('Commit Message')}:
100 100 </div>
101 101 <div class="right-content">
102 102 <div class="message">
103 103 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
104 104 </div>
105 105 </div>
106 106 </div>
107 107 <div class="pull-right">
108 108 ${h.reset('reset',_('Cancel'),class_="btn btn-small")}
109 109 ${h.submit('commit_btn',_('Commit changes'),class_="btn btn-small btn-success")}
110 110 </div>
111 111 </div>
112 112 ${h.end_form()}
113 113 </div>
114 114 <script type="text/javascript">
115 115
116 116 $('#commit_btn').on('click', function() {
117 117 var button = $(this);
118 118 if (button.hasClass('clicked')) {
119 119 button.attr('disabled', true);
120 120 } else {
121 121 button.addClass('clicked');
122 122 }
123 123 });
124 124
125 125 $('#specify-custom-path').on('click', function(e){
126 126 e.preventDefault();
127 127 $('#specify-custom-path-container').hide();
128 128 $('#remove-custom-path-container').show();
129 129 $('#destination-label').css('margin-top', '13px');
130 130 });
131 131
132 132 $('#remove-custom-path').on('click', function(e){
133 133 e.preventDefault();
134 134 $('#specify-custom-path-container').show();
135 135 $('#remove-custom-path-container').hide();
136 136 $('#location').val('${c.f_path}');
137 137 $('#destination-label').css('margin-top', '0');
138 138 });
139 139
140 140 var hide_upload = function(){
141 141 $('#files_data').show();
142 142 $('#upload_file_container').hide();
143 143 $('#filename_container').show();
144 144 };
145 145
146 146 $('#file_enable').on('click', function(e){
147 147 e.preventDefault();
148 148 hide_upload();
149 149 });
150 150
151 151 $('#upload_file_enable').on('click', function(e){
152 152 e.preventDefault();
153 153 $('#files_data').hide();
154 154 $('#upload_file_container').show();
155 155 $('#filename_container').hide();
156 156 if (detectIE() && detectIE() <= 9) {
157 157 $('#upload_file_container .file-upload-input label').hide();
158 158 $('#upload_file_container .file-upload-input span').hide();
159 159 $('#upload_file_container .file-upload-input input').show();
160 160 }
161 161 });
162 162
163 163 $('#upload_file').on('change', function() {
164 164 if (this.files && this.files[0]) {
165 165 $('#filename_upload').val(this.files[0].name);
166 166 }
167 167 });
168 168
169 169 hide_upload();
170 170
171 171 var renderer = "";
172 172 var reset_url = "${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}";
173 173 var myCodeMirror = initCodeMirror('editor', reset_url, false);
174 174
175 175 var modes_select = $('#set_mode');
176 176 fillCodeMirrorOptions(modes_select);
177 177
178 178 var filename_selector = '#filename';
179 179 var callback = function(filename, mimetype, mode){
180 180 CodeMirrorPreviewEnable(mode);
181 181 };
182 182 // on change of select field set mode
183 183 setCodeMirrorModeFromSelect(
184 184 modes_select, filename_selector, myCodeMirror, callback);
185 185
186 186 // on entering the new filename set mode, from given extension
187 187 setCodeMirrorModeFromInput(
188 188 modes_select, filename_selector, myCodeMirror, callback);
189 189
190 190 // if the file is renderable set line wraps automatically
191 191 if (renderer !== ""){
192 192 var line_wrap = 'on';
193 193 $($('#line_wrap option[value="'+line_wrap+'"]')[0]).attr("selected", "selected");
194 194 setCodeMirrorLineWrap(myCodeMirror, true);
195 195 }
196 196
197 197 // on select line wraps change the editor
198 198 $('#line_wrap').on('change', function(e){
199 199 var selected = e.currentTarget;
200 200 var line_wraps = {'on': true, 'off': false}[selected.value];
201 201 setCodeMirrorLineWrap(myCodeMirror, line_wraps)
202 202 });
203 203
204 204 // render preview/edit button
205 205 $('#render_preview').on('click', function(e){
206 206 if($(this).hasClass('preview')){
207 207 $(this).removeClass('preview');
208 208 $(this).html("${_('Edit')}");
209 209 $('#editor_preview').show();
210 210 $(myCodeMirror.getWrapperElement()).hide();
211 211
212 212 var possible_renderer = {
213 213 'rst':'rst',
214 214 'markdown':'markdown',
215 215 'gfm': 'markdown'}[myCodeMirror.getMode().name];
216 216 var _text = myCodeMirror.getValue();
217 217 var _renderer = possible_renderer || DEFAULT_RENDERER;
218 218 var post_data = {'text': _text, 'renderer': _renderer, 'csrf_token': CSRF_TOKEN};
219 219 $('#editor_preview').html(_gettext('Loading ...'));
220 220 var url = pyroutes.url('repo_commit_comment_preview',
221 221 {'repo_name': '${c.repo_name}',
222 222 'commit_id': '${c.commit.raw_id}'});
223 223
224 224 ajaxPOST(url, post_data, function(o){
225 225 $('#editor_preview').html(o);
226 226 })
227 227 }
228 228 else{
229 229 $(this).addClass('preview');
230 230 $(this).html("${_('Preview')}");
231 231 $('#editor_preview').hide();
232 232 $(myCodeMirror.getWrapperElement()).show();
233 233 }
234 234 });
235 235 $('#filename').focus();
236 236
237 237 </script>
238 238 </%def>
General Comments 0
You need to be logged in to leave comments. Login now