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