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