##// END OF EJS Templates
archvies: allowing to obtain archives without the commit short id in the name for better automation of obtained artifacts.
milka -
r4534:5ad2c43b default
parent child Browse files
Show More
@@ -1,1631 +1,1648 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 h.escape(branch_name), h.escape(rule))
128 h.escape(branch_name), h.escape(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_ref_name
140 default_commit_id = self.db_repo.landing_ref_name
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) as e:
184 except (CommitDoesNotExistError, LookupError) as e:
185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
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, landing_ref):
214 def _is_valid_head(self, commit_id, repo, landing_ref):
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 repo.is_empty():
240 if repo.is_empty():
241 is_head = True
241 is_head = True
242 branch_name = landing_ref
242 branch_name = landing_ref
243 sha_commit_id = EmptyCommit().raw_id
243 sha_commit_id = EmptyCommit().raw_id
244 else:
244 else:
245 commit = repo.get_commit(commit_id=commit_id)
245 commit = repo.get_commit(commit_id=commit_id)
246 if commit:
246 if commit:
247 branch_name = commit.branch
247 branch_name = commit.branch
248 sha_commit_id = commit.raw_id
248 sha_commit_id = commit.raw_id
249
249
250 return branch_name, sha_commit_id, is_head
250 return branch_name, sha_commit_id, is_head
251
251
252 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
252 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
253
253
254 repo_id = self.db_repo.repo_id
254 repo_id = self.db_repo.repo_id
255 force_recache = self.get_recache_flag()
255 force_recache = self.get_recache_flag()
256
256
257 cache_seconds = safe_int(
257 cache_seconds = safe_int(
258 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
258 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
259 cache_on = not force_recache and cache_seconds > 0
259 cache_on = not force_recache and cache_seconds > 0
260 log.debug(
260 log.debug(
261 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
261 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
262 'with caching: %s[TTL: %ss]' % (
262 'with caching: %s[TTL: %ss]' % (
263 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
263 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
264
264
265 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
265 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
266 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
266 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
267
267
268 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
268 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
269 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
269 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
270 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
270 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
271 ver, _repo_id, _commit_id, _f_path)
271 ver, _repo_id, _commit_id, _f_path)
272
272
273 c.full_load = _full_load
273 c.full_load = _full_load
274 return render(
274 return render(
275 'rhodecode:templates/files/files_browser_tree.mako',
275 'rhodecode:templates/files/files_browser_tree.mako',
276 self._get_template_context(c), self.request, _at_rev)
276 self._get_template_context(c), self.request, _at_rev)
277
277
278 return compute_file_tree(
278 return compute_file_tree(
279 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
279 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
280 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
280 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
281
281
282 def _get_archive_spec(self, fname):
282 def _get_archive_spec(self, fname):
283 log.debug('Detecting archive spec for: `%s`', fname)
283 log.debug('Detecting archive spec for: `%s`', fname)
284
284
285 fileformat = None
285 fileformat = None
286 ext = None
286 ext = None
287 content_type = None
287 content_type = None
288 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
288 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
289
289
290 if fname.endswith(extension):
290 if fname.endswith(extension):
291 fileformat = a_type
291 fileformat = a_type
292 log.debug('archive is of type: %s', fileformat)
292 log.debug('archive is of type: %s', fileformat)
293 ext = extension
293 ext = extension
294 break
294 break
295
295
296 if not fileformat:
296 if not fileformat:
297 raise ValueError()
297 raise ValueError()
298
298
299 # left over part of whole fname is the commit
299 # left over part of whole fname is the commit
300 commit_id = fname[:-len(ext)]
300 commit_id = fname[:-len(ext)]
301
301
302 return commit_id, ext, fileformat, content_type
302 return commit_id, ext, fileformat, content_type
303
303
304 def create_pure_path(self, *parts):
304 def create_pure_path(self, *parts):
305 # Split paths and sanitize them, removing any ../ etc
305 # Split paths and sanitize them, removing any ../ etc
306 sanitized_path = [
306 sanitized_path = [
307 x for x in pathlib2.PurePath(*parts).parts
307 x for x in pathlib2.PurePath(*parts).parts
308 if x not in ['.', '..']]
308 if x not in ['.', '..']]
309
309
310 pure_path = pathlib2.PurePath(*sanitized_path)
310 pure_path = pathlib2.PurePath(*sanitized_path)
311 return pure_path
311 return pure_path
312
312
313 def _is_lf_enabled(self, target_repo):
313 def _is_lf_enabled(self, target_repo):
314 lf_enabled = False
314 lf_enabled = False
315
315
316 lf_key_for_vcs_map = {
316 lf_key_for_vcs_map = {
317 'hg': 'extensions_largefiles',
317 'hg': 'extensions_largefiles',
318 'git': 'vcs_git_lfs_enabled'
318 'git': 'vcs_git_lfs_enabled'
319 }
319 }
320
320
321 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
321 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
322
322
323 if lf_key_for_vcs:
323 if lf_key_for_vcs:
324 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
324 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
325
325
326 return lf_enabled
326 return lf_enabled
327
327
328 def _get_archive_name(self, db_repo_name, commit_sha, ext, subrepos=False, path_sha=''):
329 # original backward compat name of archive
330 clean_name = safe_str(db_repo_name.replace('/', '_'))
331
332 # e.g vcsserver.zip
333 # e.g vcsserver-abcdefgh.zip
334 # e.g vcsserver-abcdefgh-defghijk.zip
335 archive_name = '{}{}{}{}{}'.format(
336 clean_name,
337 '-sub' if subrepos else '',
338 commit_sha,
339 '-{}'.format(path_sha) if path_sha else '',
340 ext)
341 return archive_name
342
328 @LoginRequired()
343 @LoginRequired()
329 @HasRepoPermissionAnyDecorator(
344 @HasRepoPermissionAnyDecorator(
330 'repository.read', 'repository.write', 'repository.admin')
345 'repository.read', 'repository.write', 'repository.admin')
331 @view_config(
346 @view_config(
332 route_name='repo_archivefile', request_method='GET',
347 route_name='repo_archivefile', request_method='GET',
333 renderer=None)
348 renderer=None)
334 def repo_archivefile(self):
349 def repo_archivefile(self):
335 # archive cache config
350 # archive cache config
336 from rhodecode import CONFIG
351 from rhodecode import CONFIG
337 _ = self.request.translate
352 _ = self.request.translate
338 self.load_default_context()
353 self.load_default_context()
339 default_at_path = '/'
354 default_at_path = '/'
340 fname = self.request.matchdict['fname']
355 fname = self.request.matchdict['fname']
341 subrepos = self.request.GET.get('subrepos') == 'true'
356 subrepos = self.request.GET.get('subrepos') == 'true'
357 with_hash = str2bool(self.request.GET.get('with_hash', '1'))
342 at_path = self.request.GET.get('at_path') or default_at_path
358 at_path = self.request.GET.get('at_path') or default_at_path
343
359
344 if not self.db_repo.enable_downloads:
360 if not self.db_repo.enable_downloads:
345 return Response(_('Downloads disabled'))
361 return Response(_('Downloads disabled'))
346
362
347 try:
363 try:
348 commit_id, ext, fileformat, content_type = \
364 commit_id, ext, fileformat, content_type = \
349 self._get_archive_spec(fname)
365 self._get_archive_spec(fname)
350 except ValueError:
366 except ValueError:
351 return Response(_('Unknown archive type for: `{}`').format(
367 return Response(_('Unknown archive type for: `{}`').format(
352 h.escape(fname)))
368 h.escape(fname)))
353
369
354 try:
370 try:
355 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
371 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
356 except CommitDoesNotExistError:
372 except CommitDoesNotExistError:
357 return Response(_('Unknown commit_id {}').format(
373 return Response(_('Unknown commit_id {}').format(
358 h.escape(commit_id)))
374 h.escape(commit_id)))
359 except EmptyRepositoryError:
375 except EmptyRepositoryError:
360 return Response(_('Empty repository'))
376 return Response(_('Empty repository'))
361
377
362 try:
378 try:
363 at_path = commit.get_node(at_path).path or default_at_path
379 at_path = commit.get_node(at_path).path or default_at_path
364 except Exception:
380 except Exception:
365 return Response(_('No node at path {} for this repository').format(at_path))
381 return Response(_('No node at path {} for this repository').format(at_path))
366
382
367 path_sha = sha1(at_path)[:8]
383 # path sha is part of subdir
368
384 path_sha = ''
369 # original backward compat name of archive
385 if at_path != default_at_path:
370 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
386 path_sha = sha1(at_path)[:8]
371 short_sha = safe_str(commit.short_id)
387 short_sha = '-{}'.format(safe_str(commit.short_id))
388 # used for cache etc
389 archive_name = self._get_archive_name(
390 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
391 path_sha=path_sha)
372
392
373 if at_path == default_at_path:
393 if not with_hash:
374 archive_name = '{}-{}{}{}'.format(
394 short_sha = ''
375 clean_name,
395 path_sha = ''
376 '-sub' if subrepos else '',
396
377 short_sha,
397 # what end client gets served
378 ext)
398 response_archive_name = self._get_archive_name(
379 # custom path and new name
399 self.db_repo_name, commit_sha=short_sha, ext=ext, subrepos=subrepos,
380 else:
400 path_sha=path_sha)
381 archive_name = '{}-{}{}-{}{}'.format(
401 # remove extension from our archive directory name
382 clean_name,
402 archive_dir_name = response_archive_name[:-len(ext)]
383 '-sub' if subrepos else '',
384 short_sha,
385 path_sha,
386 ext)
387
403
388 use_cached_archive = False
404 use_cached_archive = False
389 archive_cache_enabled = CONFIG.get(
405 archive_cache_enabled = CONFIG.get(
390 'archive_cache_dir') and not self.request.GET.get('no_cache')
406 'archive_cache_dir') and not self.request.GET.get('no_cache')
391 cached_archive_path = None
407 cached_archive_path = None
392
408
393 if archive_cache_enabled:
409 if archive_cache_enabled:
394 # check if we it's ok to write
410 # check if we it's ok to write
395 if not os.path.isdir(CONFIG['archive_cache_dir']):
411 if not os.path.isdir(CONFIG['archive_cache_dir']):
396 os.makedirs(CONFIG['archive_cache_dir'])
412 os.makedirs(CONFIG['archive_cache_dir'])
397 cached_archive_path = os.path.join(
413 cached_archive_path = os.path.join(
398 CONFIG['archive_cache_dir'], archive_name)
414 CONFIG['archive_cache_dir'], archive_name)
399 if os.path.isfile(cached_archive_path):
415 if os.path.isfile(cached_archive_path):
400 log.debug('Found cached archive in %s', cached_archive_path)
416 log.debug('Found cached archive in %s', cached_archive_path)
401 fd, archive = None, cached_archive_path
417 fd, archive = None, cached_archive_path
402 use_cached_archive = True
418 use_cached_archive = True
403 else:
419 else:
404 log.debug('Archive %s is not yet cached', archive_name)
420 log.debug('Archive %s is not yet cached', archive_name)
405
421
422 # generate new archive, as previous was not found in the cache
406 if not use_cached_archive:
423 if not use_cached_archive:
407 # generate new archive
424
408 fd, archive = tempfile.mkstemp()
425 fd, archive = tempfile.mkstemp()
409 log.debug('Creating new temp archive in %s', archive)
426 log.debug('Creating new temp archive in %s', archive)
410 try:
427 try:
411 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
428 commit.archive_repo(archive, archive_dir_name=archive_dir_name,
412 archive_at_path=at_path)
429 kind=fileformat, subrepos=subrepos,
430 archive_at_path=at_path, with_hash=with_hash)
413 except ImproperArchiveTypeError:
431 except ImproperArchiveTypeError:
414 return _('Unknown archive type')
432 return _('Unknown archive type')
415 if archive_cache_enabled:
433 if archive_cache_enabled:
416 # if we generated the archive and we have cache enabled
434 # if we generated the archive and we have cache enabled
417 # let's use this for future
435 # let's use this for future
418 log.debug('Storing new archive in %s', cached_archive_path)
436 log.debug('Storing new archive in %s', cached_archive_path)
419 shutil.move(archive, cached_archive_path)
437 shutil.move(archive, cached_archive_path)
420 archive = cached_archive_path
438 archive = cached_archive_path
421
439
422 # store download action
440 # store download action
423 audit_logger.store_web(
441 audit_logger.store_web(
424 'repo.archive.download', action_data={
442 'repo.archive.download', action_data={
425 'user_agent': self.request.user_agent,
443 'user_agent': self.request.user_agent,
426 'archive_name': archive_name,
444 'archive_name': archive_name,
427 'archive_spec': fname,
445 'archive_spec': fname,
428 'archive_cached': use_cached_archive},
446 'archive_cached': use_cached_archive},
429 user=self._rhodecode_user,
447 user=self._rhodecode_user,
430 repo=self.db_repo,
448 repo=self.db_repo,
431 commit=True
449 commit=True
432 )
450 )
433
451
434 def get_chunked_archive(archive_path):
452 def get_chunked_archive(archive_path):
435 with open(archive_path, 'rb') as stream:
453 with open(archive_path, 'rb') as stream:
436 while True:
454 while True:
437 data = stream.read(16 * 1024)
455 data = stream.read(16 * 1024)
438 if not data:
456 if not data:
439 if fd: # fd means we used temporary file
457 if fd: # fd means we used temporary file
440 os.close(fd)
458 os.close(fd)
441 if not archive_cache_enabled:
459 if not archive_cache_enabled:
442 log.debug('Destroying temp archive %s', archive_path)
460 log.debug('Destroying temp archive %s', archive_path)
443 os.remove(archive_path)
461 os.remove(archive_path)
444 break
462 break
445 yield data
463 yield data
446
464
447 response = Response(app_iter=get_chunked_archive(archive))
465 response = Response(app_iter=get_chunked_archive(archive))
448 response.content_disposition = str(
466 response.content_disposition = str('attachment; filename=%s' % response_archive_name)
449 'attachment; filename=%s' % archive_name)
450 response.content_type = str(content_type)
467 response.content_type = str(content_type)
451
468
452 return response
469 return response
453
470
454 def _get_file_node(self, commit_id, f_path):
471 def _get_file_node(self, commit_id, f_path):
455 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
472 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
456 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
473 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
457 try:
474 try:
458 node = commit.get_node(f_path)
475 node = commit.get_node(f_path)
459 if node.is_dir():
476 if node.is_dir():
460 raise NodeError('%s path is a %s not a file'
477 raise NodeError('%s path is a %s not a file'
461 % (node, type(node)))
478 % (node, type(node)))
462 except NodeDoesNotExistError:
479 except NodeDoesNotExistError:
463 commit = EmptyCommit(
480 commit = EmptyCommit(
464 commit_id=commit_id,
481 commit_id=commit_id,
465 idx=commit.idx,
482 idx=commit.idx,
466 repo=commit.repository,
483 repo=commit.repository,
467 alias=commit.repository.alias,
484 alias=commit.repository.alias,
468 message=commit.message,
485 message=commit.message,
469 author=commit.author,
486 author=commit.author,
470 date=commit.date)
487 date=commit.date)
471 node = FileNode(f_path, '', commit=commit)
488 node = FileNode(f_path, '', commit=commit)
472 else:
489 else:
473 commit = EmptyCommit(
490 commit = EmptyCommit(
474 repo=self.rhodecode_vcs_repo,
491 repo=self.rhodecode_vcs_repo,
475 alias=self.rhodecode_vcs_repo.alias)
492 alias=self.rhodecode_vcs_repo.alias)
476 node = FileNode(f_path, '', commit=commit)
493 node = FileNode(f_path, '', commit=commit)
477 return node
494 return node
478
495
479 @LoginRequired()
496 @LoginRequired()
480 @HasRepoPermissionAnyDecorator(
497 @HasRepoPermissionAnyDecorator(
481 'repository.read', 'repository.write', 'repository.admin')
498 'repository.read', 'repository.write', 'repository.admin')
482 @view_config(
499 @view_config(
483 route_name='repo_files_diff', request_method='GET',
500 route_name='repo_files_diff', request_method='GET',
484 renderer=None)
501 renderer=None)
485 def repo_files_diff(self):
502 def repo_files_diff(self):
486 c = self.load_default_context()
503 c = self.load_default_context()
487 f_path = self._get_f_path(self.request.matchdict)
504 f_path = self._get_f_path(self.request.matchdict)
488 diff1 = self.request.GET.get('diff1', '')
505 diff1 = self.request.GET.get('diff1', '')
489 diff2 = self.request.GET.get('diff2', '')
506 diff2 = self.request.GET.get('diff2', '')
490
507
491 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
508 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
492
509
493 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
510 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
494 line_context = self.request.GET.get('context', 3)
511 line_context = self.request.GET.get('context', 3)
495
512
496 if not any((diff1, diff2)):
513 if not any((diff1, diff2)):
497 h.flash(
514 h.flash(
498 'Need query parameter "diff1" or "diff2" to generate a diff.',
515 'Need query parameter "diff1" or "diff2" to generate a diff.',
499 category='error')
516 category='error')
500 raise HTTPBadRequest()
517 raise HTTPBadRequest()
501
518
502 c.action = self.request.GET.get('diff')
519 c.action = self.request.GET.get('diff')
503 if c.action not in ['download', 'raw']:
520 if c.action not in ['download', 'raw']:
504 compare_url = h.route_path(
521 compare_url = h.route_path(
505 'repo_compare',
522 'repo_compare',
506 repo_name=self.db_repo_name,
523 repo_name=self.db_repo_name,
507 source_ref_type='rev',
524 source_ref_type='rev',
508 source_ref=diff1,
525 source_ref=diff1,
509 target_repo=self.db_repo_name,
526 target_repo=self.db_repo_name,
510 target_ref_type='rev',
527 target_ref_type='rev',
511 target_ref=diff2,
528 target_ref=diff2,
512 _query=dict(f_path=f_path))
529 _query=dict(f_path=f_path))
513 # redirect to new view if we render diff
530 # redirect to new view if we render diff
514 raise HTTPFound(compare_url)
531 raise HTTPFound(compare_url)
515
532
516 try:
533 try:
517 node1 = self._get_file_node(diff1, path1)
534 node1 = self._get_file_node(diff1, path1)
518 node2 = self._get_file_node(diff2, f_path)
535 node2 = self._get_file_node(diff2, f_path)
519 except (RepositoryError, NodeError):
536 except (RepositoryError, NodeError):
520 log.exception("Exception while trying to get node from repository")
537 log.exception("Exception while trying to get node from repository")
521 raise HTTPFound(
538 raise HTTPFound(
522 h.route_path('repo_files', repo_name=self.db_repo_name,
539 h.route_path('repo_files', repo_name=self.db_repo_name,
523 commit_id='tip', f_path=f_path))
540 commit_id='tip', f_path=f_path))
524
541
525 if all(isinstance(node.commit, EmptyCommit)
542 if all(isinstance(node.commit, EmptyCommit)
526 for node in (node1, node2)):
543 for node in (node1, node2)):
527 raise HTTPNotFound()
544 raise HTTPNotFound()
528
545
529 c.commit_1 = node1.commit
546 c.commit_1 = node1.commit
530 c.commit_2 = node2.commit
547 c.commit_2 = node2.commit
531
548
532 if c.action == 'download':
549 if c.action == 'download':
533 _diff = diffs.get_gitdiff(node1, node2,
550 _diff = diffs.get_gitdiff(node1, node2,
534 ignore_whitespace=ignore_whitespace,
551 ignore_whitespace=ignore_whitespace,
535 context=line_context)
552 context=line_context)
536 diff = diffs.DiffProcessor(_diff, format='gitdiff')
553 diff = diffs.DiffProcessor(_diff, format='gitdiff')
537
554
538 response = Response(self.path_filter.get_raw_patch(diff))
555 response = Response(self.path_filter.get_raw_patch(diff))
539 response.content_type = 'text/plain'
556 response.content_type = 'text/plain'
540 response.content_disposition = (
557 response.content_disposition = (
541 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
558 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
542 )
559 )
543 charset = self._get_default_encoding(c)
560 charset = self._get_default_encoding(c)
544 if charset:
561 if charset:
545 response.charset = charset
562 response.charset = charset
546 return response
563 return response
547
564
548 elif c.action == 'raw':
565 elif c.action == 'raw':
549 _diff = diffs.get_gitdiff(node1, node2,
566 _diff = diffs.get_gitdiff(node1, node2,
550 ignore_whitespace=ignore_whitespace,
567 ignore_whitespace=ignore_whitespace,
551 context=line_context)
568 context=line_context)
552 diff = diffs.DiffProcessor(_diff, format='gitdiff')
569 diff = diffs.DiffProcessor(_diff, format='gitdiff')
553
570
554 response = Response(self.path_filter.get_raw_patch(diff))
571 response = Response(self.path_filter.get_raw_patch(diff))
555 response.content_type = 'text/plain'
572 response.content_type = 'text/plain'
556 charset = self._get_default_encoding(c)
573 charset = self._get_default_encoding(c)
557 if charset:
574 if charset:
558 response.charset = charset
575 response.charset = charset
559 return response
576 return response
560
577
561 # in case we ever end up here
578 # in case we ever end up here
562 raise HTTPNotFound()
579 raise HTTPNotFound()
563
580
564 @LoginRequired()
581 @LoginRequired()
565 @HasRepoPermissionAnyDecorator(
582 @HasRepoPermissionAnyDecorator(
566 'repository.read', 'repository.write', 'repository.admin')
583 'repository.read', 'repository.write', 'repository.admin')
567 @view_config(
584 @view_config(
568 route_name='repo_files_diff_2way_redirect', request_method='GET',
585 route_name='repo_files_diff_2way_redirect', request_method='GET',
569 renderer=None)
586 renderer=None)
570 def repo_files_diff_2way_redirect(self):
587 def repo_files_diff_2way_redirect(self):
571 """
588 """
572 Kept only to make OLD links work
589 Kept only to make OLD links work
573 """
590 """
574 f_path = self._get_f_path_unchecked(self.request.matchdict)
591 f_path = self._get_f_path_unchecked(self.request.matchdict)
575 diff1 = self.request.GET.get('diff1', '')
592 diff1 = self.request.GET.get('diff1', '')
576 diff2 = self.request.GET.get('diff2', '')
593 diff2 = self.request.GET.get('diff2', '')
577
594
578 if not any((diff1, diff2)):
595 if not any((diff1, diff2)):
579 h.flash(
596 h.flash(
580 'Need query parameter "diff1" or "diff2" to generate a diff.',
597 'Need query parameter "diff1" or "diff2" to generate a diff.',
581 category='error')
598 category='error')
582 raise HTTPBadRequest()
599 raise HTTPBadRequest()
583
600
584 compare_url = h.route_path(
601 compare_url = h.route_path(
585 'repo_compare',
602 'repo_compare',
586 repo_name=self.db_repo_name,
603 repo_name=self.db_repo_name,
587 source_ref_type='rev',
604 source_ref_type='rev',
588 source_ref=diff1,
605 source_ref=diff1,
589 target_ref_type='rev',
606 target_ref_type='rev',
590 target_ref=diff2,
607 target_ref=diff2,
591 _query=dict(f_path=f_path, diffmode='sideside',
608 _query=dict(f_path=f_path, diffmode='sideside',
592 target_repo=self.db_repo_name,))
609 target_repo=self.db_repo_name,))
593 raise HTTPFound(compare_url)
610 raise HTTPFound(compare_url)
594
611
595 @LoginRequired()
612 @LoginRequired()
596 @view_config(
613 @view_config(
597 route_name='repo_files:default_commit', request_method='GET',
614 route_name='repo_files:default_commit', request_method='GET',
598 renderer=None)
615 renderer=None)
599 def repo_files_default(self):
616 def repo_files_default(self):
600 c = self.load_default_context()
617 c = self.load_default_context()
601 ref_name = c.rhodecode_db_repo.landing_ref_name
618 ref_name = c.rhodecode_db_repo.landing_ref_name
602 landing_url = h.repo_files_by_ref_url(
619 landing_url = h.repo_files_by_ref_url(
603 c.rhodecode_db_repo.repo_name,
620 c.rhodecode_db_repo.repo_name,
604 c.rhodecode_db_repo.repo_type,
621 c.rhodecode_db_repo.repo_type,
605 f_path='',
622 f_path='',
606 ref_name=ref_name,
623 ref_name=ref_name,
607 commit_id='tip',
624 commit_id='tip',
608 query=dict(at=ref_name)
625 query=dict(at=ref_name)
609 )
626 )
610
627
611 raise HTTPFound(landing_url)
628 raise HTTPFound(landing_url)
612
629
613 @LoginRequired()
630 @LoginRequired()
614 @HasRepoPermissionAnyDecorator(
631 @HasRepoPermissionAnyDecorator(
615 'repository.read', 'repository.write', 'repository.admin')
632 'repository.read', 'repository.write', 'repository.admin')
616 @view_config(
633 @view_config(
617 route_name='repo_files', request_method='GET',
634 route_name='repo_files', request_method='GET',
618 renderer=None)
635 renderer=None)
619 @view_config(
636 @view_config(
620 route_name='repo_files:default_path', request_method='GET',
637 route_name='repo_files:default_path', request_method='GET',
621 renderer=None)
638 renderer=None)
622 @view_config(
639 @view_config(
623 route_name='repo_files:rendered', request_method='GET',
640 route_name='repo_files:rendered', request_method='GET',
624 renderer=None)
641 renderer=None)
625 @view_config(
642 @view_config(
626 route_name='repo_files:annotated', request_method='GET',
643 route_name='repo_files:annotated', request_method='GET',
627 renderer=None)
644 renderer=None)
628 def repo_files(self):
645 def repo_files(self):
629 c = self.load_default_context()
646 c = self.load_default_context()
630
647
631 view_name = getattr(self.request.matched_route, 'name', None)
648 view_name = getattr(self.request.matched_route, 'name', None)
632
649
633 c.annotate = view_name == 'repo_files:annotated'
650 c.annotate = view_name == 'repo_files:annotated'
634 # default is false, but .rst/.md files later are auto rendered, we can
651 # default is false, but .rst/.md files later are auto rendered, we can
635 # overwrite auto rendering by setting this GET flag
652 # overwrite auto rendering by setting this GET flag
636 c.renderer = view_name == 'repo_files:rendered' or \
653 c.renderer = view_name == 'repo_files:rendered' or \
637 not self.request.GET.get('no-render', False)
654 not self.request.GET.get('no-render', False)
638
655
639 commit_id, f_path = self._get_commit_and_path()
656 commit_id, f_path = self._get_commit_and_path()
640
657
641 c.commit = self._get_commit_or_redirect(commit_id)
658 c.commit = self._get_commit_or_redirect(commit_id)
642 c.branch = self.request.GET.get('branch', None)
659 c.branch = self.request.GET.get('branch', None)
643 c.f_path = f_path
660 c.f_path = f_path
644 at_rev = self.request.GET.get('at')
661 at_rev = self.request.GET.get('at')
645
662
646 # prev link
663 # prev link
647 try:
664 try:
648 prev_commit = c.commit.prev(c.branch)
665 prev_commit = c.commit.prev(c.branch)
649 c.prev_commit = prev_commit
666 c.prev_commit = prev_commit
650 c.url_prev = h.route_path(
667 c.url_prev = h.route_path(
651 'repo_files', repo_name=self.db_repo_name,
668 'repo_files', repo_name=self.db_repo_name,
652 commit_id=prev_commit.raw_id, f_path=f_path)
669 commit_id=prev_commit.raw_id, f_path=f_path)
653 if c.branch:
670 if c.branch:
654 c.url_prev += '?branch=%s' % c.branch
671 c.url_prev += '?branch=%s' % c.branch
655 except (CommitDoesNotExistError, VCSError):
672 except (CommitDoesNotExistError, VCSError):
656 c.url_prev = '#'
673 c.url_prev = '#'
657 c.prev_commit = EmptyCommit()
674 c.prev_commit = EmptyCommit()
658
675
659 # next link
676 # next link
660 try:
677 try:
661 next_commit = c.commit.next(c.branch)
678 next_commit = c.commit.next(c.branch)
662 c.next_commit = next_commit
679 c.next_commit = next_commit
663 c.url_next = h.route_path(
680 c.url_next = h.route_path(
664 'repo_files', repo_name=self.db_repo_name,
681 'repo_files', repo_name=self.db_repo_name,
665 commit_id=next_commit.raw_id, f_path=f_path)
682 commit_id=next_commit.raw_id, f_path=f_path)
666 if c.branch:
683 if c.branch:
667 c.url_next += '?branch=%s' % c.branch
684 c.url_next += '?branch=%s' % c.branch
668 except (CommitDoesNotExistError, VCSError):
685 except (CommitDoesNotExistError, VCSError):
669 c.url_next = '#'
686 c.url_next = '#'
670 c.next_commit = EmptyCommit()
687 c.next_commit = EmptyCommit()
671
688
672 # files or dirs
689 # files or dirs
673 try:
690 try:
674 c.file = c.commit.get_node(f_path)
691 c.file = c.commit.get_node(f_path)
675 c.file_author = True
692 c.file_author = True
676 c.file_tree = ''
693 c.file_tree = ''
677
694
678 # load file content
695 # load file content
679 if c.file.is_file():
696 if c.file.is_file():
680 c.lf_node = {}
697 c.lf_node = {}
681
698
682 has_lf_enabled = self._is_lf_enabled(self.db_repo)
699 has_lf_enabled = self._is_lf_enabled(self.db_repo)
683 if has_lf_enabled:
700 if has_lf_enabled:
684 c.lf_node = c.file.get_largefile_node()
701 c.lf_node = c.file.get_largefile_node()
685
702
686 c.file_source_page = 'true'
703 c.file_source_page = 'true'
687 c.file_last_commit = c.file.last_commit
704 c.file_last_commit = c.file.last_commit
688
705
689 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
706 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
690
707
691 if not (c.file_size_too_big or c.file.is_binary):
708 if not (c.file_size_too_big or c.file.is_binary):
692 if c.annotate: # annotation has precedence over renderer
709 if c.annotate: # annotation has precedence over renderer
693 c.annotated_lines = filenode_as_annotated_lines_tokens(
710 c.annotated_lines = filenode_as_annotated_lines_tokens(
694 c.file
711 c.file
695 )
712 )
696 else:
713 else:
697 c.renderer = (
714 c.renderer = (
698 c.renderer and h.renderer_from_filename(c.file.path)
715 c.renderer and h.renderer_from_filename(c.file.path)
699 )
716 )
700 if not c.renderer:
717 if not c.renderer:
701 c.lines = filenode_as_lines_tokens(c.file)
718 c.lines = filenode_as_lines_tokens(c.file)
702
719
703 _branch_name, _sha_commit_id, is_head = \
720 _branch_name, _sha_commit_id, is_head = \
704 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
721 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
705 landing_ref=self.db_repo.landing_ref_name)
722 landing_ref=self.db_repo.landing_ref_name)
706 c.on_branch_head = is_head
723 c.on_branch_head = is_head
707
724
708 branch = c.commit.branch if (
725 branch = c.commit.branch if (
709 c.commit.branch and '/' not in c.commit.branch) else None
726 c.commit.branch and '/' not in c.commit.branch) else None
710 c.branch_or_raw_id = branch or c.commit.raw_id
727 c.branch_or_raw_id = branch or c.commit.raw_id
711 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
728 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
712
729
713 author = c.file_last_commit.author
730 author = c.file_last_commit.author
714 c.authors = [[
731 c.authors = [[
715 h.email(author),
732 h.email(author),
716 h.person(author, 'username_or_name_or_email'),
733 h.person(author, 'username_or_name_or_email'),
717 1
734 1
718 ]]
735 ]]
719
736
720 else: # load tree content at path
737 else: # load tree content at path
721 c.file_source_page = 'false'
738 c.file_source_page = 'false'
722 c.authors = []
739 c.authors = []
723 # this loads a simple tree without metadata to speed things up
740 # this loads a simple tree without metadata to speed things up
724 # later via ajax we call repo_nodetree_full and fetch whole
741 # later via ajax we call repo_nodetree_full and fetch whole
725 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
742 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
726
743
727 c.readme_data, c.readme_file = \
744 c.readme_data, c.readme_file = \
728 self._get_readme_data(self.db_repo, c.visual.default_renderer,
745 self._get_readme_data(self.db_repo, c.visual.default_renderer,
729 c.commit.raw_id, f_path)
746 c.commit.raw_id, f_path)
730
747
731 except RepositoryError as e:
748 except RepositoryError as e:
732 h.flash(safe_str(h.escape(e)), category='error')
749 h.flash(safe_str(h.escape(e)), category='error')
733 raise HTTPNotFound()
750 raise HTTPNotFound()
734
751
735 if self.request.environ.get('HTTP_X_PJAX'):
752 if self.request.environ.get('HTTP_X_PJAX'):
736 html = render('rhodecode:templates/files/files_pjax.mako',
753 html = render('rhodecode:templates/files/files_pjax.mako',
737 self._get_template_context(c), self.request)
754 self._get_template_context(c), self.request)
738 else:
755 else:
739 html = render('rhodecode:templates/files/files.mako',
756 html = render('rhodecode:templates/files/files.mako',
740 self._get_template_context(c), self.request)
757 self._get_template_context(c), self.request)
741 return Response(html)
758 return Response(html)
742
759
743 @HasRepoPermissionAnyDecorator(
760 @HasRepoPermissionAnyDecorator(
744 'repository.read', 'repository.write', 'repository.admin')
761 'repository.read', 'repository.write', 'repository.admin')
745 @view_config(
762 @view_config(
746 route_name='repo_files:annotated_previous', request_method='GET',
763 route_name='repo_files:annotated_previous', request_method='GET',
747 renderer=None)
764 renderer=None)
748 def repo_files_annotated_previous(self):
765 def repo_files_annotated_previous(self):
749 self.load_default_context()
766 self.load_default_context()
750
767
751 commit_id, f_path = self._get_commit_and_path()
768 commit_id, f_path = self._get_commit_and_path()
752 commit = self._get_commit_or_redirect(commit_id)
769 commit = self._get_commit_or_redirect(commit_id)
753 prev_commit_id = commit.raw_id
770 prev_commit_id = commit.raw_id
754 line_anchor = self.request.GET.get('line_anchor')
771 line_anchor = self.request.GET.get('line_anchor')
755 is_file = False
772 is_file = False
756 try:
773 try:
757 _file = commit.get_node(f_path)
774 _file = commit.get_node(f_path)
758 is_file = _file.is_file()
775 is_file = _file.is_file()
759 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
776 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
760 pass
777 pass
761
778
762 if is_file:
779 if is_file:
763 history = commit.get_path_history(f_path)
780 history = commit.get_path_history(f_path)
764 prev_commit_id = history[1].raw_id \
781 prev_commit_id = history[1].raw_id \
765 if len(history) > 1 else prev_commit_id
782 if len(history) > 1 else prev_commit_id
766 prev_url = h.route_path(
783 prev_url = h.route_path(
767 'repo_files:annotated', repo_name=self.db_repo_name,
784 'repo_files:annotated', repo_name=self.db_repo_name,
768 commit_id=prev_commit_id, f_path=f_path,
785 commit_id=prev_commit_id, f_path=f_path,
769 _anchor='L{}'.format(line_anchor))
786 _anchor='L{}'.format(line_anchor))
770
787
771 raise HTTPFound(prev_url)
788 raise HTTPFound(prev_url)
772
789
773 @LoginRequired()
790 @LoginRequired()
774 @HasRepoPermissionAnyDecorator(
791 @HasRepoPermissionAnyDecorator(
775 'repository.read', 'repository.write', 'repository.admin')
792 'repository.read', 'repository.write', 'repository.admin')
776 @view_config(
793 @view_config(
777 route_name='repo_nodetree_full', request_method='GET',
794 route_name='repo_nodetree_full', request_method='GET',
778 renderer=None, xhr=True)
795 renderer=None, xhr=True)
779 @view_config(
796 @view_config(
780 route_name='repo_nodetree_full:default_path', request_method='GET',
797 route_name='repo_nodetree_full:default_path', request_method='GET',
781 renderer=None, xhr=True)
798 renderer=None, xhr=True)
782 def repo_nodetree_full(self):
799 def repo_nodetree_full(self):
783 """
800 """
784 Returns rendered html of file tree that contains commit date,
801 Returns rendered html of file tree that contains commit date,
785 author, commit_id for the specified combination of
802 author, commit_id for the specified combination of
786 repo, commit_id and file path
803 repo, commit_id and file path
787 """
804 """
788 c = self.load_default_context()
805 c = self.load_default_context()
789
806
790 commit_id, f_path = self._get_commit_and_path()
807 commit_id, f_path = self._get_commit_and_path()
791 commit = self._get_commit_or_redirect(commit_id)
808 commit = self._get_commit_or_redirect(commit_id)
792 try:
809 try:
793 dir_node = commit.get_node(f_path)
810 dir_node = commit.get_node(f_path)
794 except RepositoryError as e:
811 except RepositoryError as e:
795 return Response('error: {}'.format(h.escape(safe_str(e))))
812 return Response('error: {}'.format(h.escape(safe_str(e))))
796
813
797 if dir_node.is_file():
814 if dir_node.is_file():
798 return Response('')
815 return Response('')
799
816
800 c.file = dir_node
817 c.file = dir_node
801 c.commit = commit
818 c.commit = commit
802 at_rev = self.request.GET.get('at')
819 at_rev = self.request.GET.get('at')
803
820
804 html = self._get_tree_at_commit(
821 html = self._get_tree_at_commit(
805 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
822 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
806
823
807 return Response(html)
824 return Response(html)
808
825
809 def _get_attachement_headers(self, f_path):
826 def _get_attachement_headers(self, f_path):
810 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
827 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
811 safe_path = f_name.replace('"', '\\"')
828 safe_path = f_name.replace('"', '\\"')
812 encoded_path = urllib.quote(f_name)
829 encoded_path = urllib.quote(f_name)
813
830
814 return "attachment; " \
831 return "attachment; " \
815 "filename=\"{}\"; " \
832 "filename=\"{}\"; " \
816 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
833 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
817
834
818 @LoginRequired()
835 @LoginRequired()
819 @HasRepoPermissionAnyDecorator(
836 @HasRepoPermissionAnyDecorator(
820 'repository.read', 'repository.write', 'repository.admin')
837 'repository.read', 'repository.write', 'repository.admin')
821 @view_config(
838 @view_config(
822 route_name='repo_file_raw', request_method='GET',
839 route_name='repo_file_raw', request_method='GET',
823 renderer=None)
840 renderer=None)
824 def repo_file_raw(self):
841 def repo_file_raw(self):
825 """
842 """
826 Action for show as raw, some mimetypes are "rendered",
843 Action for show as raw, some mimetypes are "rendered",
827 those include images, icons.
844 those include images, icons.
828 """
845 """
829 c = self.load_default_context()
846 c = self.load_default_context()
830
847
831 commit_id, f_path = self._get_commit_and_path()
848 commit_id, f_path = self._get_commit_and_path()
832 commit = self._get_commit_or_redirect(commit_id)
849 commit = self._get_commit_or_redirect(commit_id)
833 file_node = self._get_filenode_or_redirect(commit, f_path)
850 file_node = self._get_filenode_or_redirect(commit, f_path)
834
851
835 raw_mimetype_mapping = {
852 raw_mimetype_mapping = {
836 # map original mimetype to a mimetype used for "show as raw"
853 # map original mimetype to a mimetype used for "show as raw"
837 # you can also provide a content-disposition to override the
854 # you can also provide a content-disposition to override the
838 # default "attachment" disposition.
855 # default "attachment" disposition.
839 # orig_type: (new_type, new_dispo)
856 # orig_type: (new_type, new_dispo)
840
857
841 # show images inline:
858 # show images inline:
842 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
859 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
843 # for example render an SVG with javascript inside or even render
860 # for example render an SVG with javascript inside or even render
844 # HTML.
861 # HTML.
845 'image/x-icon': ('image/x-icon', 'inline'),
862 'image/x-icon': ('image/x-icon', 'inline'),
846 'image/png': ('image/png', 'inline'),
863 'image/png': ('image/png', 'inline'),
847 'image/gif': ('image/gif', 'inline'),
864 'image/gif': ('image/gif', 'inline'),
848 'image/jpeg': ('image/jpeg', 'inline'),
865 'image/jpeg': ('image/jpeg', 'inline'),
849 'application/pdf': ('application/pdf', 'inline'),
866 'application/pdf': ('application/pdf', 'inline'),
850 }
867 }
851
868
852 mimetype = file_node.mimetype
869 mimetype = file_node.mimetype
853 try:
870 try:
854 mimetype, disposition = raw_mimetype_mapping[mimetype]
871 mimetype, disposition = raw_mimetype_mapping[mimetype]
855 except KeyError:
872 except KeyError:
856 # we don't know anything special about this, handle it safely
873 # we don't know anything special about this, handle it safely
857 if file_node.is_binary:
874 if file_node.is_binary:
858 # do same as download raw for binary files
875 # do same as download raw for binary files
859 mimetype, disposition = 'application/octet-stream', 'attachment'
876 mimetype, disposition = 'application/octet-stream', 'attachment'
860 else:
877 else:
861 # do not just use the original mimetype, but force text/plain,
878 # do not just use the original mimetype, but force text/plain,
862 # otherwise it would serve text/html and that might be unsafe.
879 # otherwise it would serve text/html and that might be unsafe.
863 # Note: underlying vcs library fakes text/plain mimetype if the
880 # Note: underlying vcs library fakes text/plain mimetype if the
864 # mimetype can not be determined and it thinks it is not
881 # mimetype can not be determined and it thinks it is not
865 # binary.This might lead to erroneous text display in some
882 # binary.This might lead to erroneous text display in some
866 # cases, but helps in other cases, like with text files
883 # cases, but helps in other cases, like with text files
867 # without extension.
884 # without extension.
868 mimetype, disposition = 'text/plain', 'inline'
885 mimetype, disposition = 'text/plain', 'inline'
869
886
870 if disposition == 'attachment':
887 if disposition == 'attachment':
871 disposition = self._get_attachement_headers(f_path)
888 disposition = self._get_attachement_headers(f_path)
872
889
873 stream_content = file_node.stream_bytes()
890 stream_content = file_node.stream_bytes()
874
891
875 response = Response(app_iter=stream_content)
892 response = Response(app_iter=stream_content)
876 response.content_disposition = disposition
893 response.content_disposition = disposition
877 response.content_type = mimetype
894 response.content_type = mimetype
878
895
879 charset = self._get_default_encoding(c)
896 charset = self._get_default_encoding(c)
880 if charset:
897 if charset:
881 response.charset = charset
898 response.charset = charset
882
899
883 return response
900 return response
884
901
885 @LoginRequired()
902 @LoginRequired()
886 @HasRepoPermissionAnyDecorator(
903 @HasRepoPermissionAnyDecorator(
887 'repository.read', 'repository.write', 'repository.admin')
904 'repository.read', 'repository.write', 'repository.admin')
888 @view_config(
905 @view_config(
889 route_name='repo_file_download', request_method='GET',
906 route_name='repo_file_download', request_method='GET',
890 renderer=None)
907 renderer=None)
891 @view_config(
908 @view_config(
892 route_name='repo_file_download:legacy', request_method='GET',
909 route_name='repo_file_download:legacy', request_method='GET',
893 renderer=None)
910 renderer=None)
894 def repo_file_download(self):
911 def repo_file_download(self):
895 c = self.load_default_context()
912 c = self.load_default_context()
896
913
897 commit_id, f_path = self._get_commit_and_path()
914 commit_id, f_path = self._get_commit_and_path()
898 commit = self._get_commit_or_redirect(commit_id)
915 commit = self._get_commit_or_redirect(commit_id)
899 file_node = self._get_filenode_or_redirect(commit, f_path)
916 file_node = self._get_filenode_or_redirect(commit, f_path)
900
917
901 if self.request.GET.get('lf'):
918 if self.request.GET.get('lf'):
902 # only if lf get flag is passed, we download this file
919 # only if lf get flag is passed, we download this file
903 # as LFS/Largefile
920 # as LFS/Largefile
904 lf_node = file_node.get_largefile_node()
921 lf_node = file_node.get_largefile_node()
905 if lf_node:
922 if lf_node:
906 # overwrite our pointer with the REAL large-file
923 # overwrite our pointer with the REAL large-file
907 file_node = lf_node
924 file_node = lf_node
908
925
909 disposition = self._get_attachement_headers(f_path)
926 disposition = self._get_attachement_headers(f_path)
910
927
911 stream_content = file_node.stream_bytes()
928 stream_content = file_node.stream_bytes()
912
929
913 response = Response(app_iter=stream_content)
930 response = Response(app_iter=stream_content)
914 response.content_disposition = disposition
931 response.content_disposition = disposition
915 response.content_type = file_node.mimetype
932 response.content_type = file_node.mimetype
916
933
917 charset = self._get_default_encoding(c)
934 charset = self._get_default_encoding(c)
918 if charset:
935 if charset:
919 response.charset = charset
936 response.charset = charset
920
937
921 return response
938 return response
922
939
923 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
940 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
924
941
925 cache_seconds = safe_int(
942 cache_seconds = safe_int(
926 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
943 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
927 cache_on = cache_seconds > 0
944 cache_on = cache_seconds > 0
928 log.debug(
945 log.debug(
929 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
946 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
930 'with caching: %s[TTL: %ss]' % (
947 'with caching: %s[TTL: %ss]' % (
931 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
948 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
932
949
933 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
950 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
934 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
951 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
935
952
936 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
953 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
937 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
954 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
938 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
955 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
939 _repo_id, commit_id, f_path)
956 _repo_id, commit_id, f_path)
940 try:
957 try:
941 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
958 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
942 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
959 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
943 log.exception(safe_str(e))
960 log.exception(safe_str(e))
944 h.flash(safe_str(h.escape(e)), category='error')
961 h.flash(safe_str(h.escape(e)), category='error')
945 raise HTTPFound(h.route_path(
962 raise HTTPFound(h.route_path(
946 'repo_files', repo_name=self.db_repo_name,
963 'repo_files', repo_name=self.db_repo_name,
947 commit_id='tip', f_path='/'))
964 commit_id='tip', f_path='/'))
948
965
949 return _d + _f
966 return _d + _f
950
967
951 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
968 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
952 commit_id, f_path)
969 commit_id, f_path)
953 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
970 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
954
971
955 @LoginRequired()
972 @LoginRequired()
956 @HasRepoPermissionAnyDecorator(
973 @HasRepoPermissionAnyDecorator(
957 'repository.read', 'repository.write', 'repository.admin')
974 'repository.read', 'repository.write', 'repository.admin')
958 @view_config(
975 @view_config(
959 route_name='repo_files_nodelist', request_method='GET',
976 route_name='repo_files_nodelist', request_method='GET',
960 renderer='json_ext', xhr=True)
977 renderer='json_ext', xhr=True)
961 def repo_nodelist(self):
978 def repo_nodelist(self):
962 self.load_default_context()
979 self.load_default_context()
963
980
964 commit_id, f_path = self._get_commit_and_path()
981 commit_id, f_path = self._get_commit_and_path()
965 commit = self._get_commit_or_redirect(commit_id)
982 commit = self._get_commit_or_redirect(commit_id)
966
983
967 metadata = self._get_nodelist_at_commit(
984 metadata = self._get_nodelist_at_commit(
968 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
985 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
969 return {'nodes': metadata}
986 return {'nodes': metadata}
970
987
971 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
988 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
972 items = []
989 items = []
973 for name, commit_id in branches_or_tags.items():
990 for name, commit_id in branches_or_tags.items():
974 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
991 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
975 items.append((sym_ref, name, ref_type))
992 items.append((sym_ref, name, ref_type))
976 return items
993 return items
977
994
978 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
995 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
979 return commit_id
996 return commit_id
980
997
981 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
998 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
982 return commit_id
999 return commit_id
983
1000
984 # NOTE(dan): old code we used in "diff" mode compare
1001 # NOTE(dan): old code we used in "diff" mode compare
985 new_f_path = vcspath.join(name, f_path)
1002 new_f_path = vcspath.join(name, f_path)
986 return u'%s@%s' % (new_f_path, commit_id)
1003 return u'%s@%s' % (new_f_path, commit_id)
987
1004
988 def _get_node_history(self, commit_obj, f_path, commits=None):
1005 def _get_node_history(self, commit_obj, f_path, commits=None):
989 """
1006 """
990 get commit history for given node
1007 get commit history for given node
991
1008
992 :param commit_obj: commit to calculate history
1009 :param commit_obj: commit to calculate history
993 :param f_path: path for node to calculate history for
1010 :param f_path: path for node to calculate history for
994 :param commits: if passed don't calculate history and take
1011 :param commits: if passed don't calculate history and take
995 commits defined in this list
1012 commits defined in this list
996 """
1013 """
997 _ = self.request.translate
1014 _ = self.request.translate
998
1015
999 # calculate history based on tip
1016 # calculate history based on tip
1000 tip = self.rhodecode_vcs_repo.get_commit()
1017 tip = self.rhodecode_vcs_repo.get_commit()
1001 if commits is None:
1018 if commits is None:
1002 pre_load = ["author", "branch"]
1019 pre_load = ["author", "branch"]
1003 try:
1020 try:
1004 commits = tip.get_path_history(f_path, pre_load=pre_load)
1021 commits = tip.get_path_history(f_path, pre_load=pre_load)
1005 except (NodeDoesNotExistError, CommitError):
1022 except (NodeDoesNotExistError, CommitError):
1006 # this node is not present at tip!
1023 # this node is not present at tip!
1007 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1024 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1008
1025
1009 history = []
1026 history = []
1010 commits_group = ([], _("Changesets"))
1027 commits_group = ([], _("Changesets"))
1011 for commit in commits:
1028 for commit in commits:
1012 branch = ' (%s)' % commit.branch if commit.branch else ''
1029 branch = ' (%s)' % commit.branch if commit.branch else ''
1013 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1030 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1014 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1031 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1015 history.append(commits_group)
1032 history.append(commits_group)
1016
1033
1017 symbolic_reference = self._symbolic_reference
1034 symbolic_reference = self._symbolic_reference
1018
1035
1019 if self.rhodecode_vcs_repo.alias == 'svn':
1036 if self.rhodecode_vcs_repo.alias == 'svn':
1020 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1037 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1021 f_path, self.rhodecode_vcs_repo)
1038 f_path, self.rhodecode_vcs_repo)
1022 if adjusted_f_path != f_path:
1039 if adjusted_f_path != f_path:
1023 log.debug(
1040 log.debug(
1024 'Recognized svn tag or branch in file "%s", using svn '
1041 'Recognized svn tag or branch in file "%s", using svn '
1025 'specific symbolic references', f_path)
1042 'specific symbolic references', f_path)
1026 f_path = adjusted_f_path
1043 f_path = adjusted_f_path
1027 symbolic_reference = self._symbolic_reference_svn
1044 symbolic_reference = self._symbolic_reference_svn
1028
1045
1029 branches = self._create_references(
1046 branches = self._create_references(
1030 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1047 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1031 branches_group = (branches, _("Branches"))
1048 branches_group = (branches, _("Branches"))
1032
1049
1033 tags = self._create_references(
1050 tags = self._create_references(
1034 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1051 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1035 tags_group = (tags, _("Tags"))
1052 tags_group = (tags, _("Tags"))
1036
1053
1037 history.append(branches_group)
1054 history.append(branches_group)
1038 history.append(tags_group)
1055 history.append(tags_group)
1039
1056
1040 return history, commits
1057 return history, commits
1041
1058
1042 @LoginRequired()
1059 @LoginRequired()
1043 @HasRepoPermissionAnyDecorator(
1060 @HasRepoPermissionAnyDecorator(
1044 'repository.read', 'repository.write', 'repository.admin')
1061 'repository.read', 'repository.write', 'repository.admin')
1045 @view_config(
1062 @view_config(
1046 route_name='repo_file_history', request_method='GET',
1063 route_name='repo_file_history', request_method='GET',
1047 renderer='json_ext')
1064 renderer='json_ext')
1048 def repo_file_history(self):
1065 def repo_file_history(self):
1049 self.load_default_context()
1066 self.load_default_context()
1050
1067
1051 commit_id, f_path = self._get_commit_and_path()
1068 commit_id, f_path = self._get_commit_and_path()
1052 commit = self._get_commit_or_redirect(commit_id)
1069 commit = self._get_commit_or_redirect(commit_id)
1053 file_node = self._get_filenode_or_redirect(commit, f_path)
1070 file_node = self._get_filenode_or_redirect(commit, f_path)
1054
1071
1055 if file_node.is_file():
1072 if file_node.is_file():
1056 file_history, _hist = self._get_node_history(commit, f_path)
1073 file_history, _hist = self._get_node_history(commit, f_path)
1057
1074
1058 res = []
1075 res = []
1059 for section_items, section in file_history:
1076 for section_items, section in file_history:
1060 items = []
1077 items = []
1061 for obj_id, obj_text, obj_type in section_items:
1078 for obj_id, obj_text, obj_type in section_items:
1062 at_rev = ''
1079 at_rev = ''
1063 if obj_type in ['branch', 'bookmark', 'tag']:
1080 if obj_type in ['branch', 'bookmark', 'tag']:
1064 at_rev = obj_text
1081 at_rev = obj_text
1065 entry = {
1082 entry = {
1066 'id': obj_id,
1083 'id': obj_id,
1067 'text': obj_text,
1084 'text': obj_text,
1068 'type': obj_type,
1085 'type': obj_type,
1069 'at_rev': at_rev
1086 'at_rev': at_rev
1070 }
1087 }
1071
1088
1072 items.append(entry)
1089 items.append(entry)
1073
1090
1074 res.append({
1091 res.append({
1075 'text': section,
1092 'text': section,
1076 'children': items
1093 'children': items
1077 })
1094 })
1078
1095
1079 data = {
1096 data = {
1080 'more': False,
1097 'more': False,
1081 'results': res
1098 'results': res
1082 }
1099 }
1083 return data
1100 return data
1084
1101
1085 log.warning('Cannot fetch history for directory')
1102 log.warning('Cannot fetch history for directory')
1086 raise HTTPBadRequest()
1103 raise HTTPBadRequest()
1087
1104
1088 @LoginRequired()
1105 @LoginRequired()
1089 @HasRepoPermissionAnyDecorator(
1106 @HasRepoPermissionAnyDecorator(
1090 'repository.read', 'repository.write', 'repository.admin')
1107 'repository.read', 'repository.write', 'repository.admin')
1091 @view_config(
1108 @view_config(
1092 route_name='repo_file_authors', request_method='GET',
1109 route_name='repo_file_authors', request_method='GET',
1093 renderer='rhodecode:templates/files/file_authors_box.mako')
1110 renderer='rhodecode:templates/files/file_authors_box.mako')
1094 def repo_file_authors(self):
1111 def repo_file_authors(self):
1095 c = self.load_default_context()
1112 c = self.load_default_context()
1096
1113
1097 commit_id, f_path = self._get_commit_and_path()
1114 commit_id, f_path = self._get_commit_and_path()
1098 commit = self._get_commit_or_redirect(commit_id)
1115 commit = self._get_commit_or_redirect(commit_id)
1099 file_node = self._get_filenode_or_redirect(commit, f_path)
1116 file_node = self._get_filenode_or_redirect(commit, f_path)
1100
1117
1101 if not file_node.is_file():
1118 if not file_node.is_file():
1102 raise HTTPBadRequest()
1119 raise HTTPBadRequest()
1103
1120
1104 c.file_last_commit = file_node.last_commit
1121 c.file_last_commit = file_node.last_commit
1105 if self.request.GET.get('annotate') == '1':
1122 if self.request.GET.get('annotate') == '1':
1106 # use _hist from annotation if annotation mode is on
1123 # use _hist from annotation if annotation mode is on
1107 commit_ids = set(x[1] for x in file_node.annotate)
1124 commit_ids = set(x[1] for x in file_node.annotate)
1108 _hist = (
1125 _hist = (
1109 self.rhodecode_vcs_repo.get_commit(commit_id)
1126 self.rhodecode_vcs_repo.get_commit(commit_id)
1110 for commit_id in commit_ids)
1127 for commit_id in commit_ids)
1111 else:
1128 else:
1112 _f_history, _hist = self._get_node_history(commit, f_path)
1129 _f_history, _hist = self._get_node_history(commit, f_path)
1113 c.file_author = False
1130 c.file_author = False
1114
1131
1115 unique = collections.OrderedDict()
1132 unique = collections.OrderedDict()
1116 for commit in _hist:
1133 for commit in _hist:
1117 author = commit.author
1134 author = commit.author
1118 if author not in unique:
1135 if author not in unique:
1119 unique[commit.author] = [
1136 unique[commit.author] = [
1120 h.email(author),
1137 h.email(author),
1121 h.person(author, 'username_or_name_or_email'),
1138 h.person(author, 'username_or_name_or_email'),
1122 1 # counter
1139 1 # counter
1123 ]
1140 ]
1124
1141
1125 else:
1142 else:
1126 # increase counter
1143 # increase counter
1127 unique[commit.author][2] += 1
1144 unique[commit.author][2] += 1
1128
1145
1129 c.authors = [val for val in unique.values()]
1146 c.authors = [val for val in unique.values()]
1130
1147
1131 return self._get_template_context(c)
1148 return self._get_template_context(c)
1132
1149
1133 @LoginRequired()
1150 @LoginRequired()
1134 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1151 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1135 @view_config(
1152 @view_config(
1136 route_name='repo_files_check_head', request_method='POST',
1153 route_name='repo_files_check_head', request_method='POST',
1137 renderer='json_ext', xhr=True)
1154 renderer='json_ext', xhr=True)
1138 def repo_files_check_head(self):
1155 def repo_files_check_head(self):
1139 self.load_default_context()
1156 self.load_default_context()
1140
1157
1141 commit_id, f_path = self._get_commit_and_path()
1158 commit_id, f_path = self._get_commit_and_path()
1142 _branch_name, _sha_commit_id, is_head = \
1159 _branch_name, _sha_commit_id, is_head = \
1143 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1160 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1144 landing_ref=self.db_repo.landing_ref_name)
1161 landing_ref=self.db_repo.landing_ref_name)
1145
1162
1146 new_path = self.request.POST.get('path')
1163 new_path = self.request.POST.get('path')
1147 operation = self.request.POST.get('operation')
1164 operation = self.request.POST.get('operation')
1148 path_exist = ''
1165 path_exist = ''
1149
1166
1150 if new_path and operation in ['create', 'upload']:
1167 if new_path and operation in ['create', 'upload']:
1151 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1168 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1152 try:
1169 try:
1153 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1170 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1154 # NOTE(dan): construct whole path without leading /
1171 # NOTE(dan): construct whole path without leading /
1155 file_node = commit_obj.get_node(new_f_path)
1172 file_node = commit_obj.get_node(new_f_path)
1156 if file_node is not None:
1173 if file_node is not None:
1157 path_exist = new_f_path
1174 path_exist = new_f_path
1158 except EmptyRepositoryError:
1175 except EmptyRepositoryError:
1159 pass
1176 pass
1160 except Exception:
1177 except Exception:
1161 pass
1178 pass
1162
1179
1163 return {
1180 return {
1164 'branch': _branch_name,
1181 'branch': _branch_name,
1165 'sha': _sha_commit_id,
1182 'sha': _sha_commit_id,
1166 'is_head': is_head,
1183 'is_head': is_head,
1167 'path_exists': path_exist
1184 'path_exists': path_exist
1168 }
1185 }
1169
1186
1170 @LoginRequired()
1187 @LoginRequired()
1171 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1188 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1172 @view_config(
1189 @view_config(
1173 route_name='repo_files_remove_file', request_method='GET',
1190 route_name='repo_files_remove_file', request_method='GET',
1174 renderer='rhodecode:templates/files/files_delete.mako')
1191 renderer='rhodecode:templates/files/files_delete.mako')
1175 def repo_files_remove_file(self):
1192 def repo_files_remove_file(self):
1176 _ = self.request.translate
1193 _ = self.request.translate
1177 c = self.load_default_context()
1194 c = self.load_default_context()
1178 commit_id, f_path = self._get_commit_and_path()
1195 commit_id, f_path = self._get_commit_and_path()
1179
1196
1180 self._ensure_not_locked()
1197 self._ensure_not_locked()
1181 _branch_name, _sha_commit_id, is_head = \
1198 _branch_name, _sha_commit_id, is_head = \
1182 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1199 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1183 landing_ref=self.db_repo.landing_ref_name)
1200 landing_ref=self.db_repo.landing_ref_name)
1184
1201
1185 self.forbid_non_head(is_head, f_path)
1202 self.forbid_non_head(is_head, f_path)
1186 self.check_branch_permission(_branch_name)
1203 self.check_branch_permission(_branch_name)
1187
1204
1188 c.commit = self._get_commit_or_redirect(commit_id)
1205 c.commit = self._get_commit_or_redirect(commit_id)
1189 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1206 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1190
1207
1191 c.default_message = _(
1208 c.default_message = _(
1192 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1209 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1193 c.f_path = f_path
1210 c.f_path = f_path
1194
1211
1195 return self._get_template_context(c)
1212 return self._get_template_context(c)
1196
1213
1197 @LoginRequired()
1214 @LoginRequired()
1198 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1215 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1199 @CSRFRequired()
1216 @CSRFRequired()
1200 @view_config(
1217 @view_config(
1201 route_name='repo_files_delete_file', request_method='POST',
1218 route_name='repo_files_delete_file', request_method='POST',
1202 renderer=None)
1219 renderer=None)
1203 def repo_files_delete_file(self):
1220 def repo_files_delete_file(self):
1204 _ = self.request.translate
1221 _ = self.request.translate
1205
1222
1206 c = self.load_default_context()
1223 c = self.load_default_context()
1207 commit_id, f_path = self._get_commit_and_path()
1224 commit_id, f_path = self._get_commit_and_path()
1208
1225
1209 self._ensure_not_locked()
1226 self._ensure_not_locked()
1210 _branch_name, _sha_commit_id, is_head = \
1227 _branch_name, _sha_commit_id, is_head = \
1211 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1228 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1212 landing_ref=self.db_repo.landing_ref_name)
1229 landing_ref=self.db_repo.landing_ref_name)
1213
1230
1214 self.forbid_non_head(is_head, f_path)
1231 self.forbid_non_head(is_head, f_path)
1215 self.check_branch_permission(_branch_name)
1232 self.check_branch_permission(_branch_name)
1216
1233
1217 c.commit = self._get_commit_or_redirect(commit_id)
1234 c.commit = self._get_commit_or_redirect(commit_id)
1218 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1235 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1219
1236
1220 c.default_message = _(
1237 c.default_message = _(
1221 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1238 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1222 c.f_path = f_path
1239 c.f_path = f_path
1223 node_path = f_path
1240 node_path = f_path
1224 author = self._rhodecode_db_user.full_contact
1241 author = self._rhodecode_db_user.full_contact
1225 message = self.request.POST.get('message') or c.default_message
1242 message = self.request.POST.get('message') or c.default_message
1226 try:
1243 try:
1227 nodes = {
1244 nodes = {
1228 node_path: {
1245 node_path: {
1229 'content': ''
1246 'content': ''
1230 }
1247 }
1231 }
1248 }
1232 ScmModel().delete_nodes(
1249 ScmModel().delete_nodes(
1233 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1250 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1234 message=message,
1251 message=message,
1235 nodes=nodes,
1252 nodes=nodes,
1236 parent_commit=c.commit,
1253 parent_commit=c.commit,
1237 author=author,
1254 author=author,
1238 )
1255 )
1239
1256
1240 h.flash(
1257 h.flash(
1241 _('Successfully deleted file `{}`').format(
1258 _('Successfully deleted file `{}`').format(
1242 h.escape(f_path)), category='success')
1259 h.escape(f_path)), category='success')
1243 except Exception:
1260 except Exception:
1244 log.exception('Error during commit operation')
1261 log.exception('Error during commit operation')
1245 h.flash(_('Error occurred during commit'), category='error')
1262 h.flash(_('Error occurred during commit'), category='error')
1246 raise HTTPFound(
1263 raise HTTPFound(
1247 h.route_path('repo_commit', repo_name=self.db_repo_name,
1264 h.route_path('repo_commit', repo_name=self.db_repo_name,
1248 commit_id='tip'))
1265 commit_id='tip'))
1249
1266
1250 @LoginRequired()
1267 @LoginRequired()
1251 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1268 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1252 @view_config(
1269 @view_config(
1253 route_name='repo_files_edit_file', request_method='GET',
1270 route_name='repo_files_edit_file', request_method='GET',
1254 renderer='rhodecode:templates/files/files_edit.mako')
1271 renderer='rhodecode:templates/files/files_edit.mako')
1255 def repo_files_edit_file(self):
1272 def repo_files_edit_file(self):
1256 _ = self.request.translate
1273 _ = self.request.translate
1257 c = self.load_default_context()
1274 c = self.load_default_context()
1258 commit_id, f_path = self._get_commit_and_path()
1275 commit_id, f_path = self._get_commit_and_path()
1259
1276
1260 self._ensure_not_locked()
1277 self._ensure_not_locked()
1261 _branch_name, _sha_commit_id, is_head = \
1278 _branch_name, _sha_commit_id, is_head = \
1262 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1279 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1263 landing_ref=self.db_repo.landing_ref_name)
1280 landing_ref=self.db_repo.landing_ref_name)
1264
1281
1265 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1282 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1266 self.check_branch_permission(_branch_name, commit_id=commit_id)
1283 self.check_branch_permission(_branch_name, commit_id=commit_id)
1267
1284
1268 c.commit = self._get_commit_or_redirect(commit_id)
1285 c.commit = self._get_commit_or_redirect(commit_id)
1269 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1286 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1270
1287
1271 if c.file.is_binary:
1288 if c.file.is_binary:
1272 files_url = h.route_path(
1289 files_url = h.route_path(
1273 'repo_files',
1290 'repo_files',
1274 repo_name=self.db_repo_name,
1291 repo_name=self.db_repo_name,
1275 commit_id=c.commit.raw_id, f_path=f_path)
1292 commit_id=c.commit.raw_id, f_path=f_path)
1276 raise HTTPFound(files_url)
1293 raise HTTPFound(files_url)
1277
1294
1278 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1295 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1279 c.f_path = f_path
1296 c.f_path = f_path
1280
1297
1281 return self._get_template_context(c)
1298 return self._get_template_context(c)
1282
1299
1283 @LoginRequired()
1300 @LoginRequired()
1284 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1301 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1285 @CSRFRequired()
1302 @CSRFRequired()
1286 @view_config(
1303 @view_config(
1287 route_name='repo_files_update_file', request_method='POST',
1304 route_name='repo_files_update_file', request_method='POST',
1288 renderer=None)
1305 renderer=None)
1289 def repo_files_update_file(self):
1306 def repo_files_update_file(self):
1290 _ = self.request.translate
1307 _ = self.request.translate
1291 c = self.load_default_context()
1308 c = self.load_default_context()
1292 commit_id, f_path = self._get_commit_and_path()
1309 commit_id, f_path = self._get_commit_and_path()
1293
1310
1294 self._ensure_not_locked()
1311 self._ensure_not_locked()
1295
1312
1296 c.commit = self._get_commit_or_redirect(commit_id)
1313 c.commit = self._get_commit_or_redirect(commit_id)
1297 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1314 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1298
1315
1299 if c.file.is_binary:
1316 if c.file.is_binary:
1300 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1317 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1301 commit_id=c.commit.raw_id, f_path=f_path))
1318 commit_id=c.commit.raw_id, f_path=f_path))
1302
1319
1303 _branch_name, _sha_commit_id, is_head = \
1320 _branch_name, _sha_commit_id, is_head = \
1304 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1321 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1305 landing_ref=self.db_repo.landing_ref_name)
1322 landing_ref=self.db_repo.landing_ref_name)
1306
1323
1307 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1324 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1308 self.check_branch_permission(_branch_name, commit_id=commit_id)
1325 self.check_branch_permission(_branch_name, commit_id=commit_id)
1309
1326
1310 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1327 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1311 c.f_path = f_path
1328 c.f_path = f_path
1312
1329
1313 old_content = c.file.content
1330 old_content = c.file.content
1314 sl = old_content.splitlines(1)
1331 sl = old_content.splitlines(1)
1315 first_line = sl[0] if sl else ''
1332 first_line = sl[0] if sl else ''
1316
1333
1317 r_post = self.request.POST
1334 r_post = self.request.POST
1318 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1335 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1319 line_ending_mode = detect_mode(first_line, 0)
1336 line_ending_mode = detect_mode(first_line, 0)
1320 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1337 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1321
1338
1322 message = r_post.get('message') or c.default_message
1339 message = r_post.get('message') or c.default_message
1323 org_node_path = c.file.unicode_path
1340 org_node_path = c.file.unicode_path
1324 filename = r_post['filename']
1341 filename = r_post['filename']
1325
1342
1326 root_path = c.file.dir_path
1343 root_path = c.file.dir_path
1327 pure_path = self.create_pure_path(root_path, filename)
1344 pure_path = self.create_pure_path(root_path, filename)
1328 node_path = safe_unicode(bytes(pure_path))
1345 node_path = safe_unicode(bytes(pure_path))
1329
1346
1330 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1347 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1331 commit_id=commit_id)
1348 commit_id=commit_id)
1332 if content == old_content and node_path == org_node_path:
1349 if content == old_content and node_path == org_node_path:
1333 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1350 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1334 category='warning')
1351 category='warning')
1335 raise HTTPFound(default_redirect_url)
1352 raise HTTPFound(default_redirect_url)
1336
1353
1337 try:
1354 try:
1338 mapping = {
1355 mapping = {
1339 org_node_path: {
1356 org_node_path: {
1340 'org_filename': org_node_path,
1357 'org_filename': org_node_path,
1341 'filename': node_path,
1358 'filename': node_path,
1342 'content': content,
1359 'content': content,
1343 'lexer': '',
1360 'lexer': '',
1344 'op': 'mod',
1361 'op': 'mod',
1345 'mode': c.file.mode
1362 'mode': c.file.mode
1346 }
1363 }
1347 }
1364 }
1348
1365
1349 commit = ScmModel().update_nodes(
1366 commit = ScmModel().update_nodes(
1350 user=self._rhodecode_db_user.user_id,
1367 user=self._rhodecode_db_user.user_id,
1351 repo=self.db_repo,
1368 repo=self.db_repo,
1352 message=message,
1369 message=message,
1353 nodes=mapping,
1370 nodes=mapping,
1354 parent_commit=c.commit,
1371 parent_commit=c.commit,
1355 )
1372 )
1356
1373
1357 h.flash(_('Successfully committed changes to file `{}`').format(
1374 h.flash(_('Successfully committed changes to file `{}`').format(
1358 h.escape(f_path)), category='success')
1375 h.escape(f_path)), category='success')
1359 default_redirect_url = h.route_path(
1376 default_redirect_url = h.route_path(
1360 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1377 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1361
1378
1362 except Exception:
1379 except Exception:
1363 log.exception('Error occurred during commit')
1380 log.exception('Error occurred during commit')
1364 h.flash(_('Error occurred during commit'), category='error')
1381 h.flash(_('Error occurred during commit'), category='error')
1365
1382
1366 raise HTTPFound(default_redirect_url)
1383 raise HTTPFound(default_redirect_url)
1367
1384
1368 @LoginRequired()
1385 @LoginRequired()
1369 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1386 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1370 @view_config(
1387 @view_config(
1371 route_name='repo_files_add_file', request_method='GET',
1388 route_name='repo_files_add_file', request_method='GET',
1372 renderer='rhodecode:templates/files/files_add.mako')
1389 renderer='rhodecode:templates/files/files_add.mako')
1373 @view_config(
1390 @view_config(
1374 route_name='repo_files_upload_file', request_method='GET',
1391 route_name='repo_files_upload_file', request_method='GET',
1375 renderer='rhodecode:templates/files/files_upload.mako')
1392 renderer='rhodecode:templates/files/files_upload.mako')
1376 def repo_files_add_file(self):
1393 def repo_files_add_file(self):
1377 _ = self.request.translate
1394 _ = self.request.translate
1378 c = self.load_default_context()
1395 c = self.load_default_context()
1379 commit_id, f_path = self._get_commit_and_path()
1396 commit_id, f_path = self._get_commit_and_path()
1380
1397
1381 self._ensure_not_locked()
1398 self._ensure_not_locked()
1382
1399
1383 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1400 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1384 if c.commit is None:
1401 if c.commit is None:
1385 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1402 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1386
1403
1387 if self.rhodecode_vcs_repo.is_empty():
1404 if self.rhodecode_vcs_repo.is_empty():
1388 # for empty repository we cannot check for current branch, we rely on
1405 # for empty repository we cannot check for current branch, we rely on
1389 # c.commit.branch instead
1406 # c.commit.branch instead
1390 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1407 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1391 else:
1408 else:
1392 _branch_name, _sha_commit_id, is_head = \
1409 _branch_name, _sha_commit_id, is_head = \
1393 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1410 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1394 landing_ref=self.db_repo.landing_ref_name)
1411 landing_ref=self.db_repo.landing_ref_name)
1395
1412
1396 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1413 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1397 self.check_branch_permission(_branch_name, commit_id=commit_id)
1414 self.check_branch_permission(_branch_name, commit_id=commit_id)
1398
1415
1399 c.default_message = (_('Added file via RhodeCode Enterprise'))
1416 c.default_message = (_('Added file via RhodeCode Enterprise'))
1400 c.f_path = f_path.lstrip('/') # ensure not relative path
1417 c.f_path = f_path.lstrip('/') # ensure not relative path
1401
1418
1402 return self._get_template_context(c)
1419 return self._get_template_context(c)
1403
1420
1404 @LoginRequired()
1421 @LoginRequired()
1405 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1422 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1406 @CSRFRequired()
1423 @CSRFRequired()
1407 @view_config(
1424 @view_config(
1408 route_name='repo_files_create_file', request_method='POST',
1425 route_name='repo_files_create_file', request_method='POST',
1409 renderer=None)
1426 renderer=None)
1410 def repo_files_create_file(self):
1427 def repo_files_create_file(self):
1411 _ = self.request.translate
1428 _ = self.request.translate
1412 c = self.load_default_context()
1429 c = self.load_default_context()
1413 commit_id, f_path = self._get_commit_and_path()
1430 commit_id, f_path = self._get_commit_and_path()
1414
1431
1415 self._ensure_not_locked()
1432 self._ensure_not_locked()
1416
1433
1417 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1434 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1418 if c.commit is None:
1435 if c.commit is None:
1419 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1436 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1420
1437
1421 # calculate redirect URL
1438 # calculate redirect URL
1422 if self.rhodecode_vcs_repo.is_empty():
1439 if self.rhodecode_vcs_repo.is_empty():
1423 default_redirect_url = h.route_path(
1440 default_redirect_url = h.route_path(
1424 'repo_summary', repo_name=self.db_repo_name)
1441 'repo_summary', repo_name=self.db_repo_name)
1425 else:
1442 else:
1426 default_redirect_url = h.route_path(
1443 default_redirect_url = h.route_path(
1427 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1444 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1428
1445
1429 if self.rhodecode_vcs_repo.is_empty():
1446 if self.rhodecode_vcs_repo.is_empty():
1430 # for empty repository we cannot check for current branch, we rely on
1447 # for empty repository we cannot check for current branch, we rely on
1431 # c.commit.branch instead
1448 # c.commit.branch instead
1432 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1449 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1433 else:
1450 else:
1434 _branch_name, _sha_commit_id, is_head = \
1451 _branch_name, _sha_commit_id, is_head = \
1435 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1452 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1436 landing_ref=self.db_repo.landing_ref_name)
1453 landing_ref=self.db_repo.landing_ref_name)
1437
1454
1438 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1455 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1439 self.check_branch_permission(_branch_name, commit_id=commit_id)
1456 self.check_branch_permission(_branch_name, commit_id=commit_id)
1440
1457
1441 c.default_message = (_('Added file via RhodeCode Enterprise'))
1458 c.default_message = (_('Added file via RhodeCode Enterprise'))
1442 c.f_path = f_path
1459 c.f_path = f_path
1443
1460
1444 r_post = self.request.POST
1461 r_post = self.request.POST
1445 message = r_post.get('message') or c.default_message
1462 message = r_post.get('message') or c.default_message
1446 filename = r_post.get('filename')
1463 filename = r_post.get('filename')
1447 unix_mode = 0
1464 unix_mode = 0
1448 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1465 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1449
1466
1450 if not filename:
1467 if not filename:
1451 # If there's no commit, redirect to repo summary
1468 # If there's no commit, redirect to repo summary
1452 if type(c.commit) is EmptyCommit:
1469 if type(c.commit) is EmptyCommit:
1453 redirect_url = h.route_path(
1470 redirect_url = h.route_path(
1454 'repo_summary', repo_name=self.db_repo_name)
1471 'repo_summary', repo_name=self.db_repo_name)
1455 else:
1472 else:
1456 redirect_url = default_redirect_url
1473 redirect_url = default_redirect_url
1457 h.flash(_('No filename specified'), category='warning')
1474 h.flash(_('No filename specified'), category='warning')
1458 raise HTTPFound(redirect_url)
1475 raise HTTPFound(redirect_url)
1459
1476
1460 root_path = f_path
1477 root_path = f_path
1461 pure_path = self.create_pure_path(root_path, filename)
1478 pure_path = self.create_pure_path(root_path, filename)
1462 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1479 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1463
1480
1464 author = self._rhodecode_db_user.full_contact
1481 author = self._rhodecode_db_user.full_contact
1465 nodes = {
1482 nodes = {
1466 node_path: {
1483 node_path: {
1467 'content': content
1484 'content': content
1468 }
1485 }
1469 }
1486 }
1470
1487
1471 try:
1488 try:
1472
1489
1473 commit = ScmModel().create_nodes(
1490 commit = ScmModel().create_nodes(
1474 user=self._rhodecode_db_user.user_id,
1491 user=self._rhodecode_db_user.user_id,
1475 repo=self.db_repo,
1492 repo=self.db_repo,
1476 message=message,
1493 message=message,
1477 nodes=nodes,
1494 nodes=nodes,
1478 parent_commit=c.commit,
1495 parent_commit=c.commit,
1479 author=author,
1496 author=author,
1480 )
1497 )
1481
1498
1482 h.flash(_('Successfully committed new file `{}`').format(
1499 h.flash(_('Successfully committed new file `{}`').format(
1483 h.escape(node_path)), category='success')
1500 h.escape(node_path)), category='success')
1484
1501
1485 default_redirect_url = h.route_path(
1502 default_redirect_url = h.route_path(
1486 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1503 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1487
1504
1488 except NonRelativePathError:
1505 except NonRelativePathError:
1489 log.exception('Non Relative path found')
1506 log.exception('Non Relative path found')
1490 h.flash(_('The location specified must be a relative path and must not '
1507 h.flash(_('The location specified must be a relative path and must not '
1491 'contain .. in the path'), category='warning')
1508 'contain .. in the path'), category='warning')
1492 raise HTTPFound(default_redirect_url)
1509 raise HTTPFound(default_redirect_url)
1493 except (NodeError, NodeAlreadyExistsError) as e:
1510 except (NodeError, NodeAlreadyExistsError) as e:
1494 h.flash(_(h.escape(e)), category='error')
1511 h.flash(_(h.escape(e)), category='error')
1495 except Exception:
1512 except Exception:
1496 log.exception('Error occurred during commit')
1513 log.exception('Error occurred during commit')
1497 h.flash(_('Error occurred during commit'), category='error')
1514 h.flash(_('Error occurred during commit'), category='error')
1498
1515
1499 raise HTTPFound(default_redirect_url)
1516 raise HTTPFound(default_redirect_url)
1500
1517
1501 @LoginRequired()
1518 @LoginRequired()
1502 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1519 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1503 @CSRFRequired()
1520 @CSRFRequired()
1504 @view_config(
1521 @view_config(
1505 route_name='repo_files_upload_file', request_method='POST',
1522 route_name='repo_files_upload_file', request_method='POST',
1506 renderer='json_ext')
1523 renderer='json_ext')
1507 def repo_files_upload_file(self):
1524 def repo_files_upload_file(self):
1508 _ = self.request.translate
1525 _ = self.request.translate
1509 c = self.load_default_context()
1526 c = self.load_default_context()
1510 commit_id, f_path = self._get_commit_and_path()
1527 commit_id, f_path = self._get_commit_and_path()
1511
1528
1512 self._ensure_not_locked()
1529 self._ensure_not_locked()
1513
1530
1514 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1531 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1515 if c.commit is None:
1532 if c.commit is None:
1516 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1533 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1517
1534
1518 # calculate redirect URL
1535 # calculate redirect URL
1519 if self.rhodecode_vcs_repo.is_empty():
1536 if self.rhodecode_vcs_repo.is_empty():
1520 default_redirect_url = h.route_path(
1537 default_redirect_url = h.route_path(
1521 'repo_summary', repo_name=self.db_repo_name)
1538 'repo_summary', repo_name=self.db_repo_name)
1522 else:
1539 else:
1523 default_redirect_url = h.route_path(
1540 default_redirect_url = h.route_path(
1524 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1541 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1525
1542
1526 if self.rhodecode_vcs_repo.is_empty():
1543 if self.rhodecode_vcs_repo.is_empty():
1527 # for empty repository we cannot check for current branch, we rely on
1544 # for empty repository we cannot check for current branch, we rely on
1528 # c.commit.branch instead
1545 # c.commit.branch instead
1529 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1546 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1530 else:
1547 else:
1531 _branch_name, _sha_commit_id, is_head = \
1548 _branch_name, _sha_commit_id, is_head = \
1532 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1549 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1533 landing_ref=self.db_repo.landing_ref_name)
1550 landing_ref=self.db_repo.landing_ref_name)
1534
1551
1535 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1552 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1536 if error:
1553 if error:
1537 return {
1554 return {
1538 'error': error,
1555 'error': error,
1539 'redirect_url': default_redirect_url
1556 'redirect_url': default_redirect_url
1540 }
1557 }
1541 error = self.check_branch_permission(_branch_name, json_mode=True)
1558 error = self.check_branch_permission(_branch_name, json_mode=True)
1542 if error:
1559 if error:
1543 return {
1560 return {
1544 'error': error,
1561 'error': error,
1545 'redirect_url': default_redirect_url
1562 'redirect_url': default_redirect_url
1546 }
1563 }
1547
1564
1548 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1565 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1549 c.f_path = f_path
1566 c.f_path = f_path
1550
1567
1551 r_post = self.request.POST
1568 r_post = self.request.POST
1552
1569
1553 message = c.default_message
1570 message = c.default_message
1554 user_message = r_post.getall('message')
1571 user_message = r_post.getall('message')
1555 if isinstance(user_message, list) and user_message:
1572 if isinstance(user_message, list) and user_message:
1556 # we take the first from duplicated results if it's not empty
1573 # we take the first from duplicated results if it's not empty
1557 message = user_message[0] if user_message[0] else message
1574 message = user_message[0] if user_message[0] else message
1558
1575
1559 nodes = {}
1576 nodes = {}
1560
1577
1561 for file_obj in r_post.getall('files_upload') or []:
1578 for file_obj in r_post.getall('files_upload') or []:
1562 content = file_obj.file
1579 content = file_obj.file
1563 filename = file_obj.filename
1580 filename = file_obj.filename
1564
1581
1565 root_path = f_path
1582 root_path = f_path
1566 pure_path = self.create_pure_path(root_path, filename)
1583 pure_path = self.create_pure_path(root_path, filename)
1567 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1584 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1568
1585
1569 nodes[node_path] = {
1586 nodes[node_path] = {
1570 'content': content
1587 'content': content
1571 }
1588 }
1572
1589
1573 if not nodes:
1590 if not nodes:
1574 error = 'missing files'
1591 error = 'missing files'
1575 return {
1592 return {
1576 'error': error,
1593 'error': error,
1577 'redirect_url': default_redirect_url
1594 'redirect_url': default_redirect_url
1578 }
1595 }
1579
1596
1580 author = self._rhodecode_db_user.full_contact
1597 author = self._rhodecode_db_user.full_contact
1581
1598
1582 try:
1599 try:
1583 commit = ScmModel().create_nodes(
1600 commit = ScmModel().create_nodes(
1584 user=self._rhodecode_db_user.user_id,
1601 user=self._rhodecode_db_user.user_id,
1585 repo=self.db_repo,
1602 repo=self.db_repo,
1586 message=message,
1603 message=message,
1587 nodes=nodes,
1604 nodes=nodes,
1588 parent_commit=c.commit,
1605 parent_commit=c.commit,
1589 author=author,
1606 author=author,
1590 )
1607 )
1591 if len(nodes) == 1:
1608 if len(nodes) == 1:
1592 flash_message = _('Successfully committed {} new files').format(len(nodes))
1609 flash_message = _('Successfully committed {} new files').format(len(nodes))
1593 else:
1610 else:
1594 flash_message = _('Successfully committed 1 new file')
1611 flash_message = _('Successfully committed 1 new file')
1595
1612
1596 h.flash(flash_message, category='success')
1613 h.flash(flash_message, category='success')
1597
1614
1598 default_redirect_url = h.route_path(
1615 default_redirect_url = h.route_path(
1599 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1616 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1600
1617
1601 except NonRelativePathError:
1618 except NonRelativePathError:
1602 log.exception('Non Relative path found')
1619 log.exception('Non Relative path found')
1603 error = _('The location specified must be a relative path and must not '
1620 error = _('The location specified must be a relative path and must not '
1604 'contain .. in the path')
1621 'contain .. in the path')
1605 h.flash(error, category='warning')
1622 h.flash(error, category='warning')
1606
1623
1607 return {
1624 return {
1608 'error': error,
1625 'error': error,
1609 'redirect_url': default_redirect_url
1626 'redirect_url': default_redirect_url
1610 }
1627 }
1611 except (NodeError, NodeAlreadyExistsError) as e:
1628 except (NodeError, NodeAlreadyExistsError) as e:
1612 error = h.escape(e)
1629 error = h.escape(e)
1613 h.flash(error, category='error')
1630 h.flash(error, category='error')
1614
1631
1615 return {
1632 return {
1616 'error': error,
1633 'error': error,
1617 'redirect_url': default_redirect_url
1634 'redirect_url': default_redirect_url
1618 }
1635 }
1619 except Exception:
1636 except Exception:
1620 log.exception('Error occurred during commit')
1637 log.exception('Error occurred during commit')
1621 error = _('Error occurred during commit')
1638 error = _('Error occurred during commit')
1622 h.flash(error, category='error')
1639 h.flash(error, category='error')
1623 return {
1640 return {
1624 'error': error,
1641 'error': error,
1625 'redirect_url': default_redirect_url
1642 'redirect_url': default_redirect_url
1626 }
1643 }
1627
1644
1628 return {
1645 return {
1629 'error': None,
1646 'error': None,
1630 'redirect_url': default_redirect_url
1647 'redirect_url': default_redirect_url
1631 }
1648 }
@@ -1,1948 +1,1949 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 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 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24 import os
24 import os
25 import re
25 import re
26 import time
26 import time
27 import shutil
27 import shutil
28 import datetime
28 import datetime
29 import fnmatch
29 import fnmatch
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import collections
32 import collections
33 import warnings
33 import warnings
34
34
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36
36
37 from pyramid import compat
37 from pyramid import compat
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.translation import lazy_ugettext
40 from rhodecode.translation import lazy_ugettext
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
42 from rhodecode.lib.vcs import connection
42 from rhodecode.lib.vcs import connection
43 from rhodecode.lib.vcs.utils import author_name, author_email
43 from rhodecode.lib.vcs.utils import author_name, author_email
44 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.conf import settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 RepositoryError)
50 RepositoryError)
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 FILEMODE_DEFAULT = 0o100644
56 FILEMODE_DEFAULT = 0o100644
57 FILEMODE_EXECUTABLE = 0o100755
57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58 EMPTY_COMMIT_ID = '0' * 40
59
59
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61
61
62
62
63 class Reference(_Reference):
63 class Reference(_Reference):
64
64
65 @property
65 @property
66 def branch(self):
66 def branch(self):
67 if self.type == 'branch':
67 if self.type == 'branch':
68 return self.name
68 return self.name
69
69
70 @property
70 @property
71 def bookmark(self):
71 def bookmark(self):
72 if self.type == 'book':
72 if self.type == 'book':
73 return self.name
73 return self.name
74
74
75
75
76 def unicode_to_reference(raw):
76 def unicode_to_reference(raw):
77 """
77 """
78 Convert a unicode (or string) to a reference object.
78 Convert a unicode (or string) to a reference object.
79 If unicode evaluates to False it returns None.
79 If unicode evaluates to False it returns None.
80 """
80 """
81 if raw:
81 if raw:
82 refs = raw.split(':')
82 refs = raw.split(':')
83 return Reference(*refs)
83 return Reference(*refs)
84 else:
84 else:
85 return None
85 return None
86
86
87
87
88 def reference_to_unicode(ref):
88 def reference_to_unicode(ref):
89 """
89 """
90 Convert a reference object to unicode.
90 Convert a reference object to unicode.
91 If reference is None it returns None.
91 If reference is None it returns None.
92 """
92 """
93 if ref:
93 if ref:
94 return u':'.join(ref)
94 return u':'.join(ref)
95 else:
95 else:
96 return None
96 return None
97
97
98
98
99 class MergeFailureReason(object):
99 class MergeFailureReason(object):
100 """
100 """
101 Enumeration with all the reasons why the server side merge could fail.
101 Enumeration with all the reasons why the server side merge could fail.
102
102
103 DO NOT change the number of the reasons, as they may be stored in the
103 DO NOT change the number of the reasons, as they may be stored in the
104 database.
104 database.
105
105
106 Changing the name of a reason is acceptable and encouraged to deprecate old
106 Changing the name of a reason is acceptable and encouraged to deprecate old
107 reasons.
107 reasons.
108 """
108 """
109
109
110 # Everything went well.
110 # Everything went well.
111 NONE = 0
111 NONE = 0
112
112
113 # An unexpected exception was raised. Check the logs for more details.
113 # An unexpected exception was raised. Check the logs for more details.
114 UNKNOWN = 1
114 UNKNOWN = 1
115
115
116 # The merge was not successful, there are conflicts.
116 # The merge was not successful, there are conflicts.
117 MERGE_FAILED = 2
117 MERGE_FAILED = 2
118
118
119 # The merge succeeded but we could not push it to the target repository.
119 # The merge succeeded but we could not push it to the target repository.
120 PUSH_FAILED = 3
120 PUSH_FAILED = 3
121
121
122 # The specified target is not a head in the target repository.
122 # The specified target is not a head in the target repository.
123 TARGET_IS_NOT_HEAD = 4
123 TARGET_IS_NOT_HEAD = 4
124
124
125 # The source repository contains more branches than the target. Pushing
125 # The source repository contains more branches than the target. Pushing
126 # the merge will create additional branches in the target.
126 # the merge will create additional branches in the target.
127 HG_SOURCE_HAS_MORE_BRANCHES = 5
127 HG_SOURCE_HAS_MORE_BRANCHES = 5
128
128
129 # The target reference has multiple heads. That does not allow to correctly
129 # The target reference has multiple heads. That does not allow to correctly
130 # identify the target location. This could only happen for mercurial
130 # identify the target location. This could only happen for mercurial
131 # branches.
131 # branches.
132 HG_TARGET_HAS_MULTIPLE_HEADS = 6
132 HG_TARGET_HAS_MULTIPLE_HEADS = 6
133
133
134 # The target repository is locked
134 # The target repository is locked
135 TARGET_IS_LOCKED = 7
135 TARGET_IS_LOCKED = 7
136
136
137 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
137 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
138 # A involved commit could not be found.
138 # A involved commit could not be found.
139 _DEPRECATED_MISSING_COMMIT = 8
139 _DEPRECATED_MISSING_COMMIT = 8
140
140
141 # The target repo reference is missing.
141 # The target repo reference is missing.
142 MISSING_TARGET_REF = 9
142 MISSING_TARGET_REF = 9
143
143
144 # The source repo reference is missing.
144 # The source repo reference is missing.
145 MISSING_SOURCE_REF = 10
145 MISSING_SOURCE_REF = 10
146
146
147 # The merge was not successful, there are conflicts related to sub
147 # The merge was not successful, there are conflicts related to sub
148 # repositories.
148 # repositories.
149 SUBREPO_MERGE_FAILED = 11
149 SUBREPO_MERGE_FAILED = 11
150
150
151
151
152 class UpdateFailureReason(object):
152 class UpdateFailureReason(object):
153 """
153 """
154 Enumeration with all the reasons why the pull request update could fail.
154 Enumeration with all the reasons why the pull request update could fail.
155
155
156 DO NOT change the number of the reasons, as they may be stored in the
156 DO NOT change the number of the reasons, as they may be stored in the
157 database.
157 database.
158
158
159 Changing the name of a reason is acceptable and encouraged to deprecate old
159 Changing the name of a reason is acceptable and encouraged to deprecate old
160 reasons.
160 reasons.
161 """
161 """
162
162
163 # Everything went well.
163 # Everything went well.
164 NONE = 0
164 NONE = 0
165
165
166 # An unexpected exception was raised. Check the logs for more details.
166 # An unexpected exception was raised. Check the logs for more details.
167 UNKNOWN = 1
167 UNKNOWN = 1
168
168
169 # The pull request is up to date.
169 # The pull request is up to date.
170 NO_CHANGE = 2
170 NO_CHANGE = 2
171
171
172 # The pull request has a reference type that is not supported for update.
172 # The pull request has a reference type that is not supported for update.
173 WRONG_REF_TYPE = 3
173 WRONG_REF_TYPE = 3
174
174
175 # Update failed because the target reference is missing.
175 # Update failed because the target reference is missing.
176 MISSING_TARGET_REF = 4
176 MISSING_TARGET_REF = 4
177
177
178 # Update failed because the source reference is missing.
178 # Update failed because the source reference is missing.
179 MISSING_SOURCE_REF = 5
179 MISSING_SOURCE_REF = 5
180
180
181
181
182 class MergeResponse(object):
182 class MergeResponse(object):
183
183
184 # uses .format(**metadata) for variables
184 # uses .format(**metadata) for variables
185 MERGE_STATUS_MESSAGES = {
185 MERGE_STATUS_MESSAGES = {
186 MergeFailureReason.NONE: lazy_ugettext(
186 MergeFailureReason.NONE: lazy_ugettext(
187 u'This pull request can be automatically merged.'),
187 u'This pull request can be automatically merged.'),
188 MergeFailureReason.UNKNOWN: lazy_ugettext(
188 MergeFailureReason.UNKNOWN: lazy_ugettext(
189 u'This pull request cannot be merged because of an unhandled exception. '
189 u'This pull request cannot be merged because of an unhandled exception. '
190 u'{exception}'),
190 u'{exception}'),
191 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
191 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
192 u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
192 u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
193 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
193 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
194 u'This pull request could not be merged because push to '
194 u'This pull request could not be merged because push to '
195 u'target:`{target}@{merge_commit}` failed.'),
195 u'target:`{target}@{merge_commit}` failed.'),
196 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
196 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
197 u'This pull request cannot be merged because the target '
197 u'This pull request cannot be merged because the target '
198 u'`{target_ref.name}` is not a head.'),
198 u'`{target_ref.name}` is not a head.'),
199 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
199 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
200 u'This pull request cannot be merged because the source contains '
200 u'This pull request cannot be merged because the source contains '
201 u'more branches than the target.'),
201 u'more branches than the target.'),
202 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
202 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
203 u'This pull request cannot be merged because the target `{target_ref.name}` '
203 u'This pull request cannot be merged because the target `{target_ref.name}` '
204 u'has multiple heads: `{heads}`.'),
204 u'has multiple heads: `{heads}`.'),
205 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
205 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
206 u'This pull request cannot be merged because the target repository is '
206 u'This pull request cannot be merged because the target repository is '
207 u'locked by {locked_by}.'),
207 u'locked by {locked_by}.'),
208
208
209 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
209 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
210 u'This pull request cannot be merged because the target '
210 u'This pull request cannot be merged because the target '
211 u'reference `{target_ref.name}` is missing.'),
211 u'reference `{target_ref.name}` is missing.'),
212 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
212 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
213 u'This pull request cannot be merged because the source '
213 u'This pull request cannot be merged because the source '
214 u'reference `{source_ref.name}` is missing.'),
214 u'reference `{source_ref.name}` is missing.'),
215 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
215 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
216 u'This pull request cannot be merged because of conflicts related '
216 u'This pull request cannot be merged because of conflicts related '
217 u'to sub repositories.'),
217 u'to sub repositories.'),
218
218
219 # Deprecations
219 # Deprecations
220 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
220 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
221 u'This pull request cannot be merged because the target or the '
221 u'This pull request cannot be merged because the target or the '
222 u'source reference is missing.'),
222 u'source reference is missing.'),
223
223
224 }
224 }
225
225
226 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
226 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
227 self.possible = possible
227 self.possible = possible
228 self.executed = executed
228 self.executed = executed
229 self.merge_ref = merge_ref
229 self.merge_ref = merge_ref
230 self.failure_reason = failure_reason
230 self.failure_reason = failure_reason
231 self.metadata = metadata or {}
231 self.metadata = metadata or {}
232
232
233 def __repr__(self):
233 def __repr__(self):
234 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
234 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
235
235
236 def __eq__(self, other):
236 def __eq__(self, other):
237 same_instance = isinstance(other, self.__class__)
237 same_instance = isinstance(other, self.__class__)
238 return same_instance \
238 return same_instance \
239 and self.possible == other.possible \
239 and self.possible == other.possible \
240 and self.executed == other.executed \
240 and self.executed == other.executed \
241 and self.failure_reason == other.failure_reason
241 and self.failure_reason == other.failure_reason
242
242
243 @property
243 @property
244 def label(self):
244 def label(self):
245 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
245 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
246 not k.startswith('_'))
246 not k.startswith('_'))
247 return label_dict.get(self.failure_reason)
247 return label_dict.get(self.failure_reason)
248
248
249 @property
249 @property
250 def merge_status_message(self):
250 def merge_status_message(self):
251 """
251 """
252 Return a human friendly error message for the given merge status code.
252 Return a human friendly error message for the given merge status code.
253 """
253 """
254 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
254 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
255
255
256 try:
256 try:
257 return msg.format(**self.metadata)
257 return msg.format(**self.metadata)
258 except Exception:
258 except Exception:
259 log.exception('Failed to format %s message', self)
259 log.exception('Failed to format %s message', self)
260 return msg
260 return msg
261
261
262 def asdict(self):
262 def asdict(self):
263 data = {}
263 data = {}
264 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
264 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
265 'merge_status_message']:
265 'merge_status_message']:
266 data[k] = getattr(self, k)
266 data[k] = getattr(self, k)
267 return data
267 return data
268
268
269
269
270 class TargetRefMissing(ValueError):
270 class TargetRefMissing(ValueError):
271 pass
271 pass
272
272
273
273
274 class SourceRefMissing(ValueError):
274 class SourceRefMissing(ValueError):
275 pass
275 pass
276
276
277
277
278 class BaseRepository(object):
278 class BaseRepository(object):
279 """
279 """
280 Base Repository for final backends
280 Base Repository for final backends
281
281
282 .. attribute:: DEFAULT_BRANCH_NAME
282 .. attribute:: DEFAULT_BRANCH_NAME
283
283
284 name of default branch (i.e. "trunk" for svn, "master" for git etc.
284 name of default branch (i.e. "trunk" for svn, "master" for git etc.
285
285
286 .. attribute:: commit_ids
286 .. attribute:: commit_ids
287
287
288 list of all available commit ids, in ascending order
288 list of all available commit ids, in ascending order
289
289
290 .. attribute:: path
290 .. attribute:: path
291
291
292 absolute path to the repository
292 absolute path to the repository
293
293
294 .. attribute:: bookmarks
294 .. attribute:: bookmarks
295
295
296 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
296 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
297 there are no bookmarks or the backend implementation does not support
297 there are no bookmarks or the backend implementation does not support
298 bookmarks.
298 bookmarks.
299
299
300 .. attribute:: tags
300 .. attribute:: tags
301
301
302 Mapping from name to :term:`Commit ID` of the tag.
302 Mapping from name to :term:`Commit ID` of the tag.
303
303
304 """
304 """
305
305
306 DEFAULT_BRANCH_NAME = None
306 DEFAULT_BRANCH_NAME = None
307 DEFAULT_CONTACT = u"Unknown"
307 DEFAULT_CONTACT = u"Unknown"
308 DEFAULT_DESCRIPTION = u"unknown"
308 DEFAULT_DESCRIPTION = u"unknown"
309 EMPTY_COMMIT_ID = '0' * 40
309 EMPTY_COMMIT_ID = '0' * 40
310
310
311 path = None
311 path = None
312
312
313 _is_empty = None
313 _is_empty = None
314 _commit_ids = {}
314 _commit_ids = {}
315
315
316 def __init__(self, repo_path, config=None, create=False, **kwargs):
316 def __init__(self, repo_path, config=None, create=False, **kwargs):
317 """
317 """
318 Initializes repository. Raises RepositoryError if repository could
318 Initializes repository. Raises RepositoryError if repository could
319 not be find at the given ``repo_path`` or directory at ``repo_path``
319 not be find at the given ``repo_path`` or directory at ``repo_path``
320 exists and ``create`` is set to True.
320 exists and ``create`` is set to True.
321
321
322 :param repo_path: local path of the repository
322 :param repo_path: local path of the repository
323 :param config: repository configuration
323 :param config: repository configuration
324 :param create=False: if set to True, would try to create repository.
324 :param create=False: if set to True, would try to create repository.
325 :param src_url=None: if set, should be proper url from which repository
325 :param src_url=None: if set, should be proper url from which repository
326 would be cloned; requires ``create`` parameter to be set to True -
326 would be cloned; requires ``create`` parameter to be set to True -
327 raises RepositoryError if src_url is set and create evaluates to
327 raises RepositoryError if src_url is set and create evaluates to
328 False
328 False
329 """
329 """
330 raise NotImplementedError
330 raise NotImplementedError
331
331
332 def __repr__(self):
332 def __repr__(self):
333 return '<%s at %s>' % (self.__class__.__name__, self.path)
333 return '<%s at %s>' % (self.__class__.__name__, self.path)
334
334
335 def __len__(self):
335 def __len__(self):
336 return self.count()
336 return self.count()
337
337
338 def __eq__(self, other):
338 def __eq__(self, other):
339 same_instance = isinstance(other, self.__class__)
339 same_instance = isinstance(other, self.__class__)
340 return same_instance and other.path == self.path
340 return same_instance and other.path == self.path
341
341
342 def __ne__(self, other):
342 def __ne__(self, other):
343 return not self.__eq__(other)
343 return not self.__eq__(other)
344
344
345 def get_create_shadow_cache_pr_path(self, db_repo):
345 def get_create_shadow_cache_pr_path(self, db_repo):
346 path = db_repo.cached_diffs_dir
346 path = db_repo.cached_diffs_dir
347 if not os.path.exists(path):
347 if not os.path.exists(path):
348 os.makedirs(path, 0o755)
348 os.makedirs(path, 0o755)
349 return path
349 return path
350
350
351 @classmethod
351 @classmethod
352 def get_default_config(cls, default=None):
352 def get_default_config(cls, default=None):
353 config = Config()
353 config = Config()
354 if default and isinstance(default, list):
354 if default and isinstance(default, list):
355 for section, key, val in default:
355 for section, key, val in default:
356 config.set(section, key, val)
356 config.set(section, key, val)
357 return config
357 return config
358
358
359 @LazyProperty
359 @LazyProperty
360 def _remote(self):
360 def _remote(self):
361 raise NotImplementedError
361 raise NotImplementedError
362
362
363 def _heads(self, branch=None):
363 def _heads(self, branch=None):
364 return []
364 return []
365
365
366 @LazyProperty
366 @LazyProperty
367 def EMPTY_COMMIT(self):
367 def EMPTY_COMMIT(self):
368 return EmptyCommit(self.EMPTY_COMMIT_ID)
368 return EmptyCommit(self.EMPTY_COMMIT_ID)
369
369
370 @LazyProperty
370 @LazyProperty
371 def alias(self):
371 def alias(self):
372 for k, v in settings.BACKENDS.items():
372 for k, v in settings.BACKENDS.items():
373 if v.split('.')[-1] == str(self.__class__.__name__):
373 if v.split('.')[-1] == str(self.__class__.__name__):
374 return k
374 return k
375
375
376 @LazyProperty
376 @LazyProperty
377 def name(self):
377 def name(self):
378 return safe_unicode(os.path.basename(self.path))
378 return safe_unicode(os.path.basename(self.path))
379
379
380 @LazyProperty
380 @LazyProperty
381 def description(self):
381 def description(self):
382 raise NotImplementedError
382 raise NotImplementedError
383
383
384 def refs(self):
384 def refs(self):
385 """
385 """
386 returns a `dict` with branches, bookmarks, tags, and closed_branches
386 returns a `dict` with branches, bookmarks, tags, and closed_branches
387 for this repository
387 for this repository
388 """
388 """
389 return dict(
389 return dict(
390 branches=self.branches,
390 branches=self.branches,
391 branches_closed=self.branches_closed,
391 branches_closed=self.branches_closed,
392 tags=self.tags,
392 tags=self.tags,
393 bookmarks=self.bookmarks
393 bookmarks=self.bookmarks
394 )
394 )
395
395
396 @LazyProperty
396 @LazyProperty
397 def branches(self):
397 def branches(self):
398 """
398 """
399 A `dict` which maps branch names to commit ids.
399 A `dict` which maps branch names to commit ids.
400 """
400 """
401 raise NotImplementedError
401 raise NotImplementedError
402
402
403 @LazyProperty
403 @LazyProperty
404 def branches_closed(self):
404 def branches_closed(self):
405 """
405 """
406 A `dict` which maps tags names to commit ids.
406 A `dict` which maps tags names to commit ids.
407 """
407 """
408 raise NotImplementedError
408 raise NotImplementedError
409
409
410 @LazyProperty
410 @LazyProperty
411 def bookmarks(self):
411 def bookmarks(self):
412 """
412 """
413 A `dict` which maps tags names to commit ids.
413 A `dict` which maps tags names to commit ids.
414 """
414 """
415 raise NotImplementedError
415 raise NotImplementedError
416
416
417 @LazyProperty
417 @LazyProperty
418 def tags(self):
418 def tags(self):
419 """
419 """
420 A `dict` which maps tags names to commit ids.
420 A `dict` which maps tags names to commit ids.
421 """
421 """
422 raise NotImplementedError
422 raise NotImplementedError
423
423
424 @LazyProperty
424 @LazyProperty
425 def size(self):
425 def size(self):
426 """
426 """
427 Returns combined size in bytes for all repository files
427 Returns combined size in bytes for all repository files
428 """
428 """
429 tip = self.get_commit()
429 tip = self.get_commit()
430 return tip.size
430 return tip.size
431
431
432 def size_at_commit(self, commit_id):
432 def size_at_commit(self, commit_id):
433 commit = self.get_commit(commit_id)
433 commit = self.get_commit(commit_id)
434 return commit.size
434 return commit.size
435
435
436 def _check_for_empty(self):
436 def _check_for_empty(self):
437 no_commits = len(self._commit_ids) == 0
437 no_commits = len(self._commit_ids) == 0
438 if no_commits:
438 if no_commits:
439 # check on remote to be sure
439 # check on remote to be sure
440 return self._remote.is_empty()
440 return self._remote.is_empty()
441 else:
441 else:
442 return False
442 return False
443
443
444 def is_empty(self):
444 def is_empty(self):
445 if rhodecode.is_test:
445 if rhodecode.is_test:
446 return self._check_for_empty()
446 return self._check_for_empty()
447
447
448 if self._is_empty is None:
448 if self._is_empty is None:
449 # cache empty for production, but not tests
449 # cache empty for production, but not tests
450 self._is_empty = self._check_for_empty()
450 self._is_empty = self._check_for_empty()
451
451
452 return self._is_empty
452 return self._is_empty
453
453
454 @staticmethod
454 @staticmethod
455 def check_url(url, config):
455 def check_url(url, config):
456 """
456 """
457 Function will check given url and try to verify if it's a valid
457 Function will check given url and try to verify if it's a valid
458 link.
458 link.
459 """
459 """
460 raise NotImplementedError
460 raise NotImplementedError
461
461
462 @staticmethod
462 @staticmethod
463 def is_valid_repository(path):
463 def is_valid_repository(path):
464 """
464 """
465 Check if given `path` contains a valid repository of this backend
465 Check if given `path` contains a valid repository of this backend
466 """
466 """
467 raise NotImplementedError
467 raise NotImplementedError
468
468
469 # ==========================================================================
469 # ==========================================================================
470 # COMMITS
470 # COMMITS
471 # ==========================================================================
471 # ==========================================================================
472
472
473 @CachedProperty
473 @CachedProperty
474 def commit_ids(self):
474 def commit_ids(self):
475 raise NotImplementedError
475 raise NotImplementedError
476
476
477 def append_commit_id(self, commit_id):
477 def append_commit_id(self, commit_id):
478 if commit_id not in self.commit_ids:
478 if commit_id not in self.commit_ids:
479 self._rebuild_cache(self.commit_ids + [commit_id])
479 self._rebuild_cache(self.commit_ids + [commit_id])
480
480
481 # clear cache
481 # clear cache
482 self._invalidate_prop_cache('commit_ids')
482 self._invalidate_prop_cache('commit_ids')
483 self._is_empty = False
483 self._is_empty = False
484
484
485 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
485 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
486 translate_tag=None, maybe_unreachable=False):
486 translate_tag=None, maybe_unreachable=False):
487 """
487 """
488 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
488 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
489 are both None, most recent commit is returned.
489 are both None, most recent commit is returned.
490
490
491 :param pre_load: Optional. List of commit attributes to load.
491 :param pre_load: Optional. List of commit attributes to load.
492
492
493 :raises ``EmptyRepositoryError``: if there are no commits
493 :raises ``EmptyRepositoryError``: if there are no commits
494 """
494 """
495 raise NotImplementedError
495 raise NotImplementedError
496
496
497 def __iter__(self):
497 def __iter__(self):
498 for commit_id in self.commit_ids:
498 for commit_id in self.commit_ids:
499 yield self.get_commit(commit_id=commit_id)
499 yield self.get_commit(commit_id=commit_id)
500
500
501 def get_commits(
501 def get_commits(
502 self, start_id=None, end_id=None, start_date=None, end_date=None,
502 self, start_id=None, end_id=None, start_date=None, end_date=None,
503 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
503 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
504 """
504 """
505 Returns iterator of `BaseCommit` objects from start to end
505 Returns iterator of `BaseCommit` objects from start to end
506 not inclusive. This should behave just like a list, ie. end is not
506 not inclusive. This should behave just like a list, ie. end is not
507 inclusive.
507 inclusive.
508
508
509 :param start_id: None or str, must be a valid commit id
509 :param start_id: None or str, must be a valid commit id
510 :param end_id: None or str, must be a valid commit id
510 :param end_id: None or str, must be a valid commit id
511 :param start_date:
511 :param start_date:
512 :param end_date:
512 :param end_date:
513 :param branch_name:
513 :param branch_name:
514 :param show_hidden:
514 :param show_hidden:
515 :param pre_load:
515 :param pre_load:
516 :param translate_tags:
516 :param translate_tags:
517 """
517 """
518 raise NotImplementedError
518 raise NotImplementedError
519
519
520 def __getitem__(self, key):
520 def __getitem__(self, key):
521 """
521 """
522 Allows index based access to the commit objects of this repository.
522 Allows index based access to the commit objects of this repository.
523 """
523 """
524 pre_load = ["author", "branch", "date", "message", "parents"]
524 pre_load = ["author", "branch", "date", "message", "parents"]
525 if isinstance(key, slice):
525 if isinstance(key, slice):
526 return self._get_range(key, pre_load)
526 return self._get_range(key, pre_load)
527 return self.get_commit(commit_idx=key, pre_load=pre_load)
527 return self.get_commit(commit_idx=key, pre_load=pre_load)
528
528
529 def _get_range(self, slice_obj, pre_load):
529 def _get_range(self, slice_obj, pre_load):
530 for commit_id in self.commit_ids.__getitem__(slice_obj):
530 for commit_id in self.commit_ids.__getitem__(slice_obj):
531 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
531 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
532
532
533 def count(self):
533 def count(self):
534 return len(self.commit_ids)
534 return len(self.commit_ids)
535
535
536 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
536 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
537 """
537 """
538 Creates and returns a tag for the given ``commit_id``.
538 Creates and returns a tag for the given ``commit_id``.
539
539
540 :param name: name for new tag
540 :param name: name for new tag
541 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
541 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
542 :param commit_id: commit id for which new tag would be created
542 :param commit_id: commit id for which new tag would be created
543 :param message: message of the tag's commit
543 :param message: message of the tag's commit
544 :param date: date of tag's commit
544 :param date: date of tag's commit
545
545
546 :raises TagAlreadyExistError: if tag with same name already exists
546 :raises TagAlreadyExistError: if tag with same name already exists
547 """
547 """
548 raise NotImplementedError
548 raise NotImplementedError
549
549
550 def remove_tag(self, name, user, message=None, date=None):
550 def remove_tag(self, name, user, message=None, date=None):
551 """
551 """
552 Removes tag with the given ``name``.
552 Removes tag with the given ``name``.
553
553
554 :param name: name of the tag to be removed
554 :param name: name of the tag to be removed
555 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
555 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
556 :param message: message of the tag's removal commit
556 :param message: message of the tag's removal commit
557 :param date: date of tag's removal commit
557 :param date: date of tag's removal commit
558
558
559 :raises TagDoesNotExistError: if tag with given name does not exists
559 :raises TagDoesNotExistError: if tag with given name does not exists
560 """
560 """
561 raise NotImplementedError
561 raise NotImplementedError
562
562
563 def get_diff(
563 def get_diff(
564 self, commit1, commit2, path=None, ignore_whitespace=False,
564 self, commit1, commit2, path=None, ignore_whitespace=False,
565 context=3, path1=None):
565 context=3, path1=None):
566 """
566 """
567 Returns (git like) *diff*, as plain text. Shows changes introduced by
567 Returns (git like) *diff*, as plain text. Shows changes introduced by
568 `commit2` since `commit1`.
568 `commit2` since `commit1`.
569
569
570 :param commit1: Entry point from which diff is shown. Can be
570 :param commit1: Entry point from which diff is shown. Can be
571 ``self.EMPTY_COMMIT`` - in this case, patch showing all
571 ``self.EMPTY_COMMIT`` - in this case, patch showing all
572 the changes since empty state of the repository until `commit2`
572 the changes since empty state of the repository until `commit2`
573 :param commit2: Until which commit changes should be shown.
573 :param commit2: Until which commit changes should be shown.
574 :param path: Can be set to a path of a file to create a diff of that
574 :param path: Can be set to a path of a file to create a diff of that
575 file. If `path1` is also set, this value is only associated to
575 file. If `path1` is also set, this value is only associated to
576 `commit2`.
576 `commit2`.
577 :param ignore_whitespace: If set to ``True``, would not show whitespace
577 :param ignore_whitespace: If set to ``True``, would not show whitespace
578 changes. Defaults to ``False``.
578 changes. Defaults to ``False``.
579 :param context: How many lines before/after changed lines should be
579 :param context: How many lines before/after changed lines should be
580 shown. Defaults to ``3``.
580 shown. Defaults to ``3``.
581 :param path1: Can be set to a path to associate with `commit1`. This
581 :param path1: Can be set to a path to associate with `commit1`. This
582 parameter works only for backends which support diff generation for
582 parameter works only for backends which support diff generation for
583 different paths. Other backends will raise a `ValueError` if `path1`
583 different paths. Other backends will raise a `ValueError` if `path1`
584 is set and has a different value than `path`.
584 is set and has a different value than `path`.
585 :param file_path: filter this diff by given path pattern
585 :param file_path: filter this diff by given path pattern
586 """
586 """
587 raise NotImplementedError
587 raise NotImplementedError
588
588
589 def strip(self, commit_id, branch=None):
589 def strip(self, commit_id, branch=None):
590 """
590 """
591 Strip given commit_id from the repository
591 Strip given commit_id from the repository
592 """
592 """
593 raise NotImplementedError
593 raise NotImplementedError
594
594
595 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
595 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
596 """
596 """
597 Return a latest common ancestor commit if one exists for this repo
597 Return a latest common ancestor commit if one exists for this repo
598 `commit_id1` vs `commit_id2` from `repo2`.
598 `commit_id1` vs `commit_id2` from `repo2`.
599
599
600 :param commit_id1: Commit it from this repository to use as a
600 :param commit_id1: Commit it from this repository to use as a
601 target for the comparison.
601 target for the comparison.
602 :param commit_id2: Source commit id to use for comparison.
602 :param commit_id2: Source commit id to use for comparison.
603 :param repo2: Source repository to use for comparison.
603 :param repo2: Source repository to use for comparison.
604 """
604 """
605 raise NotImplementedError
605 raise NotImplementedError
606
606
607 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
607 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
608 """
608 """
609 Compare this repository's revision `commit_id1` with `commit_id2`.
609 Compare this repository's revision `commit_id1` with `commit_id2`.
610
610
611 Returns a tuple(commits, ancestor) that would be merged from
611 Returns a tuple(commits, ancestor) that would be merged from
612 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
612 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
613 will be returned as ancestor.
613 will be returned as ancestor.
614
614
615 :param commit_id1: Commit it from this repository to use as a
615 :param commit_id1: Commit it from this repository to use as a
616 target for the comparison.
616 target for the comparison.
617 :param commit_id2: Source commit id to use for comparison.
617 :param commit_id2: Source commit id to use for comparison.
618 :param repo2: Source repository to use for comparison.
618 :param repo2: Source repository to use for comparison.
619 :param merge: If set to ``True`` will do a merge compare which also
619 :param merge: If set to ``True`` will do a merge compare which also
620 returns the common ancestor.
620 returns the common ancestor.
621 :param pre_load: Optional. List of commit attributes to load.
621 :param pre_load: Optional. List of commit attributes to load.
622 """
622 """
623 raise NotImplementedError
623 raise NotImplementedError
624
624
625 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
625 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
626 user_name='', user_email='', message='', dry_run=False,
626 user_name='', user_email='', message='', dry_run=False,
627 use_rebase=False, close_branch=False):
627 use_rebase=False, close_branch=False):
628 """
628 """
629 Merge the revisions specified in `source_ref` from `source_repo`
629 Merge the revisions specified in `source_ref` from `source_repo`
630 onto the `target_ref` of this repository.
630 onto the `target_ref` of this repository.
631
631
632 `source_ref` and `target_ref` are named tupls with the following
632 `source_ref` and `target_ref` are named tupls with the following
633 fields `type`, `name` and `commit_id`.
633 fields `type`, `name` and `commit_id`.
634
634
635 Returns a MergeResponse named tuple with the following fields
635 Returns a MergeResponse named tuple with the following fields
636 'possible', 'executed', 'source_commit', 'target_commit',
636 'possible', 'executed', 'source_commit', 'target_commit',
637 'merge_commit'.
637 'merge_commit'.
638
638
639 :param repo_id: `repo_id` target repo id.
639 :param repo_id: `repo_id` target repo id.
640 :param workspace_id: `workspace_id` unique identifier.
640 :param workspace_id: `workspace_id` unique identifier.
641 :param target_ref: `target_ref` points to the commit on top of which
641 :param target_ref: `target_ref` points to the commit on top of which
642 the `source_ref` should be merged.
642 the `source_ref` should be merged.
643 :param source_repo: The repository that contains the commits to be
643 :param source_repo: The repository that contains the commits to be
644 merged.
644 merged.
645 :param source_ref: `source_ref` points to the topmost commit from
645 :param source_ref: `source_ref` points to the topmost commit from
646 the `source_repo` which should be merged.
646 the `source_repo` which should be merged.
647 :param user_name: Merge commit `user_name`.
647 :param user_name: Merge commit `user_name`.
648 :param user_email: Merge commit `user_email`.
648 :param user_email: Merge commit `user_email`.
649 :param message: Merge commit `message`.
649 :param message: Merge commit `message`.
650 :param dry_run: If `True` the merge will not take place.
650 :param dry_run: If `True` the merge will not take place.
651 :param use_rebase: If `True` commits from the source will be rebased
651 :param use_rebase: If `True` commits from the source will be rebased
652 on top of the target instead of being merged.
652 on top of the target instead of being merged.
653 :param close_branch: If `True` branch will be close before merging it
653 :param close_branch: If `True` branch will be close before merging it
654 """
654 """
655 if dry_run:
655 if dry_run:
656 message = message or settings.MERGE_DRY_RUN_MESSAGE
656 message = message or settings.MERGE_DRY_RUN_MESSAGE
657 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
657 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
658 user_name = user_name or settings.MERGE_DRY_RUN_USER
658 user_name = user_name or settings.MERGE_DRY_RUN_USER
659 else:
659 else:
660 if not user_name:
660 if not user_name:
661 raise ValueError('user_name cannot be empty')
661 raise ValueError('user_name cannot be empty')
662 if not user_email:
662 if not user_email:
663 raise ValueError('user_email cannot be empty')
663 raise ValueError('user_email cannot be empty')
664 if not message:
664 if not message:
665 raise ValueError('message cannot be empty')
665 raise ValueError('message cannot be empty')
666
666
667 try:
667 try:
668 return self._merge_repo(
668 return self._merge_repo(
669 repo_id, workspace_id, target_ref, source_repo,
669 repo_id, workspace_id, target_ref, source_repo,
670 source_ref, message, user_name, user_email, dry_run=dry_run,
670 source_ref, message, user_name, user_email, dry_run=dry_run,
671 use_rebase=use_rebase, close_branch=close_branch)
671 use_rebase=use_rebase, close_branch=close_branch)
672 except RepositoryError as exc:
672 except RepositoryError as exc:
673 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
673 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
674 return MergeResponse(
674 return MergeResponse(
675 False, False, None, MergeFailureReason.UNKNOWN,
675 False, False, None, MergeFailureReason.UNKNOWN,
676 metadata={'exception': str(exc)})
676 metadata={'exception': str(exc)})
677
677
678 def _merge_repo(self, repo_id, workspace_id, target_ref,
678 def _merge_repo(self, repo_id, workspace_id, target_ref,
679 source_repo, source_ref, merge_message,
679 source_repo, source_ref, merge_message,
680 merger_name, merger_email, dry_run=False,
680 merger_name, merger_email, dry_run=False,
681 use_rebase=False, close_branch=False):
681 use_rebase=False, close_branch=False):
682 """Internal implementation of merge."""
682 """Internal implementation of merge."""
683 raise NotImplementedError
683 raise NotImplementedError
684
684
685 def _maybe_prepare_merge_workspace(
685 def _maybe_prepare_merge_workspace(
686 self, repo_id, workspace_id, target_ref, source_ref):
686 self, repo_id, workspace_id, target_ref, source_ref):
687 """
687 """
688 Create the merge workspace.
688 Create the merge workspace.
689
689
690 :param workspace_id: `workspace_id` unique identifier.
690 :param workspace_id: `workspace_id` unique identifier.
691 """
691 """
692 raise NotImplementedError
692 raise NotImplementedError
693
693
694 @classmethod
694 @classmethod
695 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
695 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
696 """
696 """
697 Legacy version that was used before. We still need it for
697 Legacy version that was used before. We still need it for
698 backward compat
698 backward compat
699 """
699 """
700 return os.path.join(
700 return os.path.join(
701 os.path.dirname(repo_path),
701 os.path.dirname(repo_path),
702 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
702 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
703
703
704 @classmethod
704 @classmethod
705 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
705 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
706 # The name of the shadow repository must start with '.', so it is
706 # The name of the shadow repository must start with '.', so it is
707 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
707 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
708 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
708 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
709 if os.path.exists(legacy_repository_path):
709 if os.path.exists(legacy_repository_path):
710 return legacy_repository_path
710 return legacy_repository_path
711 else:
711 else:
712 return os.path.join(
712 return os.path.join(
713 os.path.dirname(repo_path),
713 os.path.dirname(repo_path),
714 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
714 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
715
715
716 def cleanup_merge_workspace(self, repo_id, workspace_id):
716 def cleanup_merge_workspace(self, repo_id, workspace_id):
717 """
717 """
718 Remove merge workspace.
718 Remove merge workspace.
719
719
720 This function MUST not fail in case there is no workspace associated to
720 This function MUST not fail in case there is no workspace associated to
721 the given `workspace_id`.
721 the given `workspace_id`.
722
722
723 :param workspace_id: `workspace_id` unique identifier.
723 :param workspace_id: `workspace_id` unique identifier.
724 """
724 """
725 shadow_repository_path = self._get_shadow_repository_path(
725 shadow_repository_path = self._get_shadow_repository_path(
726 self.path, repo_id, workspace_id)
726 self.path, repo_id, workspace_id)
727 shadow_repository_path_del = '{}.{}.delete'.format(
727 shadow_repository_path_del = '{}.{}.delete'.format(
728 shadow_repository_path, time.time())
728 shadow_repository_path, time.time())
729
729
730 # move the shadow repo, so it never conflicts with the one used.
730 # move the shadow repo, so it never conflicts with the one used.
731 # we use this method because shutil.rmtree had some edge case problems
731 # we use this method because shutil.rmtree had some edge case problems
732 # removing symlinked repositories
732 # removing symlinked repositories
733 if not os.path.isdir(shadow_repository_path):
733 if not os.path.isdir(shadow_repository_path):
734 return
734 return
735
735
736 shutil.move(shadow_repository_path, shadow_repository_path_del)
736 shutil.move(shadow_repository_path, shadow_repository_path_del)
737 try:
737 try:
738 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
738 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
739 except Exception:
739 except Exception:
740 log.exception('Failed to gracefully remove shadow repo under %s',
740 log.exception('Failed to gracefully remove shadow repo under %s',
741 shadow_repository_path_del)
741 shadow_repository_path_del)
742 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
742 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
743
743
744 # ========== #
744 # ========== #
745 # COMMIT API #
745 # COMMIT API #
746 # ========== #
746 # ========== #
747
747
748 @LazyProperty
748 @LazyProperty
749 def in_memory_commit(self):
749 def in_memory_commit(self):
750 """
750 """
751 Returns :class:`InMemoryCommit` object for this repository.
751 Returns :class:`InMemoryCommit` object for this repository.
752 """
752 """
753 raise NotImplementedError
753 raise NotImplementedError
754
754
755 # ======================== #
755 # ======================== #
756 # UTILITIES FOR SUBCLASSES #
756 # UTILITIES FOR SUBCLASSES #
757 # ======================== #
757 # ======================== #
758
758
759 def _validate_diff_commits(self, commit1, commit2):
759 def _validate_diff_commits(self, commit1, commit2):
760 """
760 """
761 Validates that the given commits are related to this repository.
761 Validates that the given commits are related to this repository.
762
762
763 Intended as a utility for sub classes to have a consistent validation
763 Intended as a utility for sub classes to have a consistent validation
764 of input parameters in methods like :meth:`get_diff`.
764 of input parameters in methods like :meth:`get_diff`.
765 """
765 """
766 self._validate_commit(commit1)
766 self._validate_commit(commit1)
767 self._validate_commit(commit2)
767 self._validate_commit(commit2)
768 if (isinstance(commit1, EmptyCommit) and
768 if (isinstance(commit1, EmptyCommit) and
769 isinstance(commit2, EmptyCommit)):
769 isinstance(commit2, EmptyCommit)):
770 raise ValueError("Cannot compare two empty commits")
770 raise ValueError("Cannot compare two empty commits")
771
771
772 def _validate_commit(self, commit):
772 def _validate_commit(self, commit):
773 if not isinstance(commit, BaseCommit):
773 if not isinstance(commit, BaseCommit):
774 raise TypeError(
774 raise TypeError(
775 "%s is not of type BaseCommit" % repr(commit))
775 "%s is not of type BaseCommit" % repr(commit))
776 if commit.repository != self and not isinstance(commit, EmptyCommit):
776 if commit.repository != self and not isinstance(commit, EmptyCommit):
777 raise ValueError(
777 raise ValueError(
778 "Commit %s must be a valid commit from this repository %s, "
778 "Commit %s must be a valid commit from this repository %s, "
779 "related to this repository instead %s." %
779 "related to this repository instead %s." %
780 (commit, self, commit.repository))
780 (commit, self, commit.repository))
781
781
782 def _validate_commit_id(self, commit_id):
782 def _validate_commit_id(self, commit_id):
783 if not isinstance(commit_id, compat.string_types):
783 if not isinstance(commit_id, compat.string_types):
784 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
784 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
785
785
786 def _validate_commit_idx(self, commit_idx):
786 def _validate_commit_idx(self, commit_idx):
787 if not isinstance(commit_idx, (int, long)):
787 if not isinstance(commit_idx, (int, long)):
788 raise TypeError("commit_idx must be a numeric value")
788 raise TypeError("commit_idx must be a numeric value")
789
789
790 def _validate_branch_name(self, branch_name):
790 def _validate_branch_name(self, branch_name):
791 if branch_name and branch_name not in self.branches_all:
791 if branch_name and branch_name not in self.branches_all:
792 msg = ("Branch %s not found in %s" % (branch_name, self))
792 msg = ("Branch %s not found in %s" % (branch_name, self))
793 raise BranchDoesNotExistError(msg)
793 raise BranchDoesNotExistError(msg)
794
794
795 #
795 #
796 # Supporting deprecated API parts
796 # Supporting deprecated API parts
797 # TODO: johbo: consider to move this into a mixin
797 # TODO: johbo: consider to move this into a mixin
798 #
798 #
799
799
800 @property
800 @property
801 def EMPTY_CHANGESET(self):
801 def EMPTY_CHANGESET(self):
802 warnings.warn(
802 warnings.warn(
803 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
803 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
804 return self.EMPTY_COMMIT_ID
804 return self.EMPTY_COMMIT_ID
805
805
806 @property
806 @property
807 def revisions(self):
807 def revisions(self):
808 warnings.warn("Use commits attribute instead", DeprecationWarning)
808 warnings.warn("Use commits attribute instead", DeprecationWarning)
809 return self.commit_ids
809 return self.commit_ids
810
810
811 @revisions.setter
811 @revisions.setter
812 def revisions(self, value):
812 def revisions(self, value):
813 warnings.warn("Use commits attribute instead", DeprecationWarning)
813 warnings.warn("Use commits attribute instead", DeprecationWarning)
814 self.commit_ids = value
814 self.commit_ids = value
815
815
816 def get_changeset(self, revision=None, pre_load=None):
816 def get_changeset(self, revision=None, pre_load=None):
817 warnings.warn("Use get_commit instead", DeprecationWarning)
817 warnings.warn("Use get_commit instead", DeprecationWarning)
818 commit_id = None
818 commit_id = None
819 commit_idx = None
819 commit_idx = None
820 if isinstance(revision, compat.string_types):
820 if isinstance(revision, compat.string_types):
821 commit_id = revision
821 commit_id = revision
822 else:
822 else:
823 commit_idx = revision
823 commit_idx = revision
824 return self.get_commit(
824 return self.get_commit(
825 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
825 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
826
826
827 def get_changesets(
827 def get_changesets(
828 self, start=None, end=None, start_date=None, end_date=None,
828 self, start=None, end=None, start_date=None, end_date=None,
829 branch_name=None, pre_load=None):
829 branch_name=None, pre_load=None):
830 warnings.warn("Use get_commits instead", DeprecationWarning)
830 warnings.warn("Use get_commits instead", DeprecationWarning)
831 start_id = self._revision_to_commit(start)
831 start_id = self._revision_to_commit(start)
832 end_id = self._revision_to_commit(end)
832 end_id = self._revision_to_commit(end)
833 return self.get_commits(
833 return self.get_commits(
834 start_id=start_id, end_id=end_id, start_date=start_date,
834 start_id=start_id, end_id=end_id, start_date=start_date,
835 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
835 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
836
836
837 def _revision_to_commit(self, revision):
837 def _revision_to_commit(self, revision):
838 """
838 """
839 Translates a revision to a commit_id
839 Translates a revision to a commit_id
840
840
841 Helps to support the old changeset based API which allows to use
841 Helps to support the old changeset based API which allows to use
842 commit ids and commit indices interchangeable.
842 commit ids and commit indices interchangeable.
843 """
843 """
844 if revision is None:
844 if revision is None:
845 return revision
845 return revision
846
846
847 if isinstance(revision, compat.string_types):
847 if isinstance(revision, compat.string_types):
848 commit_id = revision
848 commit_id = revision
849 else:
849 else:
850 commit_id = self.commit_ids[revision]
850 commit_id = self.commit_ids[revision]
851 return commit_id
851 return commit_id
852
852
853 @property
853 @property
854 def in_memory_changeset(self):
854 def in_memory_changeset(self):
855 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
855 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
856 return self.in_memory_commit
856 return self.in_memory_commit
857
857
858 def get_path_permissions(self, username):
858 def get_path_permissions(self, username):
859 """
859 """
860 Returns a path permission checker or None if not supported
860 Returns a path permission checker or None if not supported
861
861
862 :param username: session user name
862 :param username: session user name
863 :return: an instance of BasePathPermissionChecker or None
863 :return: an instance of BasePathPermissionChecker or None
864 """
864 """
865 return None
865 return None
866
866
867 def install_hooks(self, force=False):
867 def install_hooks(self, force=False):
868 return self._remote.install_hooks(force)
868 return self._remote.install_hooks(force)
869
869
870 def get_hooks_info(self):
870 def get_hooks_info(self):
871 return self._remote.get_hooks_info()
871 return self._remote.get_hooks_info()
872
872
873
873
874 class BaseCommit(object):
874 class BaseCommit(object):
875 """
875 """
876 Each backend should implement it's commit representation.
876 Each backend should implement it's commit representation.
877
877
878 **Attributes**
878 **Attributes**
879
879
880 ``repository``
880 ``repository``
881 repository object within which commit exists
881 repository object within which commit exists
882
882
883 ``id``
883 ``id``
884 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
884 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
885 just ``tip``.
885 just ``tip``.
886
886
887 ``raw_id``
887 ``raw_id``
888 raw commit representation (i.e. full 40 length sha for git
888 raw commit representation (i.e. full 40 length sha for git
889 backend)
889 backend)
890
890
891 ``short_id``
891 ``short_id``
892 shortened (if apply) version of ``raw_id``; it would be simple
892 shortened (if apply) version of ``raw_id``; it would be simple
893 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
893 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
894 as ``raw_id`` for subversion
894 as ``raw_id`` for subversion
895
895
896 ``idx``
896 ``idx``
897 commit index
897 commit index
898
898
899 ``files``
899 ``files``
900 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
900 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
901
901
902 ``dirs``
902 ``dirs``
903 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
903 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
904
904
905 ``nodes``
905 ``nodes``
906 combined list of ``Node`` objects
906 combined list of ``Node`` objects
907
907
908 ``author``
908 ``author``
909 author of the commit, as unicode
909 author of the commit, as unicode
910
910
911 ``message``
911 ``message``
912 message of the commit, as unicode
912 message of the commit, as unicode
913
913
914 ``parents``
914 ``parents``
915 list of parent commits
915 list of parent commits
916
916
917 """
917 """
918
918
919 branch = None
919 branch = None
920 """
920 """
921 Depending on the backend this should be set to the branch name of the
921 Depending on the backend this should be set to the branch name of the
922 commit. Backends not supporting branches on commits should leave this
922 commit. Backends not supporting branches on commits should leave this
923 value as ``None``.
923 value as ``None``.
924 """
924 """
925
925
926 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
926 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
927 """
927 """
928 This template is used to generate a default prefix for repository archives
928 This template is used to generate a default prefix for repository archives
929 if no prefix has been specified.
929 if no prefix has been specified.
930 """
930 """
931
931
932 def __str__(self):
932 def __str__(self):
933 return '<%s at %s:%s>' % (
933 return '<%s at %s:%s>' % (
934 self.__class__.__name__, self.idx, self.short_id)
934 self.__class__.__name__, self.idx, self.short_id)
935
935
936 def __repr__(self):
936 def __repr__(self):
937 return self.__str__()
937 return self.__str__()
938
938
939 def __unicode__(self):
939 def __unicode__(self):
940 return u'%s:%s' % (self.idx, self.short_id)
940 return u'%s:%s' % (self.idx, self.short_id)
941
941
942 def __eq__(self, other):
942 def __eq__(self, other):
943 same_instance = isinstance(other, self.__class__)
943 same_instance = isinstance(other, self.__class__)
944 return same_instance and self.raw_id == other.raw_id
944 return same_instance and self.raw_id == other.raw_id
945
945
946 def __json__(self):
946 def __json__(self):
947 parents = []
947 parents = []
948 try:
948 try:
949 for parent in self.parents:
949 for parent in self.parents:
950 parents.append({'raw_id': parent.raw_id})
950 parents.append({'raw_id': parent.raw_id})
951 except NotImplementedError:
951 except NotImplementedError:
952 # empty commit doesn't have parents implemented
952 # empty commit doesn't have parents implemented
953 pass
953 pass
954
954
955 return {
955 return {
956 'short_id': self.short_id,
956 'short_id': self.short_id,
957 'raw_id': self.raw_id,
957 'raw_id': self.raw_id,
958 'revision': self.idx,
958 'revision': self.idx,
959 'message': self.message,
959 'message': self.message,
960 'date': self.date,
960 'date': self.date,
961 'author': self.author,
961 'author': self.author,
962 'parents': parents,
962 'parents': parents,
963 'branch': self.branch
963 'branch': self.branch
964 }
964 }
965
965
966 def __getstate__(self):
966 def __getstate__(self):
967 d = self.__dict__.copy()
967 d = self.__dict__.copy()
968 d.pop('_remote', None)
968 d.pop('_remote', None)
969 d.pop('repository', None)
969 d.pop('repository', None)
970 return d
970 return d
971
971
972 def serialize(self):
972 def serialize(self):
973 return self.__json__()
973 return self.__json__()
974
974
975 def _get_refs(self):
975 def _get_refs(self):
976 return {
976 return {
977 'branches': [self.branch] if self.branch else [],
977 'branches': [self.branch] if self.branch else [],
978 'bookmarks': getattr(self, 'bookmarks', []),
978 'bookmarks': getattr(self, 'bookmarks', []),
979 'tags': self.tags
979 'tags': self.tags
980 }
980 }
981
981
982 @LazyProperty
982 @LazyProperty
983 def last(self):
983 def last(self):
984 """
984 """
985 ``True`` if this is last commit in repository, ``False``
985 ``True`` if this is last commit in repository, ``False``
986 otherwise; trying to access this attribute while there is no
986 otherwise; trying to access this attribute while there is no
987 commits would raise `EmptyRepositoryError`
987 commits would raise `EmptyRepositoryError`
988 """
988 """
989 if self.repository is None:
989 if self.repository is None:
990 raise CommitError("Cannot check if it's most recent commit")
990 raise CommitError("Cannot check if it's most recent commit")
991 return self.raw_id == self.repository.commit_ids[-1]
991 return self.raw_id == self.repository.commit_ids[-1]
992
992
993 @LazyProperty
993 @LazyProperty
994 def parents(self):
994 def parents(self):
995 """
995 """
996 Returns list of parent commits.
996 Returns list of parent commits.
997 """
997 """
998 raise NotImplementedError
998 raise NotImplementedError
999
999
1000 @LazyProperty
1000 @LazyProperty
1001 def first_parent(self):
1001 def first_parent(self):
1002 """
1002 """
1003 Returns list of parent commits.
1003 Returns list of parent commits.
1004 """
1004 """
1005 return self.parents[0] if self.parents else EmptyCommit()
1005 return self.parents[0] if self.parents else EmptyCommit()
1006
1006
1007 @property
1007 @property
1008 def merge(self):
1008 def merge(self):
1009 """
1009 """
1010 Returns boolean if commit is a merge.
1010 Returns boolean if commit is a merge.
1011 """
1011 """
1012 return len(self.parents) > 1
1012 return len(self.parents) > 1
1013
1013
1014 @LazyProperty
1014 @LazyProperty
1015 def children(self):
1015 def children(self):
1016 """
1016 """
1017 Returns list of child commits.
1017 Returns list of child commits.
1018 """
1018 """
1019 raise NotImplementedError
1019 raise NotImplementedError
1020
1020
1021 @LazyProperty
1021 @LazyProperty
1022 def id(self):
1022 def id(self):
1023 """
1023 """
1024 Returns string identifying this commit.
1024 Returns string identifying this commit.
1025 """
1025 """
1026 raise NotImplementedError
1026 raise NotImplementedError
1027
1027
1028 @LazyProperty
1028 @LazyProperty
1029 def raw_id(self):
1029 def raw_id(self):
1030 """
1030 """
1031 Returns raw string identifying this commit.
1031 Returns raw string identifying this commit.
1032 """
1032 """
1033 raise NotImplementedError
1033 raise NotImplementedError
1034
1034
1035 @LazyProperty
1035 @LazyProperty
1036 def short_id(self):
1036 def short_id(self):
1037 """
1037 """
1038 Returns shortened version of ``raw_id`` attribute, as string,
1038 Returns shortened version of ``raw_id`` attribute, as string,
1039 identifying this commit, useful for presentation to users.
1039 identifying this commit, useful for presentation to users.
1040 """
1040 """
1041 raise NotImplementedError
1041 raise NotImplementedError
1042
1042
1043 @LazyProperty
1043 @LazyProperty
1044 def idx(self):
1044 def idx(self):
1045 """
1045 """
1046 Returns integer identifying this commit.
1046 Returns integer identifying this commit.
1047 """
1047 """
1048 raise NotImplementedError
1048 raise NotImplementedError
1049
1049
1050 @LazyProperty
1050 @LazyProperty
1051 def committer(self):
1051 def committer(self):
1052 """
1052 """
1053 Returns committer for this commit
1053 Returns committer for this commit
1054 """
1054 """
1055 raise NotImplementedError
1055 raise NotImplementedError
1056
1056
1057 @LazyProperty
1057 @LazyProperty
1058 def committer_name(self):
1058 def committer_name(self):
1059 """
1059 """
1060 Returns committer name for this commit
1060 Returns committer name for this commit
1061 """
1061 """
1062
1062
1063 return author_name(self.committer)
1063 return author_name(self.committer)
1064
1064
1065 @LazyProperty
1065 @LazyProperty
1066 def committer_email(self):
1066 def committer_email(self):
1067 """
1067 """
1068 Returns committer email address for this commit
1068 Returns committer email address for this commit
1069 """
1069 """
1070
1070
1071 return author_email(self.committer)
1071 return author_email(self.committer)
1072
1072
1073 @LazyProperty
1073 @LazyProperty
1074 def author(self):
1074 def author(self):
1075 """
1075 """
1076 Returns author for this commit
1076 Returns author for this commit
1077 """
1077 """
1078
1078
1079 raise NotImplementedError
1079 raise NotImplementedError
1080
1080
1081 @LazyProperty
1081 @LazyProperty
1082 def author_name(self):
1082 def author_name(self):
1083 """
1083 """
1084 Returns author name for this commit
1084 Returns author name for this commit
1085 """
1085 """
1086
1086
1087 return author_name(self.author)
1087 return author_name(self.author)
1088
1088
1089 @LazyProperty
1089 @LazyProperty
1090 def author_email(self):
1090 def author_email(self):
1091 """
1091 """
1092 Returns author email address for this commit
1092 Returns author email address for this commit
1093 """
1093 """
1094
1094
1095 return author_email(self.author)
1095 return author_email(self.author)
1096
1096
1097 def get_file_mode(self, path):
1097 def get_file_mode(self, path):
1098 """
1098 """
1099 Returns stat mode of the file at `path`.
1099 Returns stat mode of the file at `path`.
1100 """
1100 """
1101 raise NotImplementedError
1101 raise NotImplementedError
1102
1102
1103 def is_link(self, path):
1103 def is_link(self, path):
1104 """
1104 """
1105 Returns ``True`` if given `path` is a symlink
1105 Returns ``True`` if given `path` is a symlink
1106 """
1106 """
1107 raise NotImplementedError
1107 raise NotImplementedError
1108
1108
1109 def is_node_binary(self, path):
1109 def is_node_binary(self, path):
1110 """
1110 """
1111 Returns ``True`` is given path is a binary file
1111 Returns ``True`` is given path is a binary file
1112 """
1112 """
1113 raise NotImplementedError
1113 raise NotImplementedError
1114
1114
1115 def get_file_content(self, path):
1115 def get_file_content(self, path):
1116 """
1116 """
1117 Returns content of the file at the given `path`.
1117 Returns content of the file at the given `path`.
1118 """
1118 """
1119 raise NotImplementedError
1119 raise NotImplementedError
1120
1120
1121 def get_file_content_streamed(self, path):
1121 def get_file_content_streamed(self, path):
1122 """
1122 """
1123 returns a streaming response from vcsserver with file content
1123 returns a streaming response from vcsserver with file content
1124 """
1124 """
1125 raise NotImplementedError
1125 raise NotImplementedError
1126
1126
1127 def get_file_size(self, path):
1127 def get_file_size(self, path):
1128 """
1128 """
1129 Returns size of the file at the given `path`.
1129 Returns size of the file at the given `path`.
1130 """
1130 """
1131 raise NotImplementedError
1131 raise NotImplementedError
1132
1132
1133 def get_path_commit(self, path, pre_load=None):
1133 def get_path_commit(self, path, pre_load=None):
1134 """
1134 """
1135 Returns last commit of the file at the given `path`.
1135 Returns last commit of the file at the given `path`.
1136
1136
1137 :param pre_load: Optional. List of commit attributes to load.
1137 :param pre_load: Optional. List of commit attributes to load.
1138 """
1138 """
1139 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1139 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1140 if not commits:
1140 if not commits:
1141 raise RepositoryError(
1141 raise RepositoryError(
1142 'Failed to fetch history for path {}. '
1142 'Failed to fetch history for path {}. '
1143 'Please check if such path exists in your repository'.format(
1143 'Please check if such path exists in your repository'.format(
1144 path))
1144 path))
1145 return commits[0]
1145 return commits[0]
1146
1146
1147 def get_path_history(self, path, limit=None, pre_load=None):
1147 def get_path_history(self, path, limit=None, pre_load=None):
1148 """
1148 """
1149 Returns history of file as reversed list of :class:`BaseCommit`
1149 Returns history of file as reversed list of :class:`BaseCommit`
1150 objects for which file at given `path` has been modified.
1150 objects for which file at given `path` has been modified.
1151
1151
1152 :param limit: Optional. Allows to limit the size of the returned
1152 :param limit: Optional. Allows to limit the size of the returned
1153 history. This is intended as a hint to the underlying backend, so
1153 history. This is intended as a hint to the underlying backend, so
1154 that it can apply optimizations depending on the limit.
1154 that it can apply optimizations depending on the limit.
1155 :param pre_load: Optional. List of commit attributes to load.
1155 :param pre_load: Optional. List of commit attributes to load.
1156 """
1156 """
1157 raise NotImplementedError
1157 raise NotImplementedError
1158
1158
1159 def get_file_annotate(self, path, pre_load=None):
1159 def get_file_annotate(self, path, pre_load=None):
1160 """
1160 """
1161 Returns a generator of four element tuples with
1161 Returns a generator of four element tuples with
1162 lineno, sha, commit lazy loader and line
1162 lineno, sha, commit lazy loader and line
1163
1163
1164 :param pre_load: Optional. List of commit attributes to load.
1164 :param pre_load: Optional. List of commit attributes to load.
1165 """
1165 """
1166 raise NotImplementedError
1166 raise NotImplementedError
1167
1167
1168 def get_nodes(self, path):
1168 def get_nodes(self, path):
1169 """
1169 """
1170 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1170 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1171 state of commit at the given ``path``.
1171 state of commit at the given ``path``.
1172
1172
1173 :raises ``CommitError``: if node at the given ``path`` is not
1173 :raises ``CommitError``: if node at the given ``path`` is not
1174 instance of ``DirNode``
1174 instance of ``DirNode``
1175 """
1175 """
1176 raise NotImplementedError
1176 raise NotImplementedError
1177
1177
1178 def get_node(self, path):
1178 def get_node(self, path):
1179 """
1179 """
1180 Returns ``Node`` object from the given ``path``.
1180 Returns ``Node`` object from the given ``path``.
1181
1181
1182 :raises ``NodeDoesNotExistError``: if there is no node at the given
1182 :raises ``NodeDoesNotExistError``: if there is no node at the given
1183 ``path``
1183 ``path``
1184 """
1184 """
1185 raise NotImplementedError
1185 raise NotImplementedError
1186
1186
1187 def get_largefile_node(self, path):
1187 def get_largefile_node(self, path):
1188 """
1188 """
1189 Returns the path to largefile from Mercurial/Git-lfs storage.
1189 Returns the path to largefile from Mercurial/Git-lfs storage.
1190 or None if it's not a largefile node
1190 or None if it's not a largefile node
1191 """
1191 """
1192 return None
1192 return None
1193
1193
1194 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1194 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1195 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1195 archive_dir_name=None, write_metadata=False, mtime=None,
1196 archive_at_path='/', with_hash=True):
1196 """
1197 """
1197 Creates an archive containing the contents of the repository.
1198 Creates an archive containing the contents of the repository.
1198
1199
1199 :param archive_dest_path: path to the file which to create the archive.
1200 :param archive_dest_path: path to the file which to create the archive.
1200 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1201 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1201 :param prefix: name of root directory in archive.
1202 :param archive_dir_name: name of root directory in archive.
1202 Default is repository name and commit's short_id joined with dash:
1203 Default is repository name and commit's short_id joined with dash:
1203 ``"{repo_name}-{short_id}"``.
1204 ``"{repo_name}-{short_id}"``.
1204 :param write_metadata: write a metadata file into archive.
1205 :param write_metadata: write a metadata file into archive.
1205 :param mtime: custom modification time for archive creation, defaults
1206 :param mtime: custom modification time for archive creation, defaults
1206 to time.time() if not given.
1207 to time.time() if not given.
1207 :param archive_at_path: pack files at this path (default '/')
1208 :param archive_at_path: pack files at this path (default '/')
1208
1209
1209 :raise VCSError: If prefix has a problem.
1210 :raise VCSError: If prefix has a problem.
1210 """
1211 """
1211 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1212 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1212 if kind not in allowed_kinds:
1213 if kind not in allowed_kinds:
1213 raise ImproperArchiveTypeError(
1214 raise ImproperArchiveTypeError(
1214 'Archive kind (%s) not supported use one of %s' %
1215 'Archive kind (%s) not supported use one of %s' %
1215 (kind, allowed_kinds))
1216 (kind, allowed_kinds))
1216
1217
1217 prefix = self._validate_archive_prefix(prefix)
1218 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1218
1219
1219 mtime = mtime is not None or time.mktime(self.date.timetuple())
1220 mtime = mtime is not None or time.mktime(self.date.timetuple())
1220
1221
1221 file_info = []
1222 file_info = []
1222 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1223 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1223 for _r, _d, files in cur_rev.walk(archive_at_path):
1224 for _r, _d, files in cur_rev.walk(archive_at_path):
1224 for f in files:
1225 for f in files:
1225 f_path = os.path.join(prefix, f.path)
1226 f_path = os.path.join(archive_dir_name, f.path)
1226 file_info.append(
1227 file_info.append(
1227 (f_path, f.mode, f.is_link(), f.raw_bytes))
1228 (f_path, f.mode, f.is_link(), f.raw_bytes))
1228
1229
1229 if write_metadata:
1230 if write_metadata:
1230 metadata = [
1231 metadata = [
1231 ('repo_name', self.repository.name),
1232 ('repo_name', self.repository.name),
1232 ('commit_id', self.raw_id),
1233 ('commit_id', self.raw_id),
1233 ('mtime', mtime),
1234 ('mtime', mtime),
1234 ('branch', self.branch),
1235 ('branch', self.branch),
1235 ('tags', ','.join(self.tags)),
1236 ('tags', ','.join(self.tags)),
1236 ]
1237 ]
1237 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1238 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1238 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1239 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1239
1240
1240 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1241 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1241
1242
1242 def _validate_archive_prefix(self, prefix):
1243 def _validate_archive_prefix(self, archive_dir_name):
1243 if prefix is None:
1244 if archive_dir_name is None:
1244 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1245 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1245 repo_name=safe_str(self.repository.name),
1246 repo_name=safe_str(self.repository.name),
1246 short_id=self.short_id)
1247 short_id=self.short_id)
1247 elif not isinstance(prefix, str):
1248 elif not isinstance(archive_dir_name, str):
1248 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1249 raise ValueError("prefix not a bytes object: %s" % repr(archive_dir_name))
1249 elif prefix.startswith('/'):
1250 elif archive_dir_name.startswith('/'):
1250 raise VCSError("Prefix cannot start with leading slash")
1251 raise VCSError("Prefix cannot start with leading slash")
1251 elif prefix.strip() == '':
1252 elif archive_dir_name.strip() == '':
1252 raise VCSError("Prefix cannot be empty")
1253 raise VCSError("Prefix cannot be empty")
1253 return prefix
1254 return archive_dir_name
1254
1255
1255 @LazyProperty
1256 @LazyProperty
1256 def root(self):
1257 def root(self):
1257 """
1258 """
1258 Returns ``RootNode`` object for this commit.
1259 Returns ``RootNode`` object for this commit.
1259 """
1260 """
1260 return self.get_node('')
1261 return self.get_node('')
1261
1262
1262 def next(self, branch=None):
1263 def next(self, branch=None):
1263 """
1264 """
1264 Returns next commit from current, if branch is gives it will return
1265 Returns next commit from current, if branch is gives it will return
1265 next commit belonging to this branch
1266 next commit belonging to this branch
1266
1267
1267 :param branch: show commits within the given named branch
1268 :param branch: show commits within the given named branch
1268 """
1269 """
1269 indexes = xrange(self.idx + 1, self.repository.count())
1270 indexes = xrange(self.idx + 1, self.repository.count())
1270 return self._find_next(indexes, branch)
1271 return self._find_next(indexes, branch)
1271
1272
1272 def prev(self, branch=None):
1273 def prev(self, branch=None):
1273 """
1274 """
1274 Returns previous commit from current, if branch is gives it will
1275 Returns previous commit from current, if branch is gives it will
1275 return previous commit belonging to this branch
1276 return previous commit belonging to this branch
1276
1277
1277 :param branch: show commit within the given named branch
1278 :param branch: show commit within the given named branch
1278 """
1279 """
1279 indexes = xrange(self.idx - 1, -1, -1)
1280 indexes = xrange(self.idx - 1, -1, -1)
1280 return self._find_next(indexes, branch)
1281 return self._find_next(indexes, branch)
1281
1282
1282 def _find_next(self, indexes, branch=None):
1283 def _find_next(self, indexes, branch=None):
1283 if branch and self.branch != branch:
1284 if branch and self.branch != branch:
1284 raise VCSError('Branch option used on commit not belonging '
1285 raise VCSError('Branch option used on commit not belonging '
1285 'to that branch')
1286 'to that branch')
1286
1287
1287 for next_idx in indexes:
1288 for next_idx in indexes:
1288 commit = self.repository.get_commit(commit_idx=next_idx)
1289 commit = self.repository.get_commit(commit_idx=next_idx)
1289 if branch and branch != commit.branch:
1290 if branch and branch != commit.branch:
1290 continue
1291 continue
1291 return commit
1292 return commit
1292 raise CommitDoesNotExistError
1293 raise CommitDoesNotExistError
1293
1294
1294 def diff(self, ignore_whitespace=True, context=3):
1295 def diff(self, ignore_whitespace=True, context=3):
1295 """
1296 """
1296 Returns a `Diff` object representing the change made by this commit.
1297 Returns a `Diff` object representing the change made by this commit.
1297 """
1298 """
1298 parent = self.first_parent
1299 parent = self.first_parent
1299 diff = self.repository.get_diff(
1300 diff = self.repository.get_diff(
1300 parent, self,
1301 parent, self,
1301 ignore_whitespace=ignore_whitespace,
1302 ignore_whitespace=ignore_whitespace,
1302 context=context)
1303 context=context)
1303 return diff
1304 return diff
1304
1305
1305 @LazyProperty
1306 @LazyProperty
1306 def added(self):
1307 def added(self):
1307 """
1308 """
1308 Returns list of added ``FileNode`` objects.
1309 Returns list of added ``FileNode`` objects.
1309 """
1310 """
1310 raise NotImplementedError
1311 raise NotImplementedError
1311
1312
1312 @LazyProperty
1313 @LazyProperty
1313 def changed(self):
1314 def changed(self):
1314 """
1315 """
1315 Returns list of modified ``FileNode`` objects.
1316 Returns list of modified ``FileNode`` objects.
1316 """
1317 """
1317 raise NotImplementedError
1318 raise NotImplementedError
1318
1319
1319 @LazyProperty
1320 @LazyProperty
1320 def removed(self):
1321 def removed(self):
1321 """
1322 """
1322 Returns list of removed ``FileNode`` objects.
1323 Returns list of removed ``FileNode`` objects.
1323 """
1324 """
1324 raise NotImplementedError
1325 raise NotImplementedError
1325
1326
1326 @LazyProperty
1327 @LazyProperty
1327 def size(self):
1328 def size(self):
1328 """
1329 """
1329 Returns total number of bytes from contents of all filenodes.
1330 Returns total number of bytes from contents of all filenodes.
1330 """
1331 """
1331 return sum((node.size for node in self.get_filenodes_generator()))
1332 return sum((node.size for node in self.get_filenodes_generator()))
1332
1333
1333 def walk(self, topurl=''):
1334 def walk(self, topurl=''):
1334 """
1335 """
1335 Similar to os.walk method. Insted of filesystem it walks through
1336 Similar to os.walk method. Insted of filesystem it walks through
1336 commit starting at given ``topurl``. Returns generator of tuples
1337 commit starting at given ``topurl``. Returns generator of tuples
1337 (topnode, dirnodes, filenodes).
1338 (topnode, dirnodes, filenodes).
1338 """
1339 """
1339 topnode = self.get_node(topurl)
1340 topnode = self.get_node(topurl)
1340 if not topnode.is_dir():
1341 if not topnode.is_dir():
1341 return
1342 return
1342 yield (topnode, topnode.dirs, topnode.files)
1343 yield (topnode, topnode.dirs, topnode.files)
1343 for dirnode in topnode.dirs:
1344 for dirnode in topnode.dirs:
1344 for tup in self.walk(dirnode.path):
1345 for tup in self.walk(dirnode.path):
1345 yield tup
1346 yield tup
1346
1347
1347 def get_filenodes_generator(self):
1348 def get_filenodes_generator(self):
1348 """
1349 """
1349 Returns generator that yields *all* file nodes.
1350 Returns generator that yields *all* file nodes.
1350 """
1351 """
1351 for topnode, dirs, files in self.walk():
1352 for topnode, dirs, files in self.walk():
1352 for node in files:
1353 for node in files:
1353 yield node
1354 yield node
1354
1355
1355 #
1356 #
1356 # Utilities for sub classes to support consistent behavior
1357 # Utilities for sub classes to support consistent behavior
1357 #
1358 #
1358
1359
1359 def no_node_at_path(self, path):
1360 def no_node_at_path(self, path):
1360 return NodeDoesNotExistError(
1361 return NodeDoesNotExistError(
1361 u"There is no file nor directory at the given path: "
1362 u"There is no file nor directory at the given path: "
1362 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1363 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1363
1364
1364 def _fix_path(self, path):
1365 def _fix_path(self, path):
1365 """
1366 """
1366 Paths are stored without trailing slash so we need to get rid off it if
1367 Paths are stored without trailing slash so we need to get rid off it if
1367 needed.
1368 needed.
1368 """
1369 """
1369 return path.rstrip('/')
1370 return path.rstrip('/')
1370
1371
1371 #
1372 #
1372 # Deprecated API based on changesets
1373 # Deprecated API based on changesets
1373 #
1374 #
1374
1375
1375 @property
1376 @property
1376 def revision(self):
1377 def revision(self):
1377 warnings.warn("Use idx instead", DeprecationWarning)
1378 warnings.warn("Use idx instead", DeprecationWarning)
1378 return self.idx
1379 return self.idx
1379
1380
1380 @revision.setter
1381 @revision.setter
1381 def revision(self, value):
1382 def revision(self, value):
1382 warnings.warn("Use idx instead", DeprecationWarning)
1383 warnings.warn("Use idx instead", DeprecationWarning)
1383 self.idx = value
1384 self.idx = value
1384
1385
1385 def get_file_changeset(self, path):
1386 def get_file_changeset(self, path):
1386 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1387 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1387 return self.get_path_commit(path)
1388 return self.get_path_commit(path)
1388
1389
1389
1390
1390 class BaseChangesetClass(type):
1391 class BaseChangesetClass(type):
1391
1392
1392 def __instancecheck__(self, instance):
1393 def __instancecheck__(self, instance):
1393 return isinstance(instance, BaseCommit)
1394 return isinstance(instance, BaseCommit)
1394
1395
1395
1396
1396 class BaseChangeset(BaseCommit):
1397 class BaseChangeset(BaseCommit):
1397
1398
1398 __metaclass__ = BaseChangesetClass
1399 __metaclass__ = BaseChangesetClass
1399
1400
1400 def __new__(cls, *args, **kwargs):
1401 def __new__(cls, *args, **kwargs):
1401 warnings.warn(
1402 warnings.warn(
1402 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1403 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1403 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1404 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1404
1405
1405
1406
1406 class BaseInMemoryCommit(object):
1407 class BaseInMemoryCommit(object):
1407 """
1408 """
1408 Represents differences between repository's state (most recent head) and
1409 Represents differences between repository's state (most recent head) and
1409 changes made *in place*.
1410 changes made *in place*.
1410
1411
1411 **Attributes**
1412 **Attributes**
1412
1413
1413 ``repository``
1414 ``repository``
1414 repository object for this in-memory-commit
1415 repository object for this in-memory-commit
1415
1416
1416 ``added``
1417 ``added``
1417 list of ``FileNode`` objects marked as *added*
1418 list of ``FileNode`` objects marked as *added*
1418
1419
1419 ``changed``
1420 ``changed``
1420 list of ``FileNode`` objects marked as *changed*
1421 list of ``FileNode`` objects marked as *changed*
1421
1422
1422 ``removed``
1423 ``removed``
1423 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1424 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1424 *removed*
1425 *removed*
1425
1426
1426 ``parents``
1427 ``parents``
1427 list of :class:`BaseCommit` instances representing parents of
1428 list of :class:`BaseCommit` instances representing parents of
1428 in-memory commit. Should always be 2-element sequence.
1429 in-memory commit. Should always be 2-element sequence.
1429
1430
1430 """
1431 """
1431
1432
1432 def __init__(self, repository):
1433 def __init__(self, repository):
1433 self.repository = repository
1434 self.repository = repository
1434 self.added = []
1435 self.added = []
1435 self.changed = []
1436 self.changed = []
1436 self.removed = []
1437 self.removed = []
1437 self.parents = []
1438 self.parents = []
1438
1439
1439 def add(self, *filenodes):
1440 def add(self, *filenodes):
1440 """
1441 """
1441 Marks given ``FileNode`` objects as *to be committed*.
1442 Marks given ``FileNode`` objects as *to be committed*.
1442
1443
1443 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1444 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1444 latest commit
1445 latest commit
1445 :raises ``NodeAlreadyAddedError``: if node with same path is already
1446 :raises ``NodeAlreadyAddedError``: if node with same path is already
1446 marked as *added*
1447 marked as *added*
1447 """
1448 """
1448 # Check if not already marked as *added* first
1449 # Check if not already marked as *added* first
1449 for node in filenodes:
1450 for node in filenodes:
1450 if node.path in (n.path for n in self.added):
1451 if node.path in (n.path for n in self.added):
1451 raise NodeAlreadyAddedError(
1452 raise NodeAlreadyAddedError(
1452 "Such FileNode %s is already marked for addition"
1453 "Such FileNode %s is already marked for addition"
1453 % node.path)
1454 % node.path)
1454 for node in filenodes:
1455 for node in filenodes:
1455 self.added.append(node)
1456 self.added.append(node)
1456
1457
1457 def change(self, *filenodes):
1458 def change(self, *filenodes):
1458 """
1459 """
1459 Marks given ``FileNode`` objects to be *changed* in next commit.
1460 Marks given ``FileNode`` objects to be *changed* in next commit.
1460
1461
1461 :raises ``EmptyRepositoryError``: if there are no commits yet
1462 :raises ``EmptyRepositoryError``: if there are no commits yet
1462 :raises ``NodeAlreadyExistsError``: if node with same path is already
1463 :raises ``NodeAlreadyExistsError``: if node with same path is already
1463 marked to be *changed*
1464 marked to be *changed*
1464 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1465 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1465 marked to be *removed*
1466 marked to be *removed*
1466 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1467 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1467 commit
1468 commit
1468 :raises ``NodeNotChangedError``: if node hasn't really be changed
1469 :raises ``NodeNotChangedError``: if node hasn't really be changed
1469 """
1470 """
1470 for node in filenodes:
1471 for node in filenodes:
1471 if node.path in (n.path for n in self.removed):
1472 if node.path in (n.path for n in self.removed):
1472 raise NodeAlreadyRemovedError(
1473 raise NodeAlreadyRemovedError(
1473 "Node at %s is already marked as removed" % node.path)
1474 "Node at %s is already marked as removed" % node.path)
1474 try:
1475 try:
1475 self.repository.get_commit()
1476 self.repository.get_commit()
1476 except EmptyRepositoryError:
1477 except EmptyRepositoryError:
1477 raise EmptyRepositoryError(
1478 raise EmptyRepositoryError(
1478 "Nothing to change - try to *add* new nodes rather than "
1479 "Nothing to change - try to *add* new nodes rather than "
1479 "changing them")
1480 "changing them")
1480 for node in filenodes:
1481 for node in filenodes:
1481 if node.path in (n.path for n in self.changed):
1482 if node.path in (n.path for n in self.changed):
1482 raise NodeAlreadyChangedError(
1483 raise NodeAlreadyChangedError(
1483 "Node at '%s' is already marked as changed" % node.path)
1484 "Node at '%s' is already marked as changed" % node.path)
1484 self.changed.append(node)
1485 self.changed.append(node)
1485
1486
1486 def remove(self, *filenodes):
1487 def remove(self, *filenodes):
1487 """
1488 """
1488 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1489 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1489 *removed* in next commit.
1490 *removed* in next commit.
1490
1491
1491 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1492 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1492 be *removed*
1493 be *removed*
1493 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1494 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1494 be *changed*
1495 be *changed*
1495 """
1496 """
1496 for node in filenodes:
1497 for node in filenodes:
1497 if node.path in (n.path for n in self.removed):
1498 if node.path in (n.path for n in self.removed):
1498 raise NodeAlreadyRemovedError(
1499 raise NodeAlreadyRemovedError(
1499 "Node is already marked to for removal at %s" % node.path)
1500 "Node is already marked to for removal at %s" % node.path)
1500 if node.path in (n.path for n in self.changed):
1501 if node.path in (n.path for n in self.changed):
1501 raise NodeAlreadyChangedError(
1502 raise NodeAlreadyChangedError(
1502 "Node is already marked to be changed at %s" % node.path)
1503 "Node is already marked to be changed at %s" % node.path)
1503 # We only mark node as *removed* - real removal is done by
1504 # We only mark node as *removed* - real removal is done by
1504 # commit method
1505 # commit method
1505 self.removed.append(node)
1506 self.removed.append(node)
1506
1507
1507 def reset(self):
1508 def reset(self):
1508 """
1509 """
1509 Resets this instance to initial state (cleans ``added``, ``changed``
1510 Resets this instance to initial state (cleans ``added``, ``changed``
1510 and ``removed`` lists).
1511 and ``removed`` lists).
1511 """
1512 """
1512 self.added = []
1513 self.added = []
1513 self.changed = []
1514 self.changed = []
1514 self.removed = []
1515 self.removed = []
1515 self.parents = []
1516 self.parents = []
1516
1517
1517 def get_ipaths(self):
1518 def get_ipaths(self):
1518 """
1519 """
1519 Returns generator of paths from nodes marked as added, changed or
1520 Returns generator of paths from nodes marked as added, changed or
1520 removed.
1521 removed.
1521 """
1522 """
1522 for node in itertools.chain(self.added, self.changed, self.removed):
1523 for node in itertools.chain(self.added, self.changed, self.removed):
1523 yield node.path
1524 yield node.path
1524
1525
1525 def get_paths(self):
1526 def get_paths(self):
1526 """
1527 """
1527 Returns list of paths from nodes marked as added, changed or removed.
1528 Returns list of paths from nodes marked as added, changed or removed.
1528 """
1529 """
1529 return list(self.get_ipaths())
1530 return list(self.get_ipaths())
1530
1531
1531 def check_integrity(self, parents=None):
1532 def check_integrity(self, parents=None):
1532 """
1533 """
1533 Checks in-memory commit's integrity. Also, sets parents if not
1534 Checks in-memory commit's integrity. Also, sets parents if not
1534 already set.
1535 already set.
1535
1536
1536 :raises CommitError: if any error occurs (i.e.
1537 :raises CommitError: if any error occurs (i.e.
1537 ``NodeDoesNotExistError``).
1538 ``NodeDoesNotExistError``).
1538 """
1539 """
1539 if not self.parents:
1540 if not self.parents:
1540 parents = parents or []
1541 parents = parents or []
1541 if len(parents) == 0:
1542 if len(parents) == 0:
1542 try:
1543 try:
1543 parents = [self.repository.get_commit(), None]
1544 parents = [self.repository.get_commit(), None]
1544 except EmptyRepositoryError:
1545 except EmptyRepositoryError:
1545 parents = [None, None]
1546 parents = [None, None]
1546 elif len(parents) == 1:
1547 elif len(parents) == 1:
1547 parents += [None]
1548 parents += [None]
1548 self.parents = parents
1549 self.parents = parents
1549
1550
1550 # Local parents, only if not None
1551 # Local parents, only if not None
1551 parents = [p for p in self.parents if p]
1552 parents = [p for p in self.parents if p]
1552
1553
1553 # Check nodes marked as added
1554 # Check nodes marked as added
1554 for p in parents:
1555 for p in parents:
1555 for node in self.added:
1556 for node in self.added:
1556 try:
1557 try:
1557 p.get_node(node.path)
1558 p.get_node(node.path)
1558 except NodeDoesNotExistError:
1559 except NodeDoesNotExistError:
1559 pass
1560 pass
1560 else:
1561 else:
1561 raise NodeAlreadyExistsError(
1562 raise NodeAlreadyExistsError(
1562 "Node `%s` already exists at %s" % (node.path, p))
1563 "Node `%s` already exists at %s" % (node.path, p))
1563
1564
1564 # Check nodes marked as changed
1565 # Check nodes marked as changed
1565 missing = set(self.changed)
1566 missing = set(self.changed)
1566 not_changed = set(self.changed)
1567 not_changed = set(self.changed)
1567 if self.changed and not parents:
1568 if self.changed and not parents:
1568 raise NodeDoesNotExistError(str(self.changed[0].path))
1569 raise NodeDoesNotExistError(str(self.changed[0].path))
1569 for p in parents:
1570 for p in parents:
1570 for node in self.changed:
1571 for node in self.changed:
1571 try:
1572 try:
1572 old = p.get_node(node.path)
1573 old = p.get_node(node.path)
1573 missing.remove(node)
1574 missing.remove(node)
1574 # if content actually changed, remove node from not_changed
1575 # if content actually changed, remove node from not_changed
1575 if old.content != node.content:
1576 if old.content != node.content:
1576 not_changed.remove(node)
1577 not_changed.remove(node)
1577 except NodeDoesNotExistError:
1578 except NodeDoesNotExistError:
1578 pass
1579 pass
1579 if self.changed and missing:
1580 if self.changed and missing:
1580 raise NodeDoesNotExistError(
1581 raise NodeDoesNotExistError(
1581 "Node `%s` marked as modified but missing in parents: %s"
1582 "Node `%s` marked as modified but missing in parents: %s"
1582 % (node.path, parents))
1583 % (node.path, parents))
1583
1584
1584 if self.changed and not_changed:
1585 if self.changed and not_changed:
1585 raise NodeNotChangedError(
1586 raise NodeNotChangedError(
1586 "Node `%s` wasn't actually changed (parents: %s)"
1587 "Node `%s` wasn't actually changed (parents: %s)"
1587 % (not_changed.pop().path, parents))
1588 % (not_changed.pop().path, parents))
1588
1589
1589 # Check nodes marked as removed
1590 # Check nodes marked as removed
1590 if self.removed and not parents:
1591 if self.removed and not parents:
1591 raise NodeDoesNotExistError(
1592 raise NodeDoesNotExistError(
1592 "Cannot remove node at %s as there "
1593 "Cannot remove node at %s as there "
1593 "were no parents specified" % self.removed[0].path)
1594 "were no parents specified" % self.removed[0].path)
1594 really_removed = set()
1595 really_removed = set()
1595 for p in parents:
1596 for p in parents:
1596 for node in self.removed:
1597 for node in self.removed:
1597 try:
1598 try:
1598 p.get_node(node.path)
1599 p.get_node(node.path)
1599 really_removed.add(node)
1600 really_removed.add(node)
1600 except CommitError:
1601 except CommitError:
1601 pass
1602 pass
1602 not_removed = set(self.removed) - really_removed
1603 not_removed = set(self.removed) - really_removed
1603 if not_removed:
1604 if not_removed:
1604 # TODO: johbo: This code branch does not seem to be covered
1605 # TODO: johbo: This code branch does not seem to be covered
1605 raise NodeDoesNotExistError(
1606 raise NodeDoesNotExistError(
1606 "Cannot remove node at %s from "
1607 "Cannot remove node at %s from "
1607 "following parents: %s" % (not_removed, parents))
1608 "following parents: %s" % (not_removed, parents))
1608
1609
1609 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1610 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1610 """
1611 """
1611 Performs in-memory commit (doesn't check workdir in any way) and
1612 Performs in-memory commit (doesn't check workdir in any way) and
1612 returns newly created :class:`BaseCommit`. Updates repository's
1613 returns newly created :class:`BaseCommit`. Updates repository's
1613 attribute `commits`.
1614 attribute `commits`.
1614
1615
1615 .. note::
1616 .. note::
1616
1617
1617 While overriding this method each backend's should call
1618 While overriding this method each backend's should call
1618 ``self.check_integrity(parents)`` in the first place.
1619 ``self.check_integrity(parents)`` in the first place.
1619
1620
1620 :param message: message of the commit
1621 :param message: message of the commit
1621 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1622 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1622 :param parents: single parent or sequence of parents from which commit
1623 :param parents: single parent or sequence of parents from which commit
1623 would be derived
1624 would be derived
1624 :param date: ``datetime.datetime`` instance. Defaults to
1625 :param date: ``datetime.datetime`` instance. Defaults to
1625 ``datetime.datetime.now()``.
1626 ``datetime.datetime.now()``.
1626 :param branch: branch name, as string. If none given, default backend's
1627 :param branch: branch name, as string. If none given, default backend's
1627 branch would be used.
1628 branch would be used.
1628
1629
1629 :raises ``CommitError``: if any error occurs while committing
1630 :raises ``CommitError``: if any error occurs while committing
1630 """
1631 """
1631 raise NotImplementedError
1632 raise NotImplementedError
1632
1633
1633
1634
1634 class BaseInMemoryChangesetClass(type):
1635 class BaseInMemoryChangesetClass(type):
1635
1636
1636 def __instancecheck__(self, instance):
1637 def __instancecheck__(self, instance):
1637 return isinstance(instance, BaseInMemoryCommit)
1638 return isinstance(instance, BaseInMemoryCommit)
1638
1639
1639
1640
1640 class BaseInMemoryChangeset(BaseInMemoryCommit):
1641 class BaseInMemoryChangeset(BaseInMemoryCommit):
1641
1642
1642 __metaclass__ = BaseInMemoryChangesetClass
1643 __metaclass__ = BaseInMemoryChangesetClass
1643
1644
1644 def __new__(cls, *args, **kwargs):
1645 def __new__(cls, *args, **kwargs):
1645 warnings.warn(
1646 warnings.warn(
1646 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1647 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1647 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1648 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1648
1649
1649
1650
1650 class EmptyCommit(BaseCommit):
1651 class EmptyCommit(BaseCommit):
1651 """
1652 """
1652 An dummy empty commit. It's possible to pass hash when creating
1653 An dummy empty commit. It's possible to pass hash when creating
1653 an EmptyCommit
1654 an EmptyCommit
1654 """
1655 """
1655
1656
1656 def __init__(
1657 def __init__(
1657 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1658 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1658 message='', author='', date=None):
1659 message='', author='', date=None):
1659 self._empty_commit_id = commit_id
1660 self._empty_commit_id = commit_id
1660 # TODO: johbo: Solve idx parameter, default value does not make
1661 # TODO: johbo: Solve idx parameter, default value does not make
1661 # too much sense
1662 # too much sense
1662 self.idx = idx
1663 self.idx = idx
1663 self.message = message
1664 self.message = message
1664 self.author = author
1665 self.author = author
1665 self.date = date or datetime.datetime.fromtimestamp(0)
1666 self.date = date or datetime.datetime.fromtimestamp(0)
1666 self.repository = repo
1667 self.repository = repo
1667 self.alias = alias
1668 self.alias = alias
1668
1669
1669 @LazyProperty
1670 @LazyProperty
1670 def raw_id(self):
1671 def raw_id(self):
1671 """
1672 """
1672 Returns raw string identifying this commit, useful for web
1673 Returns raw string identifying this commit, useful for web
1673 representation.
1674 representation.
1674 """
1675 """
1675
1676
1676 return self._empty_commit_id
1677 return self._empty_commit_id
1677
1678
1678 @LazyProperty
1679 @LazyProperty
1679 def branch(self):
1680 def branch(self):
1680 if self.alias:
1681 if self.alias:
1681 from rhodecode.lib.vcs.backends import get_backend
1682 from rhodecode.lib.vcs.backends import get_backend
1682 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1683 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1683
1684
1684 @LazyProperty
1685 @LazyProperty
1685 def short_id(self):
1686 def short_id(self):
1686 return self.raw_id[:12]
1687 return self.raw_id[:12]
1687
1688
1688 @LazyProperty
1689 @LazyProperty
1689 def id(self):
1690 def id(self):
1690 return self.raw_id
1691 return self.raw_id
1691
1692
1692 def get_path_commit(self, path):
1693 def get_path_commit(self, path):
1693 return self
1694 return self
1694
1695
1695 def get_file_content(self, path):
1696 def get_file_content(self, path):
1696 return u''
1697 return u''
1697
1698
1698 def get_file_content_streamed(self, path):
1699 def get_file_content_streamed(self, path):
1699 yield self.get_file_content()
1700 yield self.get_file_content()
1700
1701
1701 def get_file_size(self, path):
1702 def get_file_size(self, path):
1702 return 0
1703 return 0
1703
1704
1704
1705
1705 class EmptyChangesetClass(type):
1706 class EmptyChangesetClass(type):
1706
1707
1707 def __instancecheck__(self, instance):
1708 def __instancecheck__(self, instance):
1708 return isinstance(instance, EmptyCommit)
1709 return isinstance(instance, EmptyCommit)
1709
1710
1710
1711
1711 class EmptyChangeset(EmptyCommit):
1712 class EmptyChangeset(EmptyCommit):
1712
1713
1713 __metaclass__ = EmptyChangesetClass
1714 __metaclass__ = EmptyChangesetClass
1714
1715
1715 def __new__(cls, *args, **kwargs):
1716 def __new__(cls, *args, **kwargs):
1716 warnings.warn(
1717 warnings.warn(
1717 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1718 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1718 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1719 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1719
1720
1720 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1721 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1721 alias=None, revision=-1, message='', author='', date=None):
1722 alias=None, revision=-1, message='', author='', date=None):
1722 if requested_revision is not None:
1723 if requested_revision is not None:
1723 warnings.warn(
1724 warnings.warn(
1724 "Parameter requested_revision not supported anymore",
1725 "Parameter requested_revision not supported anymore",
1725 DeprecationWarning)
1726 DeprecationWarning)
1726 super(EmptyChangeset, self).__init__(
1727 super(EmptyChangeset, self).__init__(
1727 commit_id=cs, repo=repo, alias=alias, idx=revision,
1728 commit_id=cs, repo=repo, alias=alias, idx=revision,
1728 message=message, author=author, date=date)
1729 message=message, author=author, date=date)
1729
1730
1730 @property
1731 @property
1731 def revision(self):
1732 def revision(self):
1732 warnings.warn("Use idx instead", DeprecationWarning)
1733 warnings.warn("Use idx instead", DeprecationWarning)
1733 return self.idx
1734 return self.idx
1734
1735
1735 @revision.setter
1736 @revision.setter
1736 def revision(self, value):
1737 def revision(self, value):
1737 warnings.warn("Use idx instead", DeprecationWarning)
1738 warnings.warn("Use idx instead", DeprecationWarning)
1738 self.idx = value
1739 self.idx = value
1739
1740
1740
1741
1741 class EmptyRepository(BaseRepository):
1742 class EmptyRepository(BaseRepository):
1742 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1743 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1743 pass
1744 pass
1744
1745
1745 def get_diff(self, *args, **kwargs):
1746 def get_diff(self, *args, **kwargs):
1746 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1747 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1747 return GitDiff('')
1748 return GitDiff('')
1748
1749
1749
1750
1750 class CollectionGenerator(object):
1751 class CollectionGenerator(object):
1751
1752
1752 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1753 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1753 self.repo = repo
1754 self.repo = repo
1754 self.commit_ids = commit_ids
1755 self.commit_ids = commit_ids
1755 # TODO: (oliver) this isn't currently hooked up
1756 # TODO: (oliver) this isn't currently hooked up
1756 self.collection_size = None
1757 self.collection_size = None
1757 self.pre_load = pre_load
1758 self.pre_load = pre_load
1758 self.translate_tag = translate_tag
1759 self.translate_tag = translate_tag
1759
1760
1760 def __len__(self):
1761 def __len__(self):
1761 if self.collection_size is not None:
1762 if self.collection_size is not None:
1762 return self.collection_size
1763 return self.collection_size
1763 return self.commit_ids.__len__()
1764 return self.commit_ids.__len__()
1764
1765
1765 def __iter__(self):
1766 def __iter__(self):
1766 for commit_id in self.commit_ids:
1767 for commit_id in self.commit_ids:
1767 # TODO: johbo: Mercurial passes in commit indices or commit ids
1768 # TODO: johbo: Mercurial passes in commit indices or commit ids
1768 yield self._commit_factory(commit_id)
1769 yield self._commit_factory(commit_id)
1769
1770
1770 def _commit_factory(self, commit_id):
1771 def _commit_factory(self, commit_id):
1771 """
1772 """
1772 Allows backends to override the way commits are generated.
1773 Allows backends to override the way commits are generated.
1773 """
1774 """
1774 return self.repo.get_commit(
1775 return self.repo.get_commit(
1775 commit_id=commit_id, pre_load=self.pre_load,
1776 commit_id=commit_id, pre_load=self.pre_load,
1776 translate_tag=self.translate_tag)
1777 translate_tag=self.translate_tag)
1777
1778
1778 def __getslice__(self, i, j):
1779 def __getslice__(self, i, j):
1779 """
1780 """
1780 Returns an iterator of sliced repository
1781 Returns an iterator of sliced repository
1781 """
1782 """
1782 commit_ids = self.commit_ids[i:j]
1783 commit_ids = self.commit_ids[i:j]
1783 return self.__class__(
1784 return self.__class__(
1784 self.repo, commit_ids, pre_load=self.pre_load,
1785 self.repo, commit_ids, pre_load=self.pre_load,
1785 translate_tag=self.translate_tag)
1786 translate_tag=self.translate_tag)
1786
1787
1787 def __repr__(self):
1788 def __repr__(self):
1788 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1789 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1789
1790
1790
1791
1791 class Config(object):
1792 class Config(object):
1792 """
1793 """
1793 Represents the configuration for a repository.
1794 Represents the configuration for a repository.
1794
1795
1795 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1796 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1796 standard library. It implements only the needed subset.
1797 standard library. It implements only the needed subset.
1797 """
1798 """
1798
1799
1799 def __init__(self):
1800 def __init__(self):
1800 self._values = {}
1801 self._values = {}
1801
1802
1802 def copy(self):
1803 def copy(self):
1803 clone = Config()
1804 clone = Config()
1804 for section, values in self._values.items():
1805 for section, values in self._values.items():
1805 clone._values[section] = values.copy()
1806 clone._values[section] = values.copy()
1806 return clone
1807 return clone
1807
1808
1808 def __repr__(self):
1809 def __repr__(self):
1809 return '<Config(%s sections) at %s>' % (
1810 return '<Config(%s sections) at %s>' % (
1810 len(self._values), hex(id(self)))
1811 len(self._values), hex(id(self)))
1811
1812
1812 def items(self, section):
1813 def items(self, section):
1813 return self._values.get(section, {}).iteritems()
1814 return self._values.get(section, {}).iteritems()
1814
1815
1815 def get(self, section, option):
1816 def get(self, section, option):
1816 return self._values.get(section, {}).get(option)
1817 return self._values.get(section, {}).get(option)
1817
1818
1818 def set(self, section, option, value):
1819 def set(self, section, option, value):
1819 section_values = self._values.setdefault(section, {})
1820 section_values = self._values.setdefault(section, {})
1820 section_values[option] = value
1821 section_values[option] = value
1821
1822
1822 def clear_section(self, section):
1823 def clear_section(self, section):
1823 self._values[section] = {}
1824 self._values[section] = {}
1824
1825
1825 def serialize(self):
1826 def serialize(self):
1826 """
1827 """
1827 Creates a list of three tuples (section, key, value) representing
1828 Creates a list of three tuples (section, key, value) representing
1828 this config object.
1829 this config object.
1829 """
1830 """
1830 items = []
1831 items = []
1831 for section in self._values:
1832 for section in self._values:
1832 for option, value in self._values[section].items():
1833 for option, value in self._values[section].items():
1833 items.append(
1834 items.append(
1834 (safe_str(section), safe_str(option), safe_str(value)))
1835 (safe_str(section), safe_str(option), safe_str(value)))
1835 return items
1836 return items
1836
1837
1837
1838
1838 class Diff(object):
1839 class Diff(object):
1839 """
1840 """
1840 Represents a diff result from a repository backend.
1841 Represents a diff result from a repository backend.
1841
1842
1842 Subclasses have to provide a backend specific value for
1843 Subclasses have to provide a backend specific value for
1843 :attr:`_header_re` and :attr:`_meta_re`.
1844 :attr:`_header_re` and :attr:`_meta_re`.
1844 """
1845 """
1845 _meta_re = None
1846 _meta_re = None
1846 _header_re = None
1847 _header_re = None
1847
1848
1848 def __init__(self, raw_diff):
1849 def __init__(self, raw_diff):
1849 self.raw = raw_diff
1850 self.raw = raw_diff
1850
1851
1851 def chunks(self):
1852 def chunks(self):
1852 """
1853 """
1853 split the diff in chunks of separate --git a/file b/file chunks
1854 split the diff in chunks of separate --git a/file b/file chunks
1854 to make diffs consistent we must prepend with \n, and make sure
1855 to make diffs consistent we must prepend with \n, and make sure
1855 we can detect last chunk as this was also has special rule
1856 we can detect last chunk as this was also has special rule
1856 """
1857 """
1857
1858
1858 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1859 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1859 header = diff_parts[0]
1860 header = diff_parts[0]
1860
1861
1861 if self._meta_re:
1862 if self._meta_re:
1862 match = self._meta_re.match(header)
1863 match = self._meta_re.match(header)
1863
1864
1864 chunks = diff_parts[1:]
1865 chunks = diff_parts[1:]
1865 total_chunks = len(chunks)
1866 total_chunks = len(chunks)
1866
1867
1867 return (
1868 return (
1868 DiffChunk(chunk, self, cur_chunk == total_chunks)
1869 DiffChunk(chunk, self, cur_chunk == total_chunks)
1869 for cur_chunk, chunk in enumerate(chunks, start=1))
1870 for cur_chunk, chunk in enumerate(chunks, start=1))
1870
1871
1871
1872
1872 class DiffChunk(object):
1873 class DiffChunk(object):
1873
1874
1874 def __init__(self, chunk, diff, last_chunk):
1875 def __init__(self, chunk, diff, last_chunk):
1875 self._diff = diff
1876 self._diff = diff
1876
1877
1877 # since we split by \ndiff --git that part is lost from original diff
1878 # since we split by \ndiff --git that part is lost from original diff
1878 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1879 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1879 if not last_chunk:
1880 if not last_chunk:
1880 chunk += '\n'
1881 chunk += '\n'
1881
1882
1882 match = self._diff._header_re.match(chunk)
1883 match = self._diff._header_re.match(chunk)
1883 self.header = match.groupdict()
1884 self.header = match.groupdict()
1884 self.diff = chunk[match.end():]
1885 self.diff = chunk[match.end():]
1885 self.raw = chunk
1886 self.raw = chunk
1886
1887
1887
1888
1888 class BasePathPermissionChecker(object):
1889 class BasePathPermissionChecker(object):
1889
1890
1890 @staticmethod
1891 @staticmethod
1891 def create_from_patterns(includes, excludes):
1892 def create_from_patterns(includes, excludes):
1892 if includes and '*' in includes and not excludes:
1893 if includes and '*' in includes and not excludes:
1893 return AllPathPermissionChecker()
1894 return AllPathPermissionChecker()
1894 elif excludes and '*' in excludes:
1895 elif excludes and '*' in excludes:
1895 return NonePathPermissionChecker()
1896 return NonePathPermissionChecker()
1896 else:
1897 else:
1897 return PatternPathPermissionChecker(includes, excludes)
1898 return PatternPathPermissionChecker(includes, excludes)
1898
1899
1899 @property
1900 @property
1900 def has_full_access(self):
1901 def has_full_access(self):
1901 raise NotImplemented()
1902 raise NotImplemented()
1902
1903
1903 def has_access(self, path):
1904 def has_access(self, path):
1904 raise NotImplemented()
1905 raise NotImplemented()
1905
1906
1906
1907
1907 class AllPathPermissionChecker(BasePathPermissionChecker):
1908 class AllPathPermissionChecker(BasePathPermissionChecker):
1908
1909
1909 @property
1910 @property
1910 def has_full_access(self):
1911 def has_full_access(self):
1911 return True
1912 return True
1912
1913
1913 def has_access(self, path):
1914 def has_access(self, path):
1914 return True
1915 return True
1915
1916
1916
1917
1917 class NonePathPermissionChecker(BasePathPermissionChecker):
1918 class NonePathPermissionChecker(BasePathPermissionChecker):
1918
1919
1919 @property
1920 @property
1920 def has_full_access(self):
1921 def has_full_access(self):
1921 return False
1922 return False
1922
1923
1923 def has_access(self, path):
1924 def has_access(self, path):
1924 return False
1925 return False
1925
1926
1926
1927
1927 class PatternPathPermissionChecker(BasePathPermissionChecker):
1928 class PatternPathPermissionChecker(BasePathPermissionChecker):
1928
1929
1929 def __init__(self, includes, excludes):
1930 def __init__(self, includes, excludes):
1930 self.includes = includes
1931 self.includes = includes
1931 self.excludes = excludes
1932 self.excludes = excludes
1932 self.includes_re = [] if not includes else [
1933 self.includes_re = [] if not includes else [
1933 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1934 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1934 self.excludes_re = [] if not excludes else [
1935 self.excludes_re = [] if not excludes else [
1935 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1936 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1936
1937
1937 @property
1938 @property
1938 def has_full_access(self):
1939 def has_full_access(self):
1939 return '*' in self.includes and not self.excludes
1940 return '*' in self.includes and not self.excludes
1940
1941
1941 def has_access(self, path):
1942 def has_access(self, path):
1942 for regex in self.excludes_re:
1943 for regex in self.excludes_re:
1943 if regex.match(path):
1944 if regex.match(path):
1944 return False
1945 return False
1945 for regex in self.includes_re:
1946 for regex in self.includes_re:
1946 if regex.match(path):
1947 if regex.match(path):
1947 return True
1948 return True
1948 return False
1949 return False
@@ -1,100 +1,100 b''
1
1
2 <div id="codeblock" class="browserblock">
2 <div id="codeblock" class="browserblock">
3 <div class="browser-header">
3 <div class="browser-header">
4 <div class="browser-nav">
4 <div class="browser-nav">
5
5
6 <div class="info_box">
6 <div class="info_box">
7
7
8 <div class="info_box_elem previous">
8 <div class="info_box_elem previous">
9 <a id="prev_commit_link" data-commit-id="${c.prev_commit.raw_id}" class=" ${('disabled' if c.url_prev == '#' else '')}" href="${c.url_prev}" title="${_('Previous commit')}"><i class="icon-left"></i></a>
9 <a id="prev_commit_link" data-commit-id="${c.prev_commit.raw_id}" class=" ${('disabled' if c.url_prev == '#' else '')}" href="${c.url_prev}" title="${_('Previous commit')}"><i class="icon-left"></i></a>
10 </div>
10 </div>
11
11
12 ${h.hidden('refs_filter')}
12 ${h.hidden('refs_filter')}
13
13
14 <div class="info_box_elem next">
14 <div class="info_box_elem next">
15 <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class=" ${('disabled' if c.url_next == '#' else '')}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a>
15 <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class=" ${('disabled' if c.url_next == '#' else '')}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a>
16 </div>
16 </div>
17 </div>
17 </div>
18
18
19 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
19 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
20
20
21 <div class="new-file">
21 <div class="new-file">
22 <div class="btn-group btn-group-actions">
22 <div class="btn-group btn-group-actions">
23 <a class="btn btn-primary no-margin" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
23 <a class="btn btn-primary no-margin" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
24 ${_('Add File')}
24 ${_('Add File')}
25 </a>
25 </a>
26
26
27 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more options')}">
27 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more options')}">
28 <i class="icon-down"></i>
28 <i class="icon-down"></i>
29 </a>
29 </a>
30
30
31 <div class="btn-action-switcher-container right-align">
31 <div class="btn-action-switcher-container right-align">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
33 <li>
33 <li>
34 <a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
34 <a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
35 <i class="icon-upload"></i>
35 <i class="icon-upload"></i>
36 ${_('Upload File')}
36 ${_('Upload File')}
37 </a>
37 </a>
38 </li>
38 </li>
39 </ul>
39 </ul>
40 </div>
40 </div>
41 </div>
41 </div>
42 </div>
42 </div>
43
43
44 % endif
44 % endif
45
45
46 % if c.enable_downloads:
46 % if c.enable_downloads:
47 <% at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
47 <% at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
48 <div class="btn btn-default new-file">
48 <div class="btn btn-default new-file">
49 % if c.f_path == '/':
49 % if c.f_path == '/':
50 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
50 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
51 ${_('Download full tree ZIP')}
51 ${_('Download full tree ZIP')}
52 </a>
52 </a>
53 % else:
53 % else:
54 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id), _query={'at_path':c.f_path})}">
54 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id), _query={'at_path':c.f_path, 'with_hash': '1'})}">
55 ${_('Download this tree ZIP')}
55 ${_('Download this tree ZIP')}
56 </a>
56 </a>
57 % endif
57 % endif
58 </div>
58 </div>
59 % endif
59 % endif
60
60
61 <div class="files-quick-filter">
61 <div class="files-quick-filter">
62 <ul class="files-filter-box">
62 <ul class="files-filter-box">
63 <li class="files-filter-box-path">
63 <li class="files-filter-box-path">
64 <i class="icon-search"></i>
64 <i class="icon-search"></i>
65 </li>
65 </li>
66 <li class="files-filter-box-input">
66 <li class="files-filter-box-input">
67 <input onkeydown="NodeFilter.initFilter(event)" class="init" type="text" placeholder="Quick filter" name="filter" size="25" id="node_filter" autocomplete="off">
67 <input onkeydown="NodeFilter.initFilter(event)" class="init" type="text" placeholder="Quick filter" name="filter" size="25" id="node_filter" autocomplete="off">
68 </li>
68 </li>
69 </ul>
69 </ul>
70 </div>
70 </div>
71 </div>
71 </div>
72
72
73 </div>
73 </div>
74
74
75 ## file tree is computed from caches, and filled in
75 ## file tree is computed from caches, and filled in
76 <div id="file-tree">
76 <div id="file-tree">
77 ${c.file_tree |n}
77 ${c.file_tree |n}
78 </div>
78 </div>
79
79
80 %if c.readme_data:
80 %if c.readme_data:
81 <div id="readme" class="anchor">
81 <div id="readme" class="anchor">
82 <div class="box">
82 <div class="box">
83 <div class="readme-title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_ref_type, c.rhodecode_db_repo.landing_ref_name))}">
83 <div class="readme-title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_ref_type, c.rhodecode_db_repo.landing_ref_name))}">
84 <div>
84 <div>
85 <i class="icon-file-text"></i>
85 <i class="icon-file-text"></i>
86 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_ref_name,f_path=c.readme_file)}">
86 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_ref_name,f_path=c.readme_file)}">
87 ${c.readme_file}
87 ${c.readme_file}
88 </a>
88 </a>
89 </div>
89 </div>
90 </div>
90 </div>
91 <div class="readme codeblock">
91 <div class="readme codeblock">
92 <div class="readme_box">
92 <div class="readme_box">
93 ${c.readme_data|n}
93 ${c.readme_data|n}
94 </div>
94 </div>
95 </div>
95 </div>
96 </div>
96 </div>
97 </div>
97 </div>
98 %endif
98 %endif
99
99
100 </div>
100 </div>
@@ -1,277 +1,276 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <%def name="refs_counters(branches, closed_branches, tags, bookmarks)">
3 <%def name="refs_counters(branches, closed_branches, tags, bookmarks)">
4 <span class="summary-branchtag summary-tag">
4 <span class="summary-branchtag summary-tag">
5 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
5 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
6 <i class="icon-branch"></i>
6 <i class="icon-branch"></i>
7 % if len(branches) == 1:
7 % if len(branches) == 1:
8 <span>${len(branches)}</span> ${_('Branch')}
8 <span>${len(branches)}</span> ${_('Branch')}
9 % else:
9 % else:
10 <span>${len(branches)}</span> ${_('Branches')}
10 <span>${len(branches)}</span> ${_('Branches')}
11 % endif
11 % endif
12 </a>
12 </a>
13 </span>
13 </span>
14
14
15 %if closed_branches:
15 %if closed_branches:
16 <span class="summary-branchtag summary-tag">
16 <span class="summary-branchtag summary-tag">
17 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
17 <a href="${h.route_path('branches_home',repo_name=c.repo_name)}" class="childs">
18 <i class="icon-branch"></i>
18 <i class="icon-branch"></i>
19 % if len(closed_branches) == 1:
19 % if len(closed_branches) == 1:
20 <span>${len(closed_branches)}</span> ${_('Closed Branch')}
20 <span>${len(closed_branches)}</span> ${_('Closed Branch')}
21 % else:
21 % else:
22 <span>${len(closed_branches)}</span> ${_('Closed Branches')}
22 <span>${len(closed_branches)}</span> ${_('Closed Branches')}
23 % endif
23 % endif
24 </a>
24 </a>
25 </span>
25 </span>
26 %endif
26 %endif
27
27
28 <span class="summary-tagtag summary-tag">
28 <span class="summary-tagtag summary-tag">
29 <a href="${h.route_path('tags_home',repo_name=c.repo_name)}" class="childs">
29 <a href="${h.route_path('tags_home',repo_name=c.repo_name)}" class="childs">
30 <i class="icon-tag"></i>
30 <i class="icon-tag"></i>
31 % if len(tags) == 1:
31 % if len(tags) == 1:
32 <span>${len(tags)}</span> ${_('Tag')}
32 <span>${len(tags)}</span> ${_('Tag')}
33 % else:
33 % else:
34 <span>${len(tags)}</span> ${_('Tags')}
34 <span>${len(tags)}</span> ${_('Tags')}
35 % endif
35 % endif
36 </a>
36 </a>
37 </span>
37 </span>
38
38
39 %if bookmarks:
39 %if bookmarks:
40 <span class="summary-booktag summary-tag">
40 <span class="summary-booktag summary-tag">
41 <a href="${h.route_path('bookmarks_home',repo_name=c.repo_name)}" class="childs">
41 <a href="${h.route_path('bookmarks_home',repo_name=c.repo_name)}" class="childs">
42 <i class="icon-bookmark"></i>
42 <i class="icon-bookmark"></i>
43 % if len(bookmarks) == 1:
43 % if len(bookmarks) == 1:
44 <span>${len(bookmarks)}</span> ${_('Bookmark')}
44 <span>${len(bookmarks)}</span> ${_('Bookmark')}
45 % else:
45 % else:
46 <span>${len(bookmarks)}</span> ${_('Bookmarks')}
46 <span>${len(bookmarks)}</span> ${_('Bookmarks')}
47 % endif
47 % endif
48 </a>
48 </a>
49 </span>
49 </span>
50 %endif
50 %endif
51 </%def>
51 </%def>
52
52
53 <%def name="summary_detail(breadcrumbs_links, show_downloads=True)">
53 <%def name="summary_detail(breadcrumbs_links, show_downloads=True)">
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
55
55
56 <div id="summary-menu-stats" class="summary-detail">
56 <div id="summary-menu-stats" class="summary-detail">
57 <div class="fieldset">
57 <div class="fieldset">
58 <div class="left-content">
58 <div class="left-content">
59 <div class="left-clone">
59 <div class="left-clone">
60 <select id="clone_option" name="clone_option">
60 <select id="clone_option" name="clone_option">
61 <option value="http" selected="selected">HTTP</option>
61 <option value="http" selected="selected">HTTP</option>
62 <option value="http_id">HTTP UID</option>
62 <option value="http_id">HTTP UID</option>
63 % if c.ssh_enabled:
63 % if c.ssh_enabled:
64 <option value="ssh">SSH</option>
64 <option value="ssh">SSH</option>
65 % endif
65 % endif
66 </select>
66 </select>
67 </div>
67 </div>
68
68
69 <div class="right-clone">
69 <div class="right-clone">
70 <%
70 <%
71 maybe_disabled = ''
71 maybe_disabled = ''
72 if h.is_svn_without_proxy(c.rhodecode_db_repo):
72 if h.is_svn_without_proxy(c.rhodecode_db_repo):
73 maybe_disabled = 'disabled'
73 maybe_disabled = 'disabled'
74 %>
74 %>
75
75
76 <span id="clone_option_http">
76 <span id="clone_option_http">
77 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url}"/>
77 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url}"/>
78 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
78 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url}" title="${_('Copy the clone url')}"></i>
79 </span>
79 </span>
80
80
81 <span style="display: none;" id="clone_option_http_id">
81 <span style="display: none;" id="clone_option_http_id">
82 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url_id}"/>
82 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url_id}"/>
83 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}"></i>
83 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_id}" title="${_('Copy the clone by id url')}"></i>
84 </span>
84 </span>
85
85
86 <span style="display: none;" id="clone_option_ssh">
86 <span style="display: none;" id="clone_option_ssh">
87 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url_ssh}"/>
87 <input type="text" class="input-monospace clone_url_input" ${maybe_disabled} readonly="readonly" value="${c.clone_repo_url_ssh}"/>
88 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_ssh}" title="${_('Copy the clone by ssh url')}"></i>
88 <i class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${c.clone_repo_url_ssh}" title="${_('Copy the clone by ssh url')}"></i>
89 </span>
89 </span>
90
90
91 % if maybe_disabled:
91 % if maybe_disabled:
92 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
92 <p class="help-block">${_('SVN Protocol is disabled. To enable it, see the')} <a href="${h.route_url('enterprise_svn_setup')}" target="_blank">${_('documentation here')}</a>.</p>
93 % endif
93 % endif
94 </div>
94 </div>
95 </div>
95 </div>
96
96
97 <div class="right-content">
97 <div class="right-content">
98 <div class="commit-info">
98 <div class="commit-info">
99 <div class="tags">
99 <div class="tags">
100 <% commit_rev = h.safe_int(c.rhodecode_db_repo.changeset_cache.get('revision'), 0) + 1 %>
100 <% commit_rev = h.safe_int(c.rhodecode_db_repo.changeset_cache.get('revision'), 0) + 1 %>
101 % if c.rhodecode_repo:
101 % if c.rhodecode_repo:
102 ${refs_counters(
102 ${refs_counters(
103 c.rhodecode_repo.branches,
103 c.rhodecode_repo.branches,
104 c.rhodecode_repo.branches_closed,
104 c.rhodecode_repo.branches_closed,
105 c.rhodecode_repo.tags,
105 c.rhodecode_repo.tags,
106 c.rhodecode_repo.bookmarks)}
106 c.rhodecode_repo.bookmarks)}
107 % else:
107 % else:
108 ## missing requirements can make c.rhodecode_repo None
108 ## missing requirements can make c.rhodecode_repo None
109 ${refs_counters([], [], [], [])}
109 ${refs_counters([], [], [], [])}
110 % endif
110 % endif
111
111
112 ## commits
112 ## commits
113 <span class="summary-tag">
113 <span class="summary-tag">
114 % if commit_rev == -1:
114 % if commit_rev == -1:
115 <i class="icon-history"></i>
115 <i class="icon-history"></i>
116 % if commit_rev == -1:
116 % if commit_rev == -1:
117 <span>0</span> ${_('Commit')}
117 <span>0</span> ${_('Commit')}
118 % else:
118 % else:
119 <span>0</span> ${_('Commits')}
119 <span>0</span> ${_('Commits')}
120 % endif
120 % endif
121 % else:
121 % else:
122 <a href="${h.route_path('repo_commits', repo_name=c.repo_name)}">
122 <a href="${h.route_path('repo_commits', repo_name=c.repo_name)}">
123 <i class="icon-history"></i>
123 <i class="icon-history"></i>
124 % if commit_rev == 1:
124 % if commit_rev == 1:
125 <span>${commit_rev}</span> ${_('Commit')}
125 <span>${commit_rev}</span> ${_('Commit')}
126 % else:
126 % else:
127 <span>${commit_rev}</span> ${_('Commits')}
127 <span>${commit_rev}</span> ${_('Commits')}
128 % endif
128 % endif
129 </a>
129 </a>
130 % endif
130 % endif
131 </span>
131 </span>
132
132
133 ## forks
133 ## forks
134 <span class="summary-tag">
134 <span class="summary-tag">
135 <a title="${_('Number of Repository Forks')}" href="${h.route_path('repo_forks_show_all', repo_name=c.repo_name)}">
135 <a title="${_('Number of Repository Forks')}" href="${h.route_path('repo_forks_show_all', repo_name=c.repo_name)}">
136 <i class="icon-code-fork"></i>
136 <i class="icon-code-fork"></i>
137 <span>${c.repository_forks}</span> ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>
137 <span>${c.repository_forks}</span> ${_ungettext('Fork', 'Forks', c.repository_forks)}</a>
138 </span>
138 </span>
139 </div>
139 </div>
140 </div>
140 </div>
141 </div>
141 </div>
142 </div>
142 </div>
143 ## owner, description, downloads, statistics
143 ## owner, description, downloads, statistics
144
144
145 ## Owner
145 ## Owner
146 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
146 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
147 <div class="left-label-summary">
147 <div class="left-label-summary">
148 <p>${_('Owner')}</p>
148 <p>${_('Owner')}</p>
149 <div class="right-label-summary">
149 <div class="right-label-summary">
150 ${base.gravatar_with_user(c.rhodecode_db_repo.user.email, 16, tooltip=True)}
150 ${base.gravatar_with_user(c.rhodecode_db_repo.user.email, 16, tooltip=True)}
151 </div>
151 </div>
152
152
153 </div>
153 </div>
154 </div>
154 </div>
155
155
156 ## Description
156 ## Description
157 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
157 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
158 <div class="left-label-summary">
158 <div class="left-label-summary">
159 <p>${_('Description')}</p>
159 <p>${_('Description')}</p>
160
160
161 <div class="right-label-summary input ${summary(c.show_stats)}">
161 <div class="right-label-summary input ${summary(c.show_stats)}">
162 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
162 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
163 ${dt.repo_desc(c.rhodecode_db_repo.description_safe, c.visual.stylify_metatags)}
163 ${dt.repo_desc(c.rhodecode_db_repo.description_safe, c.visual.stylify_metatags)}
164 </div>
164 </div>
165 </div>
165 </div>
166 </div>
166 </div>
167
167
168 ## Downloads
168 ## Downloads
169 % if show_downloads:
169 % if show_downloads:
170 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
170 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
171 <div class="left-label-summary">
171 <div class="left-label-summary">
172 <p>${_('Downloads')}</p>
172 <p>${_('Downloads')}</p>
173
173
174 <div class="right-label-summary input ${summary(c.show_stats)} downloads">
174 <div class="right-label-summary input ${summary(c.show_stats)} downloads">
175 % if c.rhodecode_repo and len(c.rhodecode_repo.commit_ids) == 0:
175 % if c.rhodecode_repo and len(c.rhodecode_repo.commit_ids) == 0:
176 <span class="disabled">
176 <span class="disabled">
177 ${_('There are no downloads yet')}
177 ${_('There are no downloads yet')}
178 </span>
178 </span>
179 % elif not c.enable_downloads:
179 % elif not c.enable_downloads:
180 <span class="disabled">
180 <span class="disabled">
181 ${_('Downloads are disabled for this repository')}.
181 ${_('Downloads are disabled for this repository')}.
182 </span>
182 </span>
183 % if c.is_super_admin:
183 % if c.is_super_admin:
184 ${h.link_to(_('Enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
184 ${h.link_to(_('Enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
185 % endif
185 % endif
186 % else:
186 % else:
187 <div class="enabled pull-left" style="margin-right: 10px">
187 <div class="enabled pull-left" style="margin-right: 10px">
188
188
189 <div class="btn-group btn-group-actions">
189 <div class="btn-group btn-group-actions">
190 <a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip')}">
190 <a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip', _query={'with_hash': '1'})}">
191 <i class="icon-download"></i>
191 <i class="icon-download"></i>
192 ${c.rhodecode_db_repo.landing_ref_name}.zip
192 ${c.rhodecode_db_repo.landing_ref_name}.zip
193 ## replaced by some JS on select
193 ## replaced by some JS on select
194 </a>
194 </a>
195
195
196 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
196 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
197 <i class="icon-down"></i>
197 <i class="icon-down"></i>
198 </a>
198 </a>
199
199
200 <div class="btn-action-switcher-container left-align">
200 <div class="btn-action-switcher-container left-align">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
203 % if extension not in ['.zip']:
203 % if extension not in ['.zip']:
204 <li>
204 <li>
205
205 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension, _query={'with_hash': '1'})}">
206 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension)}">
207 <i class="icon-download"></i>
206 <i class="icon-download"></i>
208 ${c.rhodecode_db_repo.landing_ref_name+extension}
207 ${c.rhodecode_db_repo.landing_ref_name+extension}
209 </a>
208 </a>
210 </li>
209 </li>
211 % endif
210 % endif
212 % endfor
211 % endfor
213 </ul>
212 </ul>
214 </div>
213 </div>
215 </div>
214 </div>
216
215
217 </div>
216 </div>
218 ${h.hidden('download_options')}
217 ${h.hidden('download_options')}
219 % endif
218 % endif
220 </div>
219 </div>
221 </div>
220 </div>
222 </div>
221 </div>
223 % endif
222 % endif
224
223
225 ## Repo size
224 ## Repo size
226 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
225 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
227 <div class="left-label-summary">
226 <div class="left-label-summary">
228 <p>${_('Repository size')}</p>
227 <p>${_('Repository size')}</p>
229
228
230 <div class="right-label-summary">
229 <div class="right-label-summary">
231 <div class="tags">
230 <div class="tags">
232 ## repo size
231 ## repo size
233 % if commit_rev == -1:
232 % if commit_rev == -1:
234 <span class="stats-bullet">0 B</span>
233 <span class="stats-bullet">0 B</span>
235 % else:
234 % else:
236 <span>
235 <span>
237 <a href="#showSize" onclick="calculateSize(); $(this).hide(); return false" id="show-repo-size">Show repository size</a>
236 <a href="#showSize" onclick="calculateSize(); $(this).hide(); return false" id="show-repo-size">Show repository size</a>
238 </span>
237 </span>
239 <span class="stats-bullet" id="repo_size_container" style="display:none">
238 <span class="stats-bullet" id="repo_size_container" style="display:none">
240 ${_('Calculating Repository Size...')}
239 ${_('Calculating Repository Size...')}
241 </span>
240 </span>
242 % endif
241 % endif
243 </div>
242 </div>
244 </div>
243 </div>
245 </div>
244 </div>
246 </div>
245 </div>
247
246
248 ## Statistics
247 ## Statistics
249 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
248 <div class="fieldset collapsable-content" data-toggle="summary-details" style="display: none;">
250 <div class="left-label-summary">
249 <div class="left-label-summary">
251 <p>${_('Code Statistics')}</p>
250 <p>${_('Code Statistics')}</p>
252
251
253 <div class="right-label-summary input ${summary(c.show_stats)} statistics">
252 <div class="right-label-summary input ${summary(c.show_stats)} statistics">
254 % if c.show_stats:
253 % if c.show_stats:
255 <div id="lang_stats" class="enabled">
254 <div id="lang_stats" class="enabled">
256 <a href="#showSize" onclick="calculateSize(); $('#show-repo-size').hide(); $(this).hide(); return false" id="show-repo-size">Show code statistics</a>
255 <a href="#showSize" onclick="calculateSize(); $('#show-repo-size').hide(); $(this).hide(); return false" id="show-repo-size">Show code statistics</a>
257 </div>
256 </div>
258 % else:
257 % else:
259 <span class="disabled">
258 <span class="disabled">
260 ${_('Statistics are disabled for this repository')}.
259 ${_('Statistics are disabled for this repository')}.
261 </span>
260 </span>
262 % if c.is_super_admin:
261 % if c.is_super_admin:
263 ${h.link_to(_('Enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_statistics'))}
262 ${h.link_to(_('Enable statistics'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_statistics'))}
264 % endif
263 % endif
265 % endif
264 % endif
266 </div>
265 </div>
267
266
268 </div>
267 </div>
269 </div>
268 </div>
270
269
271
270
272 </div><!--end summary-detail-->
271 </div><!--end summary-detail-->
273
272
274 <div id="summary_details_expand" class="btn-collapse" data-toggle="summary-details">
273 <div id="summary_details_expand" class="btn-collapse" data-toggle="summary-details">
275 ${_('Show More')}
274 ${_('Show More')}
276 </div>
275 </div>
277 </%def>
276 </%def>
General Comments 0
You need to be logged in to leave comments. Login now