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