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