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