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