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