##// END OF EJS Templates
security: fixed self-xss inside file views.
ergo -
r1810:a79ddada default
parent child Browse files
Show More
@@ -1,1110 +1,1110 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Files controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import itertools
26 26 import logging
27 27 import os
28 28 import shutil
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 35
36 36 from rhodecode.controllers.utils import parse_path_ref
37 37 from rhodecode.lib import diffs, helpers as h, caches
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils import jsonify
42 42 from rhodecode.lib.utils2 import (
43 43 convert_line_endings, detect_mode, safe_str, str2bool)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
46 46 from rhodecode.lib.base import BaseRepoController, render
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.exceptions import (
51 51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 53 NodeDoesNotExistError, CommitError, NodeError)
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55
56 56 from rhodecode.model.repo import RepoModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.db import Repository
59 59
60 60 from rhodecode.controllers.changeset import (
61 61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
62 62 from rhodecode.lib.exceptions import NonRelativePathError
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class FilesController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(FilesController, self).__before__()
71 71 c.cut_off_limit = self.cut_off_limit_file
72 72
73 73 def _get_default_encoding(self):
74 74 enc_list = getattr(c, 'default_encodings', [])
75 75 return enc_list[0] if enc_list else 'UTF-8'
76 76
77 77 def __get_commit_or_redirect(self, commit_id, repo_name,
78 78 redirect_after=True):
79 79 """
80 80 This is a safe way to get commit. If an error occurs it redirects to
81 81 tip with proper message
82 82
83 83 :param commit_id: id of commit to fetch
84 84 :param repo_name: repo name to redirect after
85 85 :param redirect_after: toggle redirection
86 86 """
87 87 try:
88 88 return c.rhodecode_repo.get_commit(commit_id)
89 89 except EmptyRepositoryError:
90 90 if not redirect_after:
91 91 return None
92 92 url_ = url('files_add_home',
93 93 repo_name=c.repo_name,
94 94 revision=0, f_path='', anchor='edit')
95 95 if h.HasRepoPermissionAny(
96 96 'repository.write', 'repository.admin')(c.repo_name):
97 97 add_new = h.link_to(
98 98 _('Click here to add a new file.'),
99 99 url_, class_="alert-link")
100 100 else:
101 101 add_new = ""
102 102 h.flash(h.literal(
103 103 _('There are no files yet. %s') % add_new), category='warning')
104 104 redirect(h.route_path('repo_summary', repo_name=repo_name))
105 105 except (CommitDoesNotExistError, LookupError):
106 106 msg = _('No such commit exists for this repository')
107 107 h.flash(msg, category='error')
108 108 raise HTTPNotFound()
109 109 except RepositoryError as e:
110 110 h.flash(safe_str(e), category='error')
111 111 raise HTTPNotFound()
112 112
113 113 def __get_filenode_or_redirect(self, repo_name, commit, path):
114 114 """
115 115 Returns file_node, if error occurs or given path is directory,
116 116 it'll redirect to top level path
117 117
118 118 :param repo_name: repo_name
119 119 :param commit: given commit
120 120 :param path: path to lookup
121 121 """
122 122 try:
123 123 file_node = commit.get_node(path)
124 124 if file_node.is_dir():
125 125 raise RepositoryError('The given path is a directory')
126 126 except CommitDoesNotExistError:
127 msg = _('No such commit exists for this repository')
128 log.exception(msg)
129 h.flash(msg, category='error')
127 log.exception('No such commit exists for this repository')
128 h.flash(_('No such commit exists for this repository'), category='error')
130 129 raise HTTPNotFound()
131 130 except RepositoryError as e:
132 131 h.flash(safe_str(e), category='error')
133 132 raise HTTPNotFound()
134 133
135 134 return file_node
136 135
137 136 def __get_tree_cache_manager(self, repo_name, namespace_type):
138 137 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
139 138 return caches.get_cache_manager('repo_cache_long', _namespace)
140 139
141 140 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
142 141 full_load=False, force=False):
143 142 def _cached_tree():
144 143 log.debug('Generating cached file tree for %s, %s, %s',
145 144 repo_name, commit_id, f_path)
146 145 c.full_load = full_load
147 146 return render('files/files_browser_tree.mako')
148 147
149 148 cache_manager = self.__get_tree_cache_manager(
150 149 repo_name, caches.FILE_TREE)
151 150
152 151 cache_key = caches.compute_key_from_params(
153 152 repo_name, commit_id, f_path)
154 153
155 154 if force:
156 155 # we want to force recompute of caches
157 156 cache_manager.remove_value(cache_key)
158 157
159 158 return cache_manager.get(cache_key, createfunc=_cached_tree)
160 159
161 160 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
162 161 def _cached_nodes():
163 162 log.debug('Generating cached nodelist for %s, %s, %s',
164 163 repo_name, commit_id, f_path)
165 164 _d, _f = ScmModel().get_nodes(
166 165 repo_name, commit_id, f_path, flat=False)
167 166 return _d + _f
168 167
169 168 cache_manager = self.__get_tree_cache_manager(
170 169 repo_name, caches.FILE_SEARCH_TREE_META)
171 170
172 171 cache_key = caches.compute_key_from_params(
173 172 repo_name, commit_id, f_path)
174 173 return cache_manager.get(cache_key, createfunc=_cached_nodes)
175 174
176 175 @LoginRequired()
177 176 @HasRepoPermissionAnyDecorator(
178 177 'repository.read', 'repository.write', 'repository.admin')
179 178 def index(
180 179 self, repo_name, revision, f_path, annotate=False, rendered=False):
181 180 commit_id = revision
182 181
183 182 # redirect to given commit_id from form if given
184 183 get_commit_id = request.GET.get('at_rev', None)
185 184 if get_commit_id:
186 185 self.__get_commit_or_redirect(get_commit_id, repo_name)
187 186
188 187 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
189 188 c.branch = request.GET.get('branch', None)
190 189 c.f_path = f_path
191 190 c.annotate = annotate
192 191 # default is false, but .rst/.md files later are autorendered, we can
193 192 # overwrite autorendering by setting this GET flag
194 193 c.renderer = rendered or not request.GET.get('no-render', False)
195 194
196 195 # prev link
197 196 try:
198 197 prev_commit = c.commit.prev(c.branch)
199 198 c.prev_commit = prev_commit
200 199 c.url_prev = url('files_home', repo_name=c.repo_name,
201 200 revision=prev_commit.raw_id, f_path=f_path)
202 201 if c.branch:
203 202 c.url_prev += '?branch=%s' % c.branch
204 203 except (CommitDoesNotExistError, VCSError):
205 204 c.url_prev = '#'
206 205 c.prev_commit = EmptyCommit()
207 206
208 207 # next link
209 208 try:
210 209 next_commit = c.commit.next(c.branch)
211 210 c.next_commit = next_commit
212 211 c.url_next = url('files_home', repo_name=c.repo_name,
213 212 revision=next_commit.raw_id, f_path=f_path)
214 213 if c.branch:
215 214 c.url_next += '?branch=%s' % c.branch
216 215 except (CommitDoesNotExistError, VCSError):
217 216 c.url_next = '#'
218 217 c.next_commit = EmptyCommit()
219 218
220 219 # files or dirs
221 220 try:
222 221 c.file = c.commit.get_node(f_path)
223 222 c.file_author = True
224 223 c.file_tree = ''
225 224 if c.file.is_file():
226 225 c.lf_node = c.file.get_largefile_node()
227 226
228 227 c.file_source_page = 'true'
229 228 c.file_last_commit = c.file.last_commit
230 229 if c.file.size < self.cut_off_limit_file:
231 230 if c.annotate: # annotation has precedence over renderer
232 231 c.annotated_lines = filenode_as_annotated_lines_tokens(
233 232 c.file
234 233 )
235 234 else:
236 235 c.renderer = (
237 236 c.renderer and h.renderer_from_filename(c.file.path)
238 237 )
239 238 if not c.renderer:
240 239 c.lines = filenode_as_lines_tokens(c.file)
241 240
242 241 c.on_branch_head = self._is_valid_head(
243 242 commit_id, c.rhodecode_repo)
244 243
245 244 branch = c.commit.branch if (
246 245 c.commit.branch and '/' not in c.commit.branch) else None
247 246 c.branch_or_raw_id = branch or c.commit.raw_id
248 247 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
249 248
250 249 author = c.file_last_commit.author
251 250 c.authors = [(h.email(author),
252 251 h.person(author, 'username_or_name_or_email'))]
253 252 else:
254 253 c.file_source_page = 'false'
255 254 c.authors = []
256 255 c.file_tree = self._get_tree_at_commit(
257 256 repo_name, c.commit.raw_id, f_path)
258 257
259 258 except RepositoryError as e:
260 259 h.flash(safe_str(e), category='error')
261 260 raise HTTPNotFound()
262 261
263 262 if request.environ.get('HTTP_X_PJAX'):
264 263 return render('files/files_pjax.mako')
265 264
266 265 return render('files/files.mako')
267 266
268 267 @LoginRequired()
269 268 @HasRepoPermissionAnyDecorator(
270 269 'repository.read', 'repository.write', 'repository.admin')
271 270 def annotate_previous(self, repo_name, revision, f_path):
272 271
273 272 commit_id = revision
274 273 commit = self.__get_commit_or_redirect(commit_id, repo_name)
275 274 prev_commit_id = commit.raw_id
276 275
277 276 f_path = f_path
278 277 is_file = False
279 278 try:
280 279 _file = commit.get_node(f_path)
281 280 is_file = _file.is_file()
282 281 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
283 282 pass
284 283
285 284 if is_file:
286 285 history = commit.get_file_history(f_path)
287 286 prev_commit_id = history[1].raw_id \
288 287 if len(history) > 1 else prev_commit_id
289 288
290 289 return redirect(h.url(
291 290 'files_annotate_home', repo_name=repo_name,
292 291 revision=prev_commit_id, f_path=f_path))
293 292
294 293 @LoginRequired()
295 294 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 295 'repository.admin')
297 296 @jsonify
298 297 def history(self, repo_name, revision, f_path):
299 298 commit = self.__get_commit_or_redirect(revision, repo_name)
300 299 f_path = f_path
301 300 _file = commit.get_node(f_path)
302 301 if _file.is_file():
303 302 file_history, _hist = self._get_node_history(commit, f_path)
304 303
305 304 res = []
306 305 for obj in file_history:
307 306 res.append({
308 307 'text': obj[1],
309 308 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
310 309 })
311 310
312 311 data = {
313 312 'more': False,
314 313 'results': res
315 314 }
316 315 return data
317 316
318 317 @LoginRequired()
319 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 319 'repository.admin')
321 320 def authors(self, repo_name, revision, f_path):
322 321 commit = self.__get_commit_or_redirect(revision, repo_name)
323 322 file_node = commit.get_node(f_path)
324 323 if file_node.is_file():
325 324 c.file_last_commit = file_node.last_commit
326 325 if request.GET.get('annotate') == '1':
327 326 # use _hist from annotation if annotation mode is on
328 327 commit_ids = set(x[1] for x in file_node.annotate)
329 328 _hist = (
330 329 c.rhodecode_repo.get_commit(commit_id)
331 330 for commit_id in commit_ids)
332 331 else:
333 332 _f_history, _hist = self._get_node_history(commit, f_path)
334 333 c.file_author = False
335 334 c.authors = []
336 335 for author in set(commit.author for commit in _hist):
337 336 c.authors.append((
338 337 h.email(author),
339 338 h.person(author, 'username_or_name_or_email')))
340 339 return render('files/file_authors_box.mako')
341 340
342 341 @LoginRequired()
343 342 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
344 343 'repository.admin')
345 344 def rawfile(self, repo_name, revision, f_path):
346 345 """
347 346 Action for download as raw
348 347 """
349 348 commit = self.__get_commit_or_redirect(revision, repo_name)
350 349 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
351 350
352 351 if request.GET.get('lf'):
353 352 # only if lf get flag is passed, we download this file
354 353 # as LFS/Largefile
355 354 lf_node = file_node.get_largefile_node()
356 355 if lf_node:
357 356 # overwrite our pointer with the REAL large-file
358 357 file_node = lf_node
359 358
360 359 response.content_disposition = 'attachment; filename=%s' % \
361 360 safe_str(f_path.split(Repository.NAME_SEP)[-1])
362 361
363 362 response.content_type = file_node.mimetype
364 363 charset = self._get_default_encoding()
365 364 if charset:
366 365 response.charset = charset
367 366
368 367 return file_node.content
369 368
370 369 @LoginRequired()
371 370 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
372 371 'repository.admin')
373 372 def raw(self, repo_name, revision, f_path):
374 373 """
375 374 Action for show as raw, some mimetypes are "rendered",
376 375 those include images, icons.
377 376 """
378 377 commit = self.__get_commit_or_redirect(revision, repo_name)
379 378 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
380 379
381 380 raw_mimetype_mapping = {
382 381 # map original mimetype to a mimetype used for "show as raw"
383 382 # you can also provide a content-disposition to override the
384 383 # default "attachment" disposition.
385 384 # orig_type: (new_type, new_dispo)
386 385
387 386 # show images inline:
388 387 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
389 388 # for example render an SVG with javascript inside or even render
390 389 # HTML.
391 390 'image/x-icon': ('image/x-icon', 'inline'),
392 391 'image/png': ('image/png', 'inline'),
393 392 'image/gif': ('image/gif', 'inline'),
394 393 'image/jpeg': ('image/jpeg', 'inline'),
395 394 'application/pdf': ('application/pdf', 'inline'),
396 395 }
397 396
398 397 mimetype = file_node.mimetype
399 398 try:
400 399 mimetype, dispo = raw_mimetype_mapping[mimetype]
401 400 except KeyError:
402 401 # we don't know anything special about this, handle it safely
403 402 if file_node.is_binary:
404 403 # do same as download raw for binary files
405 404 mimetype, dispo = 'application/octet-stream', 'attachment'
406 405 else:
407 406 # do not just use the original mimetype, but force text/plain,
408 407 # otherwise it would serve text/html and that might be unsafe.
409 408 # Note: underlying vcs library fakes text/plain mimetype if the
410 409 # mimetype can not be determined and it thinks it is not
411 410 # binary.This might lead to erroneous text display in some
412 411 # cases, but helps in other cases, like with text files
413 412 # without extension.
414 413 mimetype, dispo = 'text/plain', 'inline'
415 414
416 415 if dispo == 'attachment':
417 416 dispo = 'attachment; filename=%s' % safe_str(
418 417 f_path.split(os.sep)[-1])
419 418
420 419 response.content_disposition = dispo
421 420 response.content_type = mimetype
422 421 charset = self._get_default_encoding()
423 422 if charset:
424 423 response.charset = charset
425 424 return file_node.content
426 425
427 426 @CSRFRequired()
428 427 @LoginRequired()
429 428 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
430 429 def delete(self, repo_name, revision, f_path):
431 430 commit_id = revision
432 431
433 432 repo = c.rhodecode_db_repo
434 433 if repo.enable_locking and repo.locked[0]:
435 434 h.flash(_('This repository has been locked by %s on %s')
436 435 % (h.person_by_id(repo.locked[0]),
437 436 h.format_date(h.time_to_datetime(repo.locked[1]))),
438 437 'warning')
439 438 return redirect(h.url('files_home',
440 439 repo_name=repo_name, revision='tip'))
441 440
442 441 if not self._is_valid_head(commit_id, repo.scm_instance()):
443 442 h.flash(_('You can only delete files with revision '
444 443 'being a valid branch '), category='warning')
445 444 return redirect(h.url('files_home',
446 445 repo_name=repo_name, revision='tip',
447 446 f_path=f_path))
448 447
449 448 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
450 449 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
451 450
452 451 c.default_message = _(
453 'Deleted file %s via RhodeCode Enterprise') % (f_path)
452 'Deleted file {} via RhodeCode Enterprise').format(f_path)
454 453 c.f_path = f_path
455 454 node_path = f_path
456 455 author = c.rhodecode_user.full_contact
457 456 message = request.POST.get('message') or c.default_message
458 457 try:
459 458 nodes = {
460 459 node_path: {
461 460 'content': ''
462 461 }
463 462 }
464 463 self.scm_model.delete_nodes(
465 464 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
466 465 message=message,
467 466 nodes=nodes,
468 467 parent_commit=c.commit,
469 468 author=author,
470 469 )
471 470
472 h.flash(_('Successfully deleted file %s') % f_path,
473 category='success')
471 h.flash(
472 _('Successfully deleted file `{}`').format(
473 h.escape(f_path)), category='success')
474 474 except Exception:
475 475 msg = _('Error occurred during commit')
476 476 log.exception(msg)
477 477 h.flash(msg, category='error')
478 478 return redirect(url('changeset_home',
479 479 repo_name=c.repo_name, revision='tip'))
480 480
481 481 @LoginRequired()
482 482 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
483 483 def delete_home(self, repo_name, revision, f_path):
484 484 commit_id = revision
485 485
486 486 repo = c.rhodecode_db_repo
487 487 if repo.enable_locking and repo.locked[0]:
488 488 h.flash(_('This repository has been locked by %s on %s')
489 489 % (h.person_by_id(repo.locked[0]),
490 490 h.format_date(h.time_to_datetime(repo.locked[1]))),
491 491 'warning')
492 492 return redirect(h.url('files_home',
493 493 repo_name=repo_name, revision='tip'))
494 494
495 495 if not self._is_valid_head(commit_id, repo.scm_instance()):
496 496 h.flash(_('You can only delete files with revision '
497 497 'being a valid branch '), category='warning')
498 498 return redirect(h.url('files_home',
499 499 repo_name=repo_name, revision='tip',
500 500 f_path=f_path))
501 501
502 502 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
503 503 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
504 504
505 505 c.default_message = _(
506 'Deleted file %s via RhodeCode Enterprise') % (f_path)
506 'Deleted file {} via RhodeCode Enterprise').format(f_path)
507 507 c.f_path = f_path
508 508
509 509 return render('files/files_delete.mako')
510 510
511 511 @CSRFRequired()
512 512 @LoginRequired()
513 513 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
514 514 def edit(self, repo_name, revision, f_path):
515 515 commit_id = revision
516 516
517 517 repo = c.rhodecode_db_repo
518 518 if repo.enable_locking and repo.locked[0]:
519 519 h.flash(_('This repository has been locked by %s on %s')
520 520 % (h.person_by_id(repo.locked[0]),
521 521 h.format_date(h.time_to_datetime(repo.locked[1]))),
522 522 'warning')
523 523 return redirect(h.url('files_home',
524 524 repo_name=repo_name, revision='tip'))
525 525
526 526 if not self._is_valid_head(commit_id, repo.scm_instance()):
527 527 h.flash(_('You can only edit files with revision '
528 528 'being a valid branch '), category='warning')
529 529 return redirect(h.url('files_home',
530 530 repo_name=repo_name, revision='tip',
531 531 f_path=f_path))
532 532
533 533 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
534 534 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
535 535
536 536 if c.file.is_binary:
537 537 return redirect(url('files_home', repo_name=c.repo_name,
538 538 revision=c.commit.raw_id, f_path=f_path))
539 539 c.default_message = _(
540 'Edited file %s via RhodeCode Enterprise') % (f_path)
540 'Edited file {} via RhodeCode Enterprise').format(f_path)
541 541 c.f_path = f_path
542 542 old_content = c.file.content
543 543 sl = old_content.splitlines(1)
544 544 first_line = sl[0] if sl else ''
545 545
546 546 # modes: 0 - Unix, 1 - Mac, 2 - DOS
547 547 mode = detect_mode(first_line, 0)
548 548 content = convert_line_endings(request.POST.get('content', ''), mode)
549 549
550 550 message = request.POST.get('message') or c.default_message
551 551 org_f_path = c.file.unicode_path
552 552 filename = request.POST['filename']
553 553 org_filename = c.file.name
554 554
555 555 if content == old_content and filename == org_filename:
556 556 h.flash(_('No changes'), category='warning')
557 557 return redirect(url('changeset_home', repo_name=c.repo_name,
558 558 revision='tip'))
559 559 try:
560 560 mapping = {
561 561 org_f_path: {
562 562 'org_filename': org_f_path,
563 563 'filename': os.path.join(c.file.dir_path, filename),
564 564 'content': content,
565 565 'lexer': '',
566 566 'op': 'mod',
567 567 }
568 568 }
569 569
570 570 ScmModel().update_nodes(
571 571 user=c.rhodecode_user.user_id,
572 572 repo=c.rhodecode_db_repo,
573 573 message=message,
574 574 nodes=mapping,
575 575 parent_commit=c.commit,
576 576 )
577 577
578 h.flash(_('Successfully committed to %s') % f_path,
579 category='success')
578 h.flash(
579 _('Successfully committed changes to file `{}`').format(
580 h.escape(f_path)), category='success')
580 581 except Exception:
581 msg = _('Error occurred during commit')
582 log.exception(msg)
583 h.flash(msg, category='error')
582 log.exception('Error occurred during commit')
583 h.flash(_('Error occurred during commit'), category='error')
584 584 return redirect(url('changeset_home',
585 585 repo_name=c.repo_name, revision='tip'))
586 586
587 587 @LoginRequired()
588 588 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
589 589 def edit_home(self, repo_name, revision, f_path):
590 590 commit_id = revision
591 591
592 592 repo = c.rhodecode_db_repo
593 593 if repo.enable_locking and repo.locked[0]:
594 594 h.flash(_('This repository has been locked by %s on %s')
595 595 % (h.person_by_id(repo.locked[0]),
596 596 h.format_date(h.time_to_datetime(repo.locked[1]))),
597 597 'warning')
598 598 return redirect(h.url('files_home',
599 599 repo_name=repo_name, revision='tip'))
600 600
601 601 if not self._is_valid_head(commit_id, repo.scm_instance()):
602 602 h.flash(_('You can only edit files with revision '
603 603 'being a valid branch '), category='warning')
604 604 return redirect(h.url('files_home',
605 605 repo_name=repo_name, revision='tip',
606 606 f_path=f_path))
607 607
608 608 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
609 609 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
610 610
611 611 if c.file.is_binary:
612 612 return redirect(url('files_home', repo_name=c.repo_name,
613 613 revision=c.commit.raw_id, f_path=f_path))
614 614 c.default_message = _(
615 'Edited file %s via RhodeCode Enterprise') % (f_path)
615 'Edited file {} via RhodeCode Enterprise').format(f_path)
616 616 c.f_path = f_path
617 617
618 618 return render('files/files_edit.mako')
619 619
620 620 def _is_valid_head(self, commit_id, repo):
621 621 # check if commit is a branch identifier- basically we cannot
622 622 # create multiple heads via file editing
623 623 valid_heads = repo.branches.keys() + repo.branches.values()
624 624
625 625 if h.is_svn(repo) and not repo.is_empty():
626 626 # Note: Subversion only has one head, we add it here in case there
627 627 # is no branch matched.
628 628 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
629 629
630 630 # check if commit is a branch name or branch hash
631 631 return commit_id in valid_heads
632 632
633 633 @CSRFRequired()
634 634 @LoginRequired()
635 635 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
636 636 def add(self, repo_name, revision, f_path):
637 637 repo = Repository.get_by_repo_name(repo_name)
638 638 if repo.enable_locking and repo.locked[0]:
639 639 h.flash(_('This repository has been locked by %s on %s')
640 640 % (h.person_by_id(repo.locked[0]),
641 641 h.format_date(h.time_to_datetime(repo.locked[1]))),
642 642 'warning')
643 643 return redirect(h.url('files_home',
644 644 repo_name=repo_name, revision='tip'))
645 645
646 646 r_post = request.POST
647 647
648 648 c.commit = self.__get_commit_or_redirect(
649 649 revision, repo_name, redirect_after=False)
650 650 if c.commit is None:
651 651 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
652 652 c.default_message = (_('Added file via RhodeCode Enterprise'))
653 653 c.f_path = f_path
654 654 unix_mode = 0
655 655 content = convert_line_endings(r_post.get('content', ''), unix_mode)
656 656
657 657 message = r_post.get('message') or c.default_message
658 658 filename = r_post.get('filename')
659 659 location = r_post.get('location', '') # dir location
660 660 file_obj = r_post.get('upload_file', None)
661 661
662 662 if file_obj is not None and hasattr(file_obj, 'filename'):
663 663 filename = r_post.get('filename_upload')
664 664 content = file_obj.file
665 665
666 666 if hasattr(content, 'file'):
667 667 # non posix systems store real file under file attr
668 668 content = content.file
669 669
670 670 # If there's no commit, redirect to repo summary
671 671 if type(c.commit) is EmptyCommit:
672 672 redirect_url = h.route_path('repo_summary', repo_name=c.repo_name)
673 673 else:
674 674 redirect_url = url("changeset_home", repo_name=c.repo_name,
675 675 revision='tip')
676 676
677 677 if not filename:
678 678 h.flash(_('No filename'), category='warning')
679 679 return redirect(redirect_url)
680 680
681 681 # extract the location from filename,
682 682 # allows using foo/bar.txt syntax to create subdirectories
683 683 subdir_loc = filename.rsplit('/', 1)
684 684 if len(subdir_loc) == 2:
685 685 location = os.path.join(location, subdir_loc[0])
686 686
687 687 # strip all crap out of file, just leave the basename
688 688 filename = os.path.basename(filename)
689 689 node_path = os.path.join(location, filename)
690 690 author = c.rhodecode_user.full_contact
691 691
692 692 try:
693 693 nodes = {
694 694 node_path: {
695 695 'content': content
696 696 }
697 697 }
698 698 self.scm_model.create_nodes(
699 699 user=c.rhodecode_user.user_id,
700 700 repo=c.rhodecode_db_repo,
701 701 message=message,
702 702 nodes=nodes,
703 703 parent_commit=c.commit,
704 704 author=author,
705 705 )
706 706
707 h.flash(_('Successfully committed to %s') % node_path,
708 category='success')
707 h.flash(
708 _('Successfully committed new file `{}`').format(
709 h.escape(node_path)), category='success')
709 710 except NonRelativePathError as e:
710 711 h.flash(_(
711 712 'The location specified must be a relative path and must not '
712 713 'contain .. in the path'), category='warning')
713 714 return redirect(url('changeset_home', repo_name=c.repo_name,
714 715 revision='tip'))
715 716 except (NodeError, NodeAlreadyExistsError) as e:
716 h.flash(_(e), category='error')
717 h.flash(_(h.escape(e)), category='error')
717 718 except Exception:
718 msg = _('Error occurred during commit')
719 log.exception(msg)
720 h.flash(msg, category='error')
719 log.exception('Error occurred during commit')
720 h.flash(_('Error occurred during commit'), category='error')
721 721 return redirect(url('changeset_home',
722 722 repo_name=c.repo_name, revision='tip'))
723 723
724 724 @LoginRequired()
725 725 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
726 726 def add_home(self, repo_name, revision, f_path):
727 727
728 728 repo = Repository.get_by_repo_name(repo_name)
729 729 if repo.enable_locking and repo.locked[0]:
730 730 h.flash(_('This repository has been locked by %s on %s')
731 731 % (h.person_by_id(repo.locked[0]),
732 732 h.format_date(h.time_to_datetime(repo.locked[1]))),
733 733 'warning')
734 734 return redirect(h.url('files_home',
735 735 repo_name=repo_name, revision='tip'))
736 736
737 737 c.commit = self.__get_commit_or_redirect(
738 738 revision, repo_name, redirect_after=False)
739 739 if c.commit is None:
740 740 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
741 741 c.default_message = (_('Added file via RhodeCode Enterprise'))
742 742 c.f_path = f_path
743 743
744 744 return render('files/files_add.mako')
745 745
746 746 @LoginRequired()
747 747 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 748 'repository.admin')
749 749 def archivefile(self, repo_name, fname):
750 750 fileformat = None
751 751 commit_id = None
752 752 ext = None
753 753 subrepos = request.GET.get('subrepos') == 'true'
754 754
755 755 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
756 756 archive_spec = fname.split(ext_data[1])
757 757 if len(archive_spec) == 2 and archive_spec[1] == '':
758 758 fileformat = a_type or ext_data[1]
759 759 commit_id = archive_spec[0]
760 760 ext = ext_data[1]
761 761
762 762 dbrepo = RepoModel().get_by_repo_name(repo_name)
763 763 if not dbrepo.enable_downloads:
764 764 return _('Downloads disabled')
765 765
766 766 try:
767 767 commit = c.rhodecode_repo.get_commit(commit_id)
768 768 content_type = settings.ARCHIVE_SPECS[fileformat][0]
769 769 except CommitDoesNotExistError:
770 770 return _('Unknown revision %s') % commit_id
771 771 except EmptyRepositoryError:
772 772 return _('Empty repository')
773 773 except KeyError:
774 774 return _('Unknown archive type')
775 775
776 776 # archive cache
777 777 from rhodecode import CONFIG
778 778
779 779 archive_name = '%s-%s%s%s' % (
780 780 safe_str(repo_name.replace('/', '_')),
781 781 '-sub' if subrepos else '',
782 782 safe_str(commit.short_id), ext)
783 783
784 784 use_cached_archive = False
785 785 archive_cache_enabled = CONFIG.get(
786 786 'archive_cache_dir') and not request.GET.get('no_cache')
787 787
788 788 if archive_cache_enabled:
789 789 # check if we it's ok to write
790 790 if not os.path.isdir(CONFIG['archive_cache_dir']):
791 791 os.makedirs(CONFIG['archive_cache_dir'])
792 792 cached_archive_path = os.path.join(
793 793 CONFIG['archive_cache_dir'], archive_name)
794 794 if os.path.isfile(cached_archive_path):
795 795 log.debug('Found cached archive in %s', cached_archive_path)
796 796 fd, archive = None, cached_archive_path
797 797 use_cached_archive = True
798 798 else:
799 799 log.debug('Archive %s is not yet cached', archive_name)
800 800
801 801 if not use_cached_archive:
802 802 # generate new archive
803 803 fd, archive = tempfile.mkstemp()
804 log.debug('Creating new temp archive in %s' % (archive,))
804 log.debug('Creating new temp archive in %s', archive)
805 805 try:
806 806 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
807 807 except ImproperArchiveTypeError:
808 808 return _('Unknown archive type')
809 809 if archive_cache_enabled:
810 810 # if we generated the archive and we have cache enabled
811 811 # let's use this for future
812 log.debug('Storing new archive in %s' % (cached_archive_path,))
812 log.debug('Storing new archive in %s', cached_archive_path)
813 813 shutil.move(archive, cached_archive_path)
814 814 archive = cached_archive_path
815 815
816 816 # store download action
817 817 audit_logger.store_web(
818 818 action='repo.archive.download',
819 819 action_data={'user_agent': request.user_agent,
820 820 'archive_name': archive_name,
821 821 'archive_spec': fname,
822 822 'archive_cached': use_cached_archive},
823 823 user=c.rhodecode_user,
824 824 repo=dbrepo,
825 825 commit=True
826 826 )
827 827
828 828 response.content_disposition = str(
829 829 'attachment; filename=%s' % archive_name)
830 830 response.content_type = str(content_type)
831 831
832 832 def get_chunked_archive(archive):
833 833 with open(archive, 'rb') as stream:
834 834 while True:
835 835 data = stream.read(16 * 1024)
836 836 if not data:
837 837 if fd: # fd means we used temporary file
838 838 os.close(fd)
839 839 if not archive_cache_enabled:
840 840 log.debug('Destroying temp archive %s', archive)
841 841 os.remove(archive)
842 842 break
843 843 yield data
844 844
845 845 return get_chunked_archive(archive)
846 846
847 847 @LoginRequired()
848 848 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
849 849 'repository.admin')
850 850 def diff(self, repo_name, f_path):
851 851
852 852 c.action = request.GET.get('diff')
853 853 diff1 = request.GET.get('diff1', '')
854 854 diff2 = request.GET.get('diff2', '')
855 855
856 856 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
857 857
858 858 ignore_whitespace = str2bool(request.GET.get('ignorews'))
859 859 line_context = request.GET.get('context', 3)
860 860
861 861 if not any((diff1, diff2)):
862 862 h.flash(
863 863 'Need query parameter "diff1" or "diff2" to generate a diff.',
864 864 category='error')
865 865 raise HTTPBadRequest()
866 866
867 867 if c.action not in ['download', 'raw']:
868 868 # redirect to new view if we render diff
869 869 return redirect(
870 870 url('compare_url', repo_name=repo_name,
871 871 source_ref_type='rev',
872 872 source_ref=diff1,
873 873 target_repo=c.repo_name,
874 874 target_ref_type='rev',
875 875 target_ref=diff2,
876 876 f_path=f_path))
877 877
878 878 try:
879 879 node1 = self._get_file_node(diff1, path1)
880 880 node2 = self._get_file_node(diff2, f_path)
881 881 except (RepositoryError, NodeError):
882 882 log.exception("Exception while trying to get node from repository")
883 883 return redirect(url(
884 884 'files_home', repo_name=c.repo_name, f_path=f_path))
885 885
886 886 if all(isinstance(node.commit, EmptyCommit)
887 887 for node in (node1, node2)):
888 888 raise HTTPNotFound
889 889
890 890 c.commit_1 = node1.commit
891 891 c.commit_2 = node2.commit
892 892
893 893 if c.action == 'download':
894 894 _diff = diffs.get_gitdiff(node1, node2,
895 895 ignore_whitespace=ignore_whitespace,
896 896 context=line_context)
897 897 diff = diffs.DiffProcessor(_diff, format='gitdiff')
898 898
899 899 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
900 900 response.content_type = 'text/plain'
901 901 response.content_disposition = (
902 902 'attachment; filename=%s' % (diff_name,)
903 903 )
904 904 charset = self._get_default_encoding()
905 905 if charset:
906 906 response.charset = charset
907 907 return diff.as_raw()
908 908
909 909 elif c.action == 'raw':
910 910 _diff = diffs.get_gitdiff(node1, node2,
911 911 ignore_whitespace=ignore_whitespace,
912 912 context=line_context)
913 913 diff = diffs.DiffProcessor(_diff, format='gitdiff')
914 914 response.content_type = 'text/plain'
915 915 charset = self._get_default_encoding()
916 916 if charset:
917 917 response.charset = charset
918 918 return diff.as_raw()
919 919
920 920 else:
921 921 return redirect(
922 922 url('compare_url', repo_name=repo_name,
923 923 source_ref_type='rev',
924 924 source_ref=diff1,
925 925 target_repo=c.repo_name,
926 926 target_ref_type='rev',
927 927 target_ref=diff2,
928 928 f_path=f_path))
929 929
930 930 @LoginRequired()
931 931 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
932 932 'repository.admin')
933 933 def diff_2way(self, repo_name, f_path):
934 934 """
935 935 Kept only to make OLD links work
936 936 """
937 937 diff1 = request.GET.get('diff1', '')
938 938 diff2 = request.GET.get('diff2', '')
939 939
940 940 if not any((diff1, diff2)):
941 941 h.flash(
942 942 'Need query parameter "diff1" or "diff2" to generate a diff.',
943 943 category='error')
944 944 raise HTTPBadRequest()
945 945
946 946 return redirect(
947 947 url('compare_url', repo_name=repo_name,
948 948 source_ref_type='rev',
949 949 source_ref=diff1,
950 950 target_repo=c.repo_name,
951 951 target_ref_type='rev',
952 952 target_ref=diff2,
953 953 f_path=f_path,
954 954 diffmode='sideside'))
955 955
956 956 def _get_file_node(self, commit_id, f_path):
957 957 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
958 958 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
959 959 try:
960 960 node = commit.get_node(f_path)
961 961 if node.is_dir():
962 962 raise NodeError('%s path is a %s not a file'
963 963 % (node, type(node)))
964 964 except NodeDoesNotExistError:
965 965 commit = EmptyCommit(
966 966 commit_id=commit_id,
967 967 idx=commit.idx,
968 968 repo=commit.repository,
969 969 alias=commit.repository.alias,
970 970 message=commit.message,
971 971 author=commit.author,
972 972 date=commit.date)
973 973 node = FileNode(f_path, '', commit=commit)
974 974 else:
975 975 commit = EmptyCommit(
976 976 repo=c.rhodecode_repo,
977 977 alias=c.rhodecode_repo.alias)
978 978 node = FileNode(f_path, '', commit=commit)
979 979 return node
980 980
981 981 def _get_node_history(self, commit, f_path, commits=None):
982 982 """
983 983 get commit history for given node
984 984
985 985 :param commit: commit to calculate history
986 986 :param f_path: path for node to calculate history for
987 987 :param commits: if passed don't calculate history and take
988 988 commits defined in this list
989 989 """
990 990 # calculate history based on tip
991 991 tip = c.rhodecode_repo.get_commit()
992 992 if commits is None:
993 993 pre_load = ["author", "branch"]
994 994 try:
995 995 commits = tip.get_file_history(f_path, pre_load=pre_load)
996 996 except (NodeDoesNotExistError, CommitError):
997 997 # this node is not present at tip!
998 998 commits = commit.get_file_history(f_path, pre_load=pre_load)
999 999
1000 1000 history = []
1001 1001 commits_group = ([], _("Changesets"))
1002 1002 for commit in commits:
1003 1003 branch = ' (%s)' % commit.branch if commit.branch else ''
1004 1004 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1005 1005 commits_group[0].append((commit.raw_id, n_desc,))
1006 1006 history.append(commits_group)
1007 1007
1008 1008 symbolic_reference = self._symbolic_reference
1009 1009
1010 1010 if c.rhodecode_repo.alias == 'svn':
1011 1011 adjusted_f_path = self._adjust_file_path_for_svn(
1012 1012 f_path, c.rhodecode_repo)
1013 1013 if adjusted_f_path != f_path:
1014 1014 log.debug(
1015 1015 'Recognized svn tag or branch in file "%s", using svn '
1016 1016 'specific symbolic references', f_path)
1017 1017 f_path = adjusted_f_path
1018 1018 symbolic_reference = self._symbolic_reference_svn
1019 1019
1020 1020 branches = self._create_references(
1021 1021 c.rhodecode_repo.branches, symbolic_reference, f_path)
1022 1022 branches_group = (branches, _("Branches"))
1023 1023
1024 1024 tags = self._create_references(
1025 1025 c.rhodecode_repo.tags, symbolic_reference, f_path)
1026 1026 tags_group = (tags, _("Tags"))
1027 1027
1028 1028 history.append(branches_group)
1029 1029 history.append(tags_group)
1030 1030
1031 1031 return history, commits
1032 1032
1033 1033 def _adjust_file_path_for_svn(self, f_path, repo):
1034 1034 """
1035 1035 Computes the relative path of `f_path`.
1036 1036
1037 1037 This is mainly based on prefix matching of the recognized tags and
1038 1038 branches in the underlying repository.
1039 1039 """
1040 1040 tags_and_branches = itertools.chain(
1041 1041 repo.branches.iterkeys(),
1042 1042 repo.tags.iterkeys())
1043 1043 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1044 1044
1045 1045 for name in tags_and_branches:
1046 1046 if f_path.startswith(name + '/'):
1047 1047 f_path = vcspath.relpath(f_path, name)
1048 1048 break
1049 1049 return f_path
1050 1050
1051 1051 def _create_references(
1052 1052 self, branches_or_tags, symbolic_reference, f_path):
1053 1053 items = []
1054 1054 for name, commit_id in branches_or_tags.items():
1055 1055 sym_ref = symbolic_reference(commit_id, name, f_path)
1056 1056 items.append((sym_ref, name))
1057 1057 return items
1058 1058
1059 1059 def _symbolic_reference(self, commit_id, name, f_path):
1060 1060 return commit_id
1061 1061
1062 1062 def _symbolic_reference_svn(self, commit_id, name, f_path):
1063 1063 new_f_path = vcspath.join(name, f_path)
1064 1064 return u'%s@%s' % (new_f_path, commit_id)
1065 1065
1066 1066 @LoginRequired()
1067 1067 @XHRRequired()
1068 1068 @HasRepoPermissionAnyDecorator(
1069 1069 'repository.read', 'repository.write', 'repository.admin')
1070 1070 @jsonify
1071 1071 def nodelist(self, repo_name, revision, f_path):
1072 1072 commit = self.__get_commit_or_redirect(revision, repo_name)
1073 1073
1074 1074 metadata = self._get_nodelist_at_commit(
1075 1075 repo_name, commit.raw_id, f_path)
1076 1076 return {'nodes': metadata}
1077 1077
1078 1078 @LoginRequired()
1079 1079 @XHRRequired()
1080 1080 @HasRepoPermissionAnyDecorator(
1081 1081 'repository.read', 'repository.write', 'repository.admin')
1082 1082 def nodetree_full(self, repo_name, commit_id, f_path):
1083 1083 """
1084 1084 Returns rendered html of file tree that contains commit date,
1085 1085 author, revision for the specified combination of
1086 1086 repo, commit_id and file path
1087 1087
1088 1088 :param repo_name: name of the repository
1089 1089 :param commit_id: commit_id of file tree
1090 1090 :param f_path: file path of the requested directory
1091 1091 """
1092 1092
1093 1093 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1094 1094 try:
1095 1095 dir_node = commit.get_node(f_path)
1096 1096 except RepositoryError as e:
1097 1097 return 'error {}'.format(safe_str(e))
1098 1098
1099 1099 if dir_node.is_file():
1100 1100 return ''
1101 1101
1102 1102 c.file = dir_node
1103 1103 c.commit = commit
1104 1104
1105 1105 # using force=True here, make a little trick. We flush the cache and
1106 1106 # compute it using the same key as without full_load, so the fully
1107 1107 # loaded cached tree is now returned instead of partial
1108 1108 return self._get_tree_at_commit(
1109 1109 repo_name, commit.raw_id, dir_node.path, full_load=True,
1110 1110 force=True)
@@ -1,988 +1,987 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 os
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.controllers.files import FilesController
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.vcs import nodes
31 31
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.tests import (
34 34 url, assert_session_flash, assert_not_in_session_flash)
35 35 from rhodecode.tests.fixture import Fixture
36 36
37 37 fixture = Fixture()
38 38
39 39 NODE_HISTORY = {
40 40 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
41 41 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
42 42 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
43 43 }
44 44
45 45
46 46
47 47 @pytest.mark.usefixtures("app")
48 48 class TestFilesController:
49 49
50 50 def test_index(self, backend):
51 51 response = self.app.get(url(
52 52 controller='files', action='index',
53 53 repo_name=backend.repo_name, revision='tip', f_path='/'))
54 54 commit = backend.repo.get_commit()
55 55
56 56 params = {
57 57 'repo_name': backend.repo_name,
58 58 'commit_id': commit.raw_id,
59 59 'date': commit.date
60 60 }
61 61 assert_dirs_in_response(response, ['docs', 'vcs'], params)
62 62 files = [
63 63 '.gitignore',
64 64 '.hgignore',
65 65 '.hgtags',
66 66 # TODO: missing in Git
67 67 # '.travis.yml',
68 68 'MANIFEST.in',
69 69 'README.rst',
70 70 # TODO: File is missing in svn repository
71 71 # 'run_test_and_report.sh',
72 72 'setup.cfg',
73 73 'setup.py',
74 74 'test_and_report.sh',
75 75 'tox.ini',
76 76 ]
77 77 assert_files_in_response(response, files, params)
78 78 assert_timeago_in_response(response, files, params)
79 79
80 80 def test_index_links_submodules_with_absolute_url(self, backend_hg):
81 81 repo = backend_hg['subrepos']
82 82 response = self.app.get(url(
83 83 controller='files', action='index',
84 84 repo_name=repo.repo_name, revision='tip', f_path='/'))
85 85 assert_response = response.assert_response()
86 86 assert_response.contains_one_link(
87 87 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
88 88
89 89 def test_index_links_submodules_with_absolute_url_subpaths(
90 90 self, backend_hg):
91 91 repo = backend_hg['subrepos']
92 92 response = self.app.get(url(
93 93 controller='files', action='index',
94 94 repo_name=repo.repo_name, revision='tip', f_path='/'))
95 95 assert_response = response.assert_response()
96 96 assert_response.contains_one_link(
97 97 'subpaths-path @ 000000000000',
98 98 'http://sub-base.example.com/subpaths-path')
99 99
100 100 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
101 101 def test_files_menu(self, backend):
102 102 new_branch = "temp_branch_name"
103 103 commits = [
104 104 {'message': 'a'},
105 105 {'message': 'b', 'branch': new_branch}
106 106 ]
107 107 backend.create_repo(commits)
108 108
109 109 backend.repo.landing_rev = "branch:%s" % new_branch
110 110
111 111 # get response based on tip and not new revision
112 112 response = self.app.get(url(
113 113 controller='files', action='index',
114 114 repo_name=backend.repo_name, revision='tip', f_path='/'),
115 115 status=200)
116 116
117 117 # make sure Files menu url is not tip but new revision
118 118 landing_rev = backend.repo.landing_rev[1]
119 119 files_url = url('files_home', repo_name=backend.repo_name,
120 120 revision=landing_rev)
121 121
122 122 assert landing_rev != 'tip'
123 123 response.mustcontain('<li class="active"><a class="menulink" href="%s">' % files_url)
124 124
125 125 def test_index_commit(self, backend):
126 126 commit = backend.repo.get_commit(commit_idx=32)
127 127
128 128 response = self.app.get(url(
129 129 controller='files', action='index',
130 130 repo_name=backend.repo_name,
131 131 revision=commit.raw_id,
132 132 f_path='/')
133 133 )
134 134
135 135 dirs = ['docs', 'tests']
136 136 files = ['README.rst']
137 137 params = {
138 138 'repo_name': backend.repo_name,
139 139 'commit_id': commit.raw_id,
140 140 }
141 141 assert_dirs_in_response(response, dirs, params)
142 142 assert_files_in_response(response, files, params)
143 143
144 144 def test_index_different_branch(self, backend):
145 145 branches = dict(
146 146 hg=(150, ['git']),
147 147 # TODO: Git test repository does not contain other branches
148 148 git=(633, ['master']),
149 149 # TODO: Branch support in Subversion
150 150 svn=(150, [])
151 151 )
152 152 idx, branches = branches[backend.alias]
153 153 commit = backend.repo.get_commit(commit_idx=idx)
154 154 response = self.app.get(url(
155 155 controller='files', action='index',
156 156 repo_name=backend.repo_name,
157 157 revision=commit.raw_id,
158 158 f_path='/'))
159 159 assert_response = response.assert_response()
160 160 for branch in branches:
161 161 assert_response.element_contains('.tags .branchtag', branch)
162 162
163 163 def test_index_paging(self, backend):
164 164 repo = backend.repo
165 165 indexes = [73, 92, 109, 1, 0]
166 166 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
167 167 for rev in indexes]
168 168
169 169 for idx in idx_map:
170 170 response = self.app.get(url(
171 171 controller='files', action='index',
172 172 repo_name=backend.repo_name,
173 173 revision=idx[1],
174 174 f_path='/'))
175 175
176 176 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
177 177
178 178 def test_file_source(self, backend):
179 179 commit = backend.repo.get_commit(commit_idx=167)
180 180 response = self.app.get(url(
181 181 controller='files', action='index',
182 182 repo_name=backend.repo_name,
183 183 revision=commit.raw_id,
184 184 f_path='vcs/nodes.py'))
185 185
186 186 msgbox = """<div class="commit right-content">%s</div>"""
187 187 response.mustcontain(msgbox % (commit.message, ))
188 188
189 189 assert_response = response.assert_response()
190 190 if commit.branch:
191 191 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
192 192 if commit.tags:
193 193 for tag in commit.tags:
194 194 assert_response.element_contains('.tags.tags-main .tagtag', tag)
195 195
196 196 def test_file_source_history(self, backend):
197 197 response = self.app.get(
198 198 url(
199 199 controller='files', action='history',
200 200 repo_name=backend.repo_name,
201 201 revision='tip',
202 202 f_path='vcs/nodes.py'),
203 203 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
204 204 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
205 205
206 206 def test_file_source_history_svn(self, backend_svn):
207 207 simple_repo = backend_svn['svn-simple-layout']
208 208 response = self.app.get(
209 209 url(
210 210 controller='files', action='history',
211 211 repo_name=simple_repo.repo_name,
212 212 revision='tip',
213 213 f_path='trunk/example.py'),
214 214 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
215 215
216 216 expected_data = json.loads(
217 217 fixture.load_resource('svn_node_history_branches.json'))
218 218 assert expected_data == response.json
219 219
220 220 def test_file_annotation_history(self, backend):
221 221 response = self.app.get(
222 222 url(
223 223 controller='files', action='history',
224 224 repo_name=backend.repo_name,
225 225 revision='tip',
226 226 f_path='vcs/nodes.py',
227 227 annotate=True),
228 228 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
229 229 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
230 230
231 231 def test_file_annotation(self, backend):
232 232 response = self.app.get(url(
233 233 controller='files', action='index',
234 234 repo_name=backend.repo_name, revision='tip', f_path='vcs/nodes.py',
235 235 annotate=True))
236 236
237 237 expected_revisions = {
238 238 'hg': 'r356',
239 239 'git': 'r345',
240 240 'svn': 'r208',
241 241 }
242 242 response.mustcontain(expected_revisions[backend.alias])
243 243
244 244 def test_file_authors(self, backend):
245 245 response = self.app.get(url(
246 246 controller='files', action='authors',
247 247 repo_name=backend.repo_name,
248 248 revision='tip',
249 249 f_path='vcs/nodes.py',
250 250 annotate=True))
251 251
252 252 expected_authors = {
253 253 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
254 254 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
255 255 'svn': ('marcin', 'lukasz'),
256 256 }
257 257
258 258 for author in expected_authors[backend.alias]:
259 259 response.mustcontain(author)
260 260
261 261 def test_tree_search_top_level(self, backend, xhr_header):
262 262 commit = backend.repo.get_commit(commit_idx=173)
263 263 response = self.app.get(
264 264 url('files_nodelist_home', repo_name=backend.repo_name,
265 265 revision=commit.raw_id, f_path='/'),
266 266 extra_environ=xhr_header)
267 267 assert 'nodes' in response.json
268 268 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
269 269
270 270 def test_tree_search_at_path(self, backend, xhr_header):
271 271 commit = backend.repo.get_commit(commit_idx=173)
272 272 response = self.app.get(
273 273 url('files_nodelist_home', repo_name=backend.repo_name,
274 274 revision=commit.raw_id, f_path='/docs'),
275 275 extra_environ=xhr_header)
276 276 assert 'nodes' in response.json
277 277 nodes = response.json['nodes']
278 278 assert {'name': 'docs/api', 'type': 'dir'} in nodes
279 279 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
280 280
281 281 def test_tree_search_at_path_missing_xhr(self, backend):
282 282 self.app.get(
283 283 url('files_nodelist_home', repo_name=backend.repo_name,
284 284 revision='tip', f_path=''), status=400)
285 285
286 286 def test_tree_view_list(self, backend, xhr_header):
287 287 commit = backend.repo.get_commit(commit_idx=173)
288 288 response = self.app.get(
289 289 url('files_nodelist_home', repo_name=backend.repo_name,
290 290 f_path='/', revision=commit.raw_id),
291 291 extra_environ=xhr_header,
292 292 )
293 293 response.mustcontain("vcs/web/simplevcs/views/repository.py")
294 294
295 295 def test_tree_view_list_at_path(self, backend, xhr_header):
296 296 commit = backend.repo.get_commit(commit_idx=173)
297 297 response = self.app.get(
298 298 url('files_nodelist_home', repo_name=backend.repo_name,
299 299 f_path='/docs', revision=commit.raw_id),
300 300 extra_environ=xhr_header,
301 301 )
302 302 response.mustcontain("docs/index.rst")
303 303
304 304 def test_tree_view_list_missing_xhr(self, backend):
305 305 self.app.get(
306 306 url('files_nodelist_home', repo_name=backend.repo_name,
307 307 f_path='/', revision='tip'), status=400)
308 308
309 309 def test_nodetree_full_success(self, backend, xhr_header):
310 310 commit = backend.repo.get_commit(commit_idx=173)
311 311 response = self.app.get(
312 312 url('files_nodetree_full', repo_name=backend.repo_name,
313 313 f_path='/', commit_id=commit.raw_id),
314 314 extra_environ=xhr_header)
315 315
316 316 assert_response = response.assert_response()
317 317
318 318 for attr in ['data-commit-id', 'data-date', 'data-author']:
319 319 elements = assert_response.get_elements('[{}]'.format(attr))
320 320 assert len(elements) > 1
321 321
322 322 for element in elements:
323 323 assert element.get(attr)
324 324
325 325 def test_nodetree_full_if_file(self, backend, xhr_header):
326 326 commit = backend.repo.get_commit(commit_idx=173)
327 327 response = self.app.get(
328 328 url('files_nodetree_full', repo_name=backend.repo_name,
329 329 f_path='README.rst', commit_id=commit.raw_id),
330 330 extra_environ=xhr_header)
331 331 assert response.body == ''
332 332
333 333 def test_tree_metadata_list_missing_xhr(self, backend):
334 334 self.app.get(
335 335 url('files_nodetree_full', repo_name=backend.repo_name,
336 336 f_path='/', commit_id='tip'), status=400)
337 337
338 338 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
339 339 self, app, backend_stub, autologin_regular_user, user_regular,
340 340 user_util):
341 341 repo = backend_stub.create_repo()
342 342 user_util.grant_user_permission_to_repo(
343 343 repo, user_regular, 'repository.write')
344 344 response = self.app.get(url(
345 345 controller='files', action='index',
346 346 repo_name=repo.repo_name, revision='tip', f_path='/'))
347 347 assert_session_flash(
348 348 response,
349 349 'There are no files yet. <a class="alert-link" '
350 350 'href="/%s/add/0/#edit">Click here to add a new file.</a>'
351 351 % (repo.repo_name))
352 352
353 353 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
354 354 self, backend_stub, user_util):
355 355 repo = backend_stub.create_repo()
356 356 repo_file_url = url(
357 357 'files_add_home',
358 358 repo_name=repo.repo_name,
359 359 revision=0, f_path='', anchor='edit')
360 360 response = self.app.get(url(
361 361 controller='files', action='index',
362 362 repo_name=repo.repo_name, revision='tip', f_path='/'))
363 363 assert_not_in_session_flash(response, repo_file_url)
364 364
365 365
366 366 # TODO: johbo: Think about a better place for these tests. Either controller
367 367 # specific unit tests or we move down the whole logic further towards the vcs
368 368 # layer
369 369 class TestAdjustFilePathForSvn(object):
370 370 """SVN specific adjustments of node history in FileController."""
371 371
372 372 def test_returns_path_relative_to_matched_reference(self):
373 373 repo = self._repo(branches=['trunk'])
374 374 self.assert_file_adjustment('trunk/file', 'file', repo)
375 375
376 376 def test_does_not_modify_file_if_no_reference_matches(self):
377 377 repo = self._repo(branches=['trunk'])
378 378 self.assert_file_adjustment('notes/file', 'notes/file', repo)
379 379
380 380 def test_does_not_adjust_partial_directory_names(self):
381 381 repo = self._repo(branches=['trun'])
382 382 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
383 383
384 384 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
385 385 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
386 386 self.assert_file_adjustment('trunk/new/file', 'file', repo)
387 387
388 388 def assert_file_adjustment(self, f_path, expected, repo):
389 389 controller = FilesController()
390 390 result = controller._adjust_file_path_for_svn(f_path, repo)
391 391 assert result == expected
392 392
393 393 def _repo(self, branches=None):
394 394 repo = mock.Mock()
395 395 repo.branches = OrderedDict((name, '0') for name in branches or [])
396 396 repo.tags = {}
397 397 return repo
398 398
399 399
400 400 @pytest.mark.usefixtures("app")
401 401 class TestRepositoryArchival(object):
402 402
403 403 def test_archival(self, backend):
404 404 backend.enable_downloads()
405 405 commit = backend.repo.get_commit(commit_idx=173)
406 406 for archive, info in settings.ARCHIVE_SPECS.items():
407 407 mime_type, arch_ext = info
408 408 short = commit.short_id + arch_ext
409 409 fname = commit.raw_id + arch_ext
410 410 filename = '%s-%s' % (backend.repo_name, short)
411 411 response = self.app.get(url(controller='files',
412 412 action='archivefile',
413 413 repo_name=backend.repo_name,
414 414 fname=fname))
415 415
416 416 assert response.status == '200 OK'
417 417 headers = {
418 418 'Pragma': 'no-cache',
419 419 'Cache-Control': 'no-cache',
420 420 'Content-Disposition': 'attachment; filename=%s' % filename,
421 421 'Content-Type': '%s; charset=utf-8' % mime_type,
422 422 }
423 423 if 'Set-Cookie' in response.response.headers:
424 424 del response.response.headers['Set-Cookie']
425 425 assert response.response.headers == headers
426 426
427 427 def test_archival_wrong_ext(self, backend):
428 428 backend.enable_downloads()
429 429 commit = backend.repo.get_commit(commit_idx=173)
430 430 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
431 431 fname = commit.raw_id + arch_ext
432 432
433 433 response = self.app.get(url(controller='files',
434 434 action='archivefile',
435 435 repo_name=backend.repo_name,
436 436 fname=fname))
437 437 response.mustcontain('Unknown archive type')
438 438
439 439 def test_archival_wrong_commit_id(self, backend):
440 440 backend.enable_downloads()
441 441 for commit_id in ['00x000000', 'tar', 'wrong', '@##$@$42413232',
442 442 '232dffcd']:
443 443 fname = '%s.zip' % commit_id
444 444
445 445 response = self.app.get(url(controller='files',
446 446 action='archivefile',
447 447 repo_name=backend.repo_name,
448 448 fname=fname))
449 449 response.mustcontain('Unknown revision')
450 450
451 451
452 452 @pytest.mark.usefixtures("app", "autologin_user")
453 453 class TestRawFileHandling(object):
454 454
455 455 def test_raw_file_ok(self, backend):
456 456 commit = backend.repo.get_commit(commit_idx=173)
457 457 response = self.app.get(url(controller='files', action='rawfile',
458 458 repo_name=backend.repo_name,
459 459 revision=commit.raw_id,
460 460 f_path='vcs/nodes.py'))
461 461
462 462 assert response.content_disposition == "attachment; filename=nodes.py"
463 463 assert response.content_type == "text/x-python"
464 464
465 465 def test_raw_file_wrong_cs(self, backend):
466 466 commit_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
467 467 f_path = 'vcs/nodes.py'
468 468
469 469 response = self.app.get(url(controller='files', action='rawfile',
470 470 repo_name=backend.repo_name,
471 471 revision=commit_id,
472 472 f_path=f_path), status=404)
473 473
474 474 msg = """No such commit exists for this repository"""
475 475 response.mustcontain(msg)
476 476
477 477 def test_raw_file_wrong_f_path(self, backend):
478 478 commit = backend.repo.get_commit(commit_idx=173)
479 479 f_path = 'vcs/ERRORnodes.py'
480 480 response = self.app.get(url(controller='files', action='rawfile',
481 481 repo_name=backend.repo_name,
482 482 revision=commit.raw_id,
483 483 f_path=f_path), status=404)
484 484
485 485 msg = (
486 486 "There is no file nor directory at the given path: "
487 487 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
488 488 response.mustcontain(msg)
489 489
490 490 def test_raw_ok(self, backend):
491 491 commit = backend.repo.get_commit(commit_idx=173)
492 492 response = self.app.get(url(controller='files', action='raw',
493 493 repo_name=backend.repo_name,
494 494 revision=commit.raw_id,
495 495 f_path='vcs/nodes.py'))
496 496
497 497 assert response.content_type == "text/plain"
498 498
499 499 def test_raw_wrong_cs(self, backend):
500 500 commit_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
501 501 f_path = 'vcs/nodes.py'
502 502
503 503 response = self.app.get(url(controller='files', action='raw',
504 504 repo_name=backend.repo_name,
505 505 revision=commit_id,
506 506 f_path=f_path), status=404)
507 507
508 508 msg = """No such commit exists for this repository"""
509 509 response.mustcontain(msg)
510 510
511 511 def test_raw_wrong_f_path(self, backend):
512 512 commit = backend.repo.get_commit(commit_idx=173)
513 513 f_path = 'vcs/ERRORnodes.py'
514 514 response = self.app.get(url(controller='files', action='raw',
515 515 repo_name=backend.repo_name,
516 516 revision=commit.raw_id,
517 517 f_path=f_path), status=404)
518 518 msg = (
519 519 "There is no file nor directory at the given path: "
520 520 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
521 521 response.mustcontain(msg)
522 522
523 523 def test_raw_svg_should_not_be_rendered(self, backend):
524 524 backend.create_repo()
525 525 backend.ensure_file("xss.svg")
526 526 response = self.app.get(url(controller='files', action='raw',
527 527 repo_name=backend.repo_name,
528 528 revision='tip',
529 529 f_path='xss.svg'))
530 530
531 531 # If the content type is image/svg+xml then it allows to render HTML
532 532 # and malicious SVG.
533 533 assert response.content_type == "text/plain"
534 534
535 535
536 536 @pytest.mark.usefixtures("app")
537 537 class TestFilesDiff:
538 538
539 539 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
540 540 def test_file_full_diff(self, backend, diff):
541 541 commit1 = backend.repo.get_commit(commit_idx=-1)
542 542 commit2 = backend.repo.get_commit(commit_idx=-2)
543 543
544 544 response = self.app.get(
545 545 url(
546 546 controller='files',
547 547 action='diff',
548 548 repo_name=backend.repo_name,
549 549 f_path='README'),
550 550 params={
551 551 'diff1': commit2.raw_id,
552 552 'diff2': commit1.raw_id,
553 553 'fulldiff': '1',
554 554 'diff': diff,
555 555 })
556 556
557 557 if diff == 'diff':
558 558 # use redirect since this is OLD view redirecting to compare page
559 559 response = response.follow()
560 560
561 561 # It's a symlink to README.rst
562 562 response.mustcontain('README.rst')
563 563 response.mustcontain('No newline at end of file')
564 564
565 565 def test_file_binary_diff(self, backend):
566 566 commits = [
567 567 {'message': 'First commit'},
568 568 {'message': 'Commit with binary',
569 569 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
570 570 ]
571 571 repo = backend.create_repo(commits=commits)
572 572
573 573 response = self.app.get(
574 574 url(
575 575 controller='files',
576 576 action='diff',
577 577 repo_name=backend.repo_name,
578 578 f_path='file.bin'),
579 579 params={
580 580 'diff1': repo.get_commit(commit_idx=0).raw_id,
581 581 'diff2': repo.get_commit(commit_idx=1).raw_id,
582 582 'fulldiff': '1',
583 583 'diff': 'diff',
584 584 })
585 585 # use redirect since this is OLD view redirecting to compare page
586 586 response = response.follow()
587 587 response.mustcontain('Expand 1 commit')
588 588 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
589 589
590 590 if backend.alias == 'svn':
591 591 response.mustcontain('new file 10644')
592 592 # TODO(marcink): SVN doesn't yet detect binary changes
593 593 else:
594 594 response.mustcontain('new file 100644')
595 595 response.mustcontain('binary diff hidden')
596 596
597 597 def test_diff_2way(self, backend):
598 598 commit1 = backend.repo.get_commit(commit_idx=-1)
599 599 commit2 = backend.repo.get_commit(commit_idx=-2)
600 600 response = self.app.get(
601 601 url(
602 602 controller='files',
603 603 action='diff_2way',
604 604 repo_name=backend.repo_name,
605 605 f_path='README'),
606 606 params={
607 607 'diff1': commit2.raw_id,
608 608 'diff2': commit1.raw_id,
609 609 })
610 610 # use redirect since this is OLD view redirecting to compare page
611 611 response = response.follow()
612 612
613 613 # It's a symlink to README.rst
614 614 response.mustcontain('README.rst')
615 615 response.mustcontain('No newline at end of file')
616 616
617 617 def test_requires_one_commit_id(self, backend, autologin_user):
618 618 response = self.app.get(
619 619 url(
620 620 controller='files',
621 621 action='diff',
622 622 repo_name=backend.repo_name,
623 623 f_path='README.rst'),
624 624 status=400)
625 625 response.mustcontain(
626 626 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
627 627
628 628 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
629 629 repo = vcsbackend.repo
630 630 response = self.app.get(
631 631 url(
632 632 controller='files',
633 633 action='diff',
634 634 repo_name=repo.name,
635 635 f_path='does-not-exist-in-any-commit',
636 636 diff1=repo[0].raw_id,
637 637 diff2=repo[1].raw_id),)
638 638
639 639 response = response.follow()
640 640 response.mustcontain('No files')
641 641
642 642 def test_returns_redirect_if_file_not_changed(self, backend):
643 643 commit = backend.repo.get_commit(commit_idx=-1)
644 644 f_path = 'README'
645 645 response = self.app.get(
646 646 url(
647 647 controller='files',
648 648 action='diff_2way',
649 649 repo_name=backend.repo_name,
650 650 f_path=f_path,
651 651 diff1=commit.raw_id,
652 652 diff2=commit.raw_id,
653 653 ),
654 654 )
655 655 response = response.follow()
656 656 response.mustcontain('No files')
657 657 response.mustcontain('No commits in this compare')
658 658
659 659 def test_supports_diff_to_different_path_svn(self, backend_svn):
660 660 #TODO: check this case
661 661 return
662 662
663 663 repo = backend_svn['svn-simple-layout'].scm_instance()
664 664 commit_id_1 = '24'
665 665 commit_id_2 = '26'
666 666
667 667
668 668 print( url(
669 669 controller='files',
670 670 action='diff',
671 671 repo_name=repo.name,
672 672 f_path='trunk/example.py',
673 673 diff1='tags/v0.2/example.py@' + commit_id_1,
674 674 diff2=commit_id_2))
675 675
676 676 response = self.app.get(
677 677 url(
678 678 controller='files',
679 679 action='diff',
680 680 repo_name=repo.name,
681 681 f_path='trunk/example.py',
682 682 diff1='tags/v0.2/example.py@' + commit_id_1,
683 683 diff2=commit_id_2))
684 684
685 685 response = response.follow()
686 686 response.mustcontain(
687 687 # diff contains this
688 688 "Will print out a useful message on invocation.")
689 689
690 690 # Note: Expecting that we indicate the user what's being compared
691 691 response.mustcontain("trunk/example.py")
692 692 response.mustcontain("tags/v0.2/example.py")
693 693
694 694 def test_show_rev_redirects_to_svn_path(self, backend_svn):
695 695 #TODO: check this case
696 696 return
697 697
698 698 repo = backend_svn['svn-simple-layout'].scm_instance()
699 699 commit_id = repo[-1].raw_id
700 700 response = self.app.get(
701 701 url(
702 702 controller='files',
703 703 action='diff',
704 704 repo_name=repo.name,
705 705 f_path='trunk/example.py',
706 706 diff1='branches/argparse/example.py@' + commit_id,
707 707 diff2=commit_id),
708 708 params={'show_rev': 'Show at Revision'},
709 709 status=302)
710 710 assert response.headers['Location'].endswith(
711 711 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
712 712
713 713 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
714 714 #TODO: check this case
715 715 return
716 716
717 717 repo = backend_svn['svn-simple-layout'].scm_instance()
718 718 commit_id = repo[-1].raw_id
719 719 response = self.app.get(
720 720 url(
721 721 controller='files',
722 722 action='diff',
723 723 repo_name=repo.name,
724 724 f_path='trunk/example.py',
725 725 diff1='branches/argparse/example.py@' + commit_id,
726 726 diff2=commit_id),
727 727 params={
728 728 'show_rev': 'Show at Revision',
729 729 'annotate': 'true',
730 730 },
731 731 status=302)
732 732 assert response.headers['Location'].endswith(
733 733 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
734 734
735 735
736 736 @pytest.mark.usefixtures("app", "autologin_user")
737 737 class TestChangingFiles:
738 738
739 739 def test_add_file_view(self, backend):
740 740 self.app.get(url(
741 741 'files_add_home',
742 742 repo_name=backend.repo_name,
743 743 revision='tip', f_path='/'))
744 744
745 745 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
746 746 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
747 747 repo = backend.create_repo()
748 748 filename = 'init.py'
749 749 response = self.app.post(
750 750 url(
751 751 'files_add',
752 752 repo_name=repo.repo_name,
753 753 revision='tip', f_path='/'),
754 754 params={
755 755 'content': "",
756 756 'filename': filename,
757 757 'location': "",
758 758 'csrf_token': csrf_token,
759 759 },
760 760 status=302)
761 assert_session_flash(
762 response, 'Successfully committed to %s'
763 % os.path.join(filename))
761 assert_session_flash(response,
762 'Successfully committed new file `{}`'.format(os.path.join(filename)))
764 763
765 764 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
766 765 response = self.app.post(
767 766 url(
768 767 'files_add',
769 768 repo_name=backend.repo_name,
770 769 revision='tip', f_path='/'),
771 770 params={
772 771 'content': "foo",
773 772 'csrf_token': csrf_token,
774 773 },
775 774 status=302)
776 775
777 776 assert_session_flash(response, 'No filename')
778 777
779 778 def test_add_file_into_repo_errors_and_no_commits(
780 779 self, backend, csrf_token):
781 780 repo = backend.create_repo()
782 781 # Create a file with no filename, it will display an error but
783 782 # the repo has no commits yet
784 783 response = self.app.post(
785 784 url(
786 785 'files_add',
787 786 repo_name=repo.repo_name,
788 787 revision='tip', f_path='/'),
789 788 params={
790 789 'content': "foo",
791 790 'csrf_token': csrf_token,
792 791 },
793 792 status=302)
794 793
795 794 assert_session_flash(response, 'No filename')
796 795
797 796 # Not allowed, redirect to the summary
798 797 redirected = response.follow()
799 798 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
800 799
801 800 # As there are no commits, displays the summary page with the error of
802 801 # creating a file with no filename
803 802
804 803 assert redirected.request.path == summary_url
805 804
806 805 @pytest.mark.parametrize("location, filename", [
807 806 ('/abs', 'foo'),
808 807 ('../rel', 'foo'),
809 808 ('file/../foo', 'foo'),
810 809 ])
811 810 def test_add_file_into_repo_bad_filenames(
812 811 self, location, filename, backend, csrf_token):
813 812 response = self.app.post(
814 813 url(
815 814 'files_add',
816 815 repo_name=backend.repo_name,
817 816 revision='tip', f_path='/'),
818 817 params={
819 818 'content': "foo",
820 819 'filename': filename,
821 820 'location': location,
822 821 'csrf_token': csrf_token,
823 822 },
824 823 status=302)
825 824
826 825 assert_session_flash(
827 826 response,
828 827 'The location specified must be a relative path and must not '
829 828 'contain .. in the path')
830 829
831 830 @pytest.mark.parametrize("cnt, location, filename", [
832 831 (1, '', 'foo.txt'),
833 832 (2, 'dir', 'foo.rst'),
834 833 (3, 'rel/dir', 'foo.bar'),
835 834 ])
836 835 def test_add_file_into_repo(self, cnt, location, filename, backend,
837 836 csrf_token):
838 837 repo = backend.create_repo()
839 838 response = self.app.post(
840 839 url(
841 840 'files_add',
842 841 repo_name=repo.repo_name,
843 842 revision='tip', f_path='/'),
844 843 params={
845 844 'content': "foo",
846 845 'filename': filename,
847 846 'location': location,
848 847 'csrf_token': csrf_token,
849 848 },
850 849 status=302)
851 assert_session_flash(
852 response, 'Successfully committed to %s'
853 % os.path.join(location, filename))
850 assert_session_flash(response,
851 'Successfully committed new file `{}`'.format(
852 os.path.join(location, filename)))
854 853
855 854 def test_edit_file_view(self, backend):
856 855 response = self.app.get(
857 856 url(
858 857 'files_edit_home',
859 858 repo_name=backend.repo_name,
860 859 revision=backend.default_head_id,
861 860 f_path='vcs/nodes.py'),
862 861 status=200)
863 862 response.mustcontain("Module holding everything related to vcs nodes.")
864 863
865 864 def test_edit_file_view_not_on_branch(self, backend):
866 865 repo = backend.create_repo()
867 866 backend.ensure_file("vcs/nodes.py")
868 867
869 868 response = self.app.get(
870 869 url(
871 870 'files_edit_home',
872 871 repo_name=repo.repo_name,
873 872 revision='tip', f_path='vcs/nodes.py'),
874 873 status=302)
875 874 assert_session_flash(
876 875 response,
877 876 'You can only edit files with revision being a valid branch')
878 877
879 878 def test_edit_file_view_commit_changes(self, backend, csrf_token):
880 879 repo = backend.create_repo()
881 880 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
882 881
883 882 response = self.app.post(
884 883 url(
885 884 'files_edit',
886 885 repo_name=repo.repo_name,
887 886 revision=backend.default_head_id,
888 887 f_path='vcs/nodes.py'),
889 888 params={
890 889 'content': "print 'hello world'",
891 890 'message': 'I committed',
892 891 'filename': "vcs/nodes.py",
893 892 'csrf_token': csrf_token,
894 893 },
895 894 status=302)
896 895 assert_session_flash(
897 response, 'Successfully committed to vcs/nodes.py')
896 response, 'Successfully committed changes to file `vcs/nodes.py`')
898 897 tip = repo.get_commit(commit_idx=-1)
899 898 assert tip.message == 'I committed'
900 899
901 900 def test_edit_file_view_commit_changes_default_message(self, backend,
902 901 csrf_token):
903 902 repo = backend.create_repo()
904 903 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
905 904
906 905 commit_id = (
907 906 backend.default_branch_name or
908 907 backend.repo.scm_instance().commit_ids[-1])
909 908
910 909 response = self.app.post(
911 910 url(
912 911 'files_edit',
913 912 repo_name=repo.repo_name,
914 913 revision=commit_id,
915 914 f_path='vcs/nodes.py'),
916 915 params={
917 916 'content': "print 'hello world'",
918 917 'message': '',
919 918 'filename': "vcs/nodes.py",
920 919 'csrf_token': csrf_token,
921 920 },
922 921 status=302)
923 922 assert_session_flash(
924 response, 'Successfully committed to vcs/nodes.py')
923 response, 'Successfully committed changes to file `vcs/nodes.py`')
925 924 tip = repo.get_commit(commit_idx=-1)
926 925 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
927 926
928 927 def test_delete_file_view(self, backend):
929 928 self.app.get(url(
930 929 'files_delete_home',
931 930 repo_name=backend.repo_name,
932 931 revision='tip', f_path='vcs/nodes.py'))
933 932
934 933 def test_delete_file_view_not_on_branch(self, backend):
935 934 repo = backend.create_repo()
936 935 backend.ensure_file('vcs/nodes.py')
937 936
938 937 response = self.app.get(
939 938 url(
940 939 'files_delete_home',
941 940 repo_name=repo.repo_name,
942 941 revision='tip', f_path='vcs/nodes.py'),
943 942 status=302)
944 943 assert_session_flash(
945 944 response,
946 945 'You can only delete files with revision being a valid branch')
947 946
948 947 def test_delete_file_view_commit_changes(self, backend, csrf_token):
949 948 repo = backend.create_repo()
950 949 backend.ensure_file("vcs/nodes.py")
951 950
952 951 response = self.app.post(
953 952 url(
954 953 'files_delete_home',
955 954 repo_name=repo.repo_name,
956 955 revision=backend.default_head_id,
957 956 f_path='vcs/nodes.py'),
958 957 params={
959 958 'message': 'i commited',
960 959 'csrf_token': csrf_token,
961 960 },
962 961 status=302)
963 962 assert_session_flash(
964 response, 'Successfully deleted file vcs/nodes.py')
963 response, 'Successfully deleted file `vcs/nodes.py`')
965 964
966 965
967 966 def assert_files_in_response(response, files, params):
968 967 template = (
969 968 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
970 969 _assert_items_in_response(response, files, template, params)
971 970
972 971
973 972 def assert_dirs_in_response(response, dirs, params):
974 973 template = (
975 974 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
976 975 _assert_items_in_response(response, dirs, template, params)
977 976
978 977
979 978 def _assert_items_in_response(response, items, template, params):
980 979 for item in items:
981 980 item_params = {'name': item}
982 981 item_params.update(params)
983 982 response.mustcontain(template % item_params)
984 983
985 984
986 985 def assert_timeago_in_response(response, items, params):
987 986 for item in items:
988 987 response.mustcontain(h.age_component(params['date']))
General Comments 0
You need to be logged in to leave comments. Login now