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