##// END OF EJS Templates
files: don't pre load heavy attributes to compute file search
super-admin -
r5142:5611212e default
parent child Browse files
Show More
@@ -1,1041 +1,1044 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Scm model for RhodeCode
21 21 """
22 22
23 23 import os.path
24 24 import traceback
25 25 import logging
26 26 import io
27 27
28 28 from sqlalchemy import func
29 29 from zope.cachedescriptors.property import Lazy as LazyProperty
30 30
31 31 import rhodecode
32 32 from rhodecode.lib.str_utils import safe_bytes
33 33 from rhodecode.lib.vcs import get_backend
34 34 from rhodecode.lib.vcs.exceptions import RepositoryError, NodeNotChangedError
35 35 from rhodecode.lib.vcs.nodes import FileNode
36 36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 37 from rhodecode.lib import helpers as h, rc_cache
38 38 from rhodecode.lib.auth import (
39 39 HasRepoPermissionAny, HasRepoGroupPermissionAny,
40 40 HasUserGroupPermissionAny)
41 41 from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError
42 42 from rhodecode.lib import hooks_utils
43 43 from rhodecode.lib.utils import (
44 44 get_filesystem_repos, make_db_config)
45 45 from rhodecode.lib.str_utils import safe_str
46 46 from rhodecode.lib.system_info import get_system_info
47 47 from rhodecode.model import BaseModel
48 48 from rhodecode.model.db import (
49 49 or_, false,
50 50 Repository, CacheKey, UserFollowing, UserLog, User, RepoGroup,
51 51 PullRequest, FileStore)
52 52 from rhodecode.model.settings import VcsSettingsModel
53 53 from rhodecode.model.validation_schema.validators import url_validator, InvalidCloneUrl
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UserTemp(object):
59 59 def __init__(self, user_id):
60 60 self.user_id = user_id
61 61
62 62 def __repr__(self):
63 63 return "<{}('id:{}')>".format(self.__class__.__name__, self.user_id)
64 64
65 65
66 66 class RepoTemp(object):
67 67 def __init__(self, repo_id):
68 68 self.repo_id = repo_id
69 69
70 70 def __repr__(self):
71 71 return "<{}('id:{}')>".format(self.__class__.__name__, self.repo_id)
72 72
73 73
74 74 class SimpleCachedRepoList(object):
75 75 """
76 76 Lighter version of of iteration of repos without the scm initialisation,
77 77 and with cache usage
78 78 """
79 79 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
80 80 self.db_repo_list = db_repo_list
81 81 self.repos_path = repos_path
82 82 self.order_by = order_by
83 83 self.reversed = (order_by or '').startswith('-')
84 84 if not perm_set:
85 85 perm_set = ['repository.read', 'repository.write',
86 86 'repository.admin']
87 87 self.perm_set = perm_set
88 88
89 89 def __len__(self):
90 90 return len(self.db_repo_list)
91 91
92 92 def __repr__(self):
93 93 return '<{} ({})>'.format(self.__class__.__name__, self.__len__())
94 94
95 95 def __iter__(self):
96 96 for dbr in self.db_repo_list:
97 97 # check permission at this level
98 98 has_perm = HasRepoPermissionAny(*self.perm_set)(
99 99 dbr.repo_name, 'SimpleCachedRepoList check')
100 100 if not has_perm:
101 101 continue
102 102
103 103 tmp_d = {
104 104 'name': dbr.repo_name,
105 105 'dbrepo': dbr.get_dict(),
106 106 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
107 107 }
108 108 yield tmp_d
109 109
110 110
111 111 class _PermCheckIterator(object):
112 112
113 113 def __init__(
114 114 self, obj_list, obj_attr, perm_set, perm_checker,
115 115 extra_kwargs=None):
116 116 """
117 117 Creates iterator from given list of objects, additionally
118 118 checking permission for them from perm_set var
119 119
120 120 :param obj_list: list of db objects
121 121 :param obj_attr: attribute of object to pass into perm_checker
122 122 :param perm_set: list of permissions to check
123 123 :param perm_checker: callable to check permissions against
124 124 """
125 125 self.obj_list = obj_list
126 126 self.obj_attr = obj_attr
127 127 self.perm_set = perm_set
128 128 self.perm_checker = perm_checker(*self.perm_set)
129 129 self.extra_kwargs = extra_kwargs or {}
130 130
131 131 def __len__(self):
132 132 return len(self.obj_list)
133 133
134 134 def __repr__(self):
135 135 return '<{} ({})>'.format(self.__class__.__name__, self.__len__())
136 136
137 137 def __iter__(self):
138 138 for db_obj in self.obj_list:
139 139 # check permission at this level
140 140 # NOTE(marcink): the __dict__.get() is ~4x faster then getattr()
141 141 name = db_obj.__dict__.get(self.obj_attr, None)
142 142 if not self.perm_checker(name, self.__class__.__name__, **self.extra_kwargs):
143 143 continue
144 144
145 145 yield db_obj
146 146
147 147
148 148 class RepoList(_PermCheckIterator):
149 149
150 150 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
151 151 if not perm_set:
152 152 perm_set = ['repository.read', 'repository.write', 'repository.admin']
153 153
154 154 super().__init__(
155 155 obj_list=db_repo_list,
156 156 obj_attr='_repo_name', perm_set=perm_set,
157 157 perm_checker=HasRepoPermissionAny,
158 158 extra_kwargs=extra_kwargs)
159 159
160 160
161 161 class RepoGroupList(_PermCheckIterator):
162 162
163 163 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
164 164 if not perm_set:
165 165 perm_set = ['group.read', 'group.write', 'group.admin']
166 166
167 167 super().__init__(
168 168 obj_list=db_repo_group_list,
169 169 obj_attr='_group_name', perm_set=perm_set,
170 170 perm_checker=HasRepoGroupPermissionAny,
171 171 extra_kwargs=extra_kwargs)
172 172
173 173
174 174 class UserGroupList(_PermCheckIterator):
175 175
176 176 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
177 177 if not perm_set:
178 178 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
179 179
180 180 super().__init__(
181 181 obj_list=db_user_group_list,
182 182 obj_attr='users_group_name', perm_set=perm_set,
183 183 perm_checker=HasUserGroupPermissionAny,
184 184 extra_kwargs=extra_kwargs)
185 185
186 186
187 187 class ScmModel(BaseModel):
188 188 """
189 189 Generic Scm Model
190 190 """
191 191
192 192 @LazyProperty
193 193 def repos_path(self):
194 194 """
195 195 Gets the repositories root path from database
196 196 """
197 197
198 198 settings_model = VcsSettingsModel(sa=self.sa)
199 199 return settings_model.get_repos_location()
200 200
201 201 def repo_scan(self, repos_path=None):
202 202 """
203 203 Listing of repositories in given path. This path should not be a
204 204 repository itself. Return a dictionary of repository objects
205 205
206 206 :param repos_path: path to directory containing repositories
207 207 """
208 208
209 209 if repos_path is None:
210 210 repos_path = self.repos_path
211 211
212 212 log.info('scanning for repositories in %s', repos_path)
213 213
214 214 config = make_db_config()
215 215 config.set('extensions', 'largefiles', '')
216 216 repos = {}
217 217
218 218 for name, path in get_filesystem_repos(repos_path, recursive=True):
219 219 # name need to be decomposed and put back together using the /
220 220 # since this is internal storage separator for rhodecode
221 221 name = Repository.normalize_repo_name(name)
222 222
223 223 try:
224 224 if name in repos:
225 225 raise RepositoryError('Duplicate repository name %s '
226 226 'found in %s' % (name, path))
227 227 elif path[0] in rhodecode.BACKENDS:
228 228 backend = get_backend(path[0])
229 229 repos[name] = backend(path[1], config=config,
230 230 with_wire={"cache": False})
231 231 except OSError:
232 232 continue
233 233 except RepositoryError:
234 234 log.exception('Failed to create a repo')
235 235 continue
236 236
237 237 log.debug('found %s paths with repositories', len(repos))
238 238 return repos
239 239
240 240 def get_repos(self, all_repos=None, sort_key=None):
241 241 """
242 242 Get all repositories from db and for each repo create it's
243 243 backend instance and fill that backed with information from database
244 244
245 245 :param all_repos: list of repository names as strings
246 246 give specific repositories list, good for filtering
247 247
248 248 :param sort_key: initial sorting of repositories
249 249 """
250 250 if all_repos is None:
251 251 all_repos = self.sa.query(Repository)\
252 252 .filter(Repository.group_id == None)\
253 253 .order_by(func.lower(Repository.repo_name)).all()
254 254 repo_iter = SimpleCachedRepoList(
255 255 all_repos, repos_path=self.repos_path, order_by=sort_key)
256 256 return repo_iter
257 257
258 258 def get_repo_groups(self, all_groups=None):
259 259 if all_groups is None:
260 260 all_groups = RepoGroup.query()\
261 261 .filter(RepoGroup.group_parent_id == None).all()
262 262 return [x for x in RepoGroupList(all_groups)]
263 263
264 264 def mark_for_invalidation(self, repo_name, delete=False):
265 265 """
266 266 Mark caches of this repo invalid in the database. `delete` flag
267 267 removes the cache entries
268 268
269 269 :param repo_name: the repo_name for which caches should be marked
270 270 invalid, or deleted
271 271 :param delete: delete the entry keys instead of setting bool
272 272 flag on them, and also purge caches used by the dogpile
273 273 """
274 274 repo = Repository.get_by_repo_name(repo_name)
275 275
276 276 if repo:
277 277 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
278 278 repo_id=repo.repo_id)
279 279 CacheKey.set_invalidate(invalidation_namespace, delete=delete)
280 280
281 281 repo_id = repo.repo_id
282 282 config = repo._config
283 283 config.set('extensions', 'largefiles', '')
284 284 repo.update_commit_cache(config=config, cs_cache=None)
285 285 if delete:
286 286 cache_namespace_uid = f'cache_repo.{repo_id}'
287 287 rc_cache.clear_cache_namespace('cache_repo', cache_namespace_uid, method=rc_cache.CLEAR_INVALIDATE)
288 288
289 289 def toggle_following_repo(self, follow_repo_id, user_id):
290 290
291 291 f = self.sa.query(UserFollowing)\
292 292 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
293 293 .filter(UserFollowing.user_id == user_id).scalar()
294 294
295 295 if f is not None:
296 296 try:
297 297 self.sa.delete(f)
298 298 return
299 299 except Exception:
300 300 log.error(traceback.format_exc())
301 301 raise
302 302
303 303 try:
304 304 f = UserFollowing()
305 305 f.user_id = user_id
306 306 f.follows_repo_id = follow_repo_id
307 307 self.sa.add(f)
308 308 except Exception:
309 309 log.error(traceback.format_exc())
310 310 raise
311 311
312 312 def toggle_following_user(self, follow_user_id, user_id):
313 313 f = self.sa.query(UserFollowing)\
314 314 .filter(UserFollowing.follows_user_id == follow_user_id)\
315 315 .filter(UserFollowing.user_id == user_id).scalar()
316 316
317 317 if f is not None:
318 318 try:
319 319 self.sa.delete(f)
320 320 return
321 321 except Exception:
322 322 log.error(traceback.format_exc())
323 323 raise
324 324
325 325 try:
326 326 f = UserFollowing()
327 327 f.user_id = user_id
328 328 f.follows_user_id = follow_user_id
329 329 self.sa.add(f)
330 330 except Exception:
331 331 log.error(traceback.format_exc())
332 332 raise
333 333
334 334 def is_following_repo(self, repo_name, user_id, cache=False):
335 335 r = self.sa.query(Repository)\
336 336 .filter(Repository.repo_name == repo_name).scalar()
337 337
338 338 f = self.sa.query(UserFollowing)\
339 339 .filter(UserFollowing.follows_repository == r)\
340 340 .filter(UserFollowing.user_id == user_id).scalar()
341 341
342 342 return f is not None
343 343
344 344 def is_following_user(self, username, user_id, cache=False):
345 345 u = User.get_by_username(username)
346 346
347 347 f = self.sa.query(UserFollowing)\
348 348 .filter(UserFollowing.follows_user == u)\
349 349 .filter(UserFollowing.user_id == user_id).scalar()
350 350
351 351 return f is not None
352 352
353 353 def get_followers(self, repo):
354 354 repo = self._get_repo(repo)
355 355
356 356 return self.sa.query(UserFollowing)\
357 357 .filter(UserFollowing.follows_repository == repo).count()
358 358
359 359 def get_forks(self, repo):
360 360 repo = self._get_repo(repo)
361 361 return self.sa.query(Repository)\
362 362 .filter(Repository.fork == repo).count()
363 363
364 364 def get_pull_requests(self, repo):
365 365 repo = self._get_repo(repo)
366 366 return self.sa.query(PullRequest)\
367 367 .filter(PullRequest.target_repo == repo)\
368 368 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
369 369
370 370 def get_artifacts(self, repo):
371 371 repo = self._get_repo(repo)
372 372 return self.sa.query(FileStore)\
373 373 .filter(FileStore.repo == repo)\
374 374 .filter(or_(FileStore.hidden == None, FileStore.hidden == false())).count()
375 375
376 376 def mark_as_fork(self, repo, fork, user):
377 377 repo = self._get_repo(repo)
378 378 fork = self._get_repo(fork)
379 379 if fork and repo.repo_id == fork.repo_id:
380 380 raise Exception("Cannot set repository as fork of itself")
381 381
382 382 if fork and repo.repo_type != fork.repo_type:
383 383 raise RepositoryError(
384 384 "Cannot set repository as fork of repository with other type")
385 385
386 386 repo.fork = fork
387 387 self.sa.add(repo)
388 388 return repo
389 389
390 390 def pull_changes(self, repo, username, remote_uri=None, validate_uri=True):
391 391 dbrepo = self._get_repo(repo)
392 392 remote_uri = remote_uri or dbrepo.clone_uri
393 393 if not remote_uri:
394 394 raise Exception("This repository doesn't have a clone uri")
395 395
396 396 repo = dbrepo.scm_instance(cache=False)
397 397 repo.config.clear_section('hooks')
398 398
399 399 try:
400 400 # NOTE(marcink): add extra validation so we skip invalid urls
401 401 # this is due this tasks can be executed via scheduler without
402 402 # proper validation of remote_uri
403 403 if validate_uri:
404 404 config = make_db_config(clear_session=False)
405 405 url_validator(remote_uri, dbrepo.repo_type, config)
406 406 except InvalidCloneUrl:
407 407 raise
408 408
409 409 repo_name = dbrepo.repo_name
410 410 try:
411 411 # TODO: we need to make sure those operations call proper hooks !
412 412 repo.fetch(remote_uri)
413 413
414 414 self.mark_for_invalidation(repo_name)
415 415 except Exception:
416 416 log.error(traceback.format_exc())
417 417 raise
418 418
419 419 def push_changes(self, repo, username, remote_uri=None, validate_uri=True):
420 420 dbrepo = self._get_repo(repo)
421 421 remote_uri = remote_uri or dbrepo.push_uri
422 422 if not remote_uri:
423 423 raise Exception("This repository doesn't have a clone uri")
424 424
425 425 repo = dbrepo.scm_instance(cache=False)
426 426 repo.config.clear_section('hooks')
427 427
428 428 try:
429 429 # NOTE(marcink): add extra validation so we skip invalid urls
430 430 # this is due this tasks can be executed via scheduler without
431 431 # proper validation of remote_uri
432 432 if validate_uri:
433 433 config = make_db_config(clear_session=False)
434 434 url_validator(remote_uri, dbrepo.repo_type, config)
435 435 except InvalidCloneUrl:
436 436 raise
437 437
438 438 try:
439 439 repo.push(remote_uri)
440 440 except Exception:
441 441 log.error(traceback.format_exc())
442 442 raise
443 443
444 444 def commit_change(self, repo, repo_name, commit, user, author, message,
445 445 content: bytes, f_path: bytes):
446 446 """
447 447 Commits changes
448 448 """
449 449 user = self._get_user(user)
450 450
451 451 # message and author needs to be unicode
452 452 # proper backend should then translate that into required type
453 453 message = safe_str(message)
454 454 author = safe_str(author)
455 455 imc = repo.in_memory_commit
456 456 imc.change(FileNode(f_path, content, mode=commit.get_file_mode(f_path)))
457 457 try:
458 458 # TODO: handle pre-push action !
459 459 tip = imc.commit(
460 460 message=message, author=author, parents=[commit],
461 461 branch=commit.branch)
462 462 except Exception as e:
463 463 log.error(traceback.format_exc())
464 464 raise IMCCommitError(str(e))
465 465 finally:
466 466 # always clear caches, if commit fails we want fresh object also
467 467 self.mark_for_invalidation(repo_name)
468 468
469 469 # We trigger the post-push action
470 470 hooks_utils.trigger_post_push_hook(
471 471 username=user.username, action='push_local', hook_type='post_push',
472 472 repo_name=repo_name, repo_type=repo.alias, commit_ids=[tip.raw_id])
473 473 return tip
474 474
475 475 def _sanitize_path(self, f_path: bytes):
476 476 if f_path.startswith(b'/') or f_path.startswith(b'./') or b'../' in f_path:
477 477 raise NonRelativePathError(b'%b is not an relative path' % f_path)
478 478 if f_path:
479 479 f_path = os.path.normpath(f_path)
480 480 return f_path
481 481
482 482 def get_dirnode_metadata(self, request, commit, dir_node):
483 483 if not dir_node.is_dir():
484 484 return []
485 485
486 486 data = []
487 487 for node in dir_node:
488 488 if not node.is_file():
489 489 # we skip file-nodes
490 490 continue
491 491
492 492 last_commit = node.last_commit
493 493 last_commit_date = last_commit.date
494 494 data.append({
495 495 'name': node.name,
496 496 'size': h.format_byte_size_binary(node.size),
497 497 'modified_at': h.format_date(last_commit_date),
498 498 'modified_ts': last_commit_date.isoformat(),
499 499 'revision': last_commit.revision,
500 500 'short_id': last_commit.short_id,
501 501 'message': h.escape(last_commit.message),
502 502 'author': h.escape(last_commit.author),
503 503 'user_profile': h.gravatar_with_user(
504 504 request, last_commit.author),
505 505 })
506 506
507 507 return data
508 508
509 509 def get_nodes(self, repo_name, commit_id, root_path='/', flat=True,
510 510 extended_info=False, content=False, max_file_bytes=None):
511 511 """
512 512 recursive walk in root dir and return a set of all path in that dir
513 513 based on repository walk function
514 514
515 515 :param repo_name: name of repository
516 516 :param commit_id: commit id for which to list nodes
517 517 :param root_path: root path to list
518 518 :param flat: return as a list, if False returns a dict with description
519 519 :param extended_info: show additional info such as md5, binary, size etc
520 520 :param content: add nodes content to the return data
521 521 :param max_file_bytes: will not return file contents over this limit
522 522
523 523 """
524 524 _files = list()
525 525 _dirs = list()
526 526
527 527 try:
528 528 _repo = self._get_repo(repo_name)
529 529 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
530 530 root_path = root_path.lstrip('/')
531 531
532 532 # get RootNode, inject pre-load options before walking
533 533 top_node = commit.get_node(root_path)
534 534 extended_info_pre_load = []
535 535 if extended_info:
536 536 extended_info_pre_load += ['md5']
537 537 top_node.default_pre_load = ['is_binary', 'size'] + extended_info_pre_load
538 538
539 539 for __, dirs, files in commit.walk(top_node):
540 540
541 541 for f in files:
542 542 _content = None
543 543 _data = f_name = f.str_path
544 544
545 545 if not flat:
546 546 _data = {
547 547 "name": h.escape(f_name),
548 548 "type": "file",
549 549 }
550 550 if extended_info:
551 551 _data.update({
552 552 "md5": f.md5,
553 553 "binary": f.is_binary,
554 554 "size": f.size,
555 555 "extension": f.extension,
556 556 "mimetype": f.mimetype,
557 557 "lines": f.lines()[0]
558 558 })
559 559
560 560 if content:
561 561 over_size_limit = (max_file_bytes is not None
562 562 and f.size > max_file_bytes)
563 563 full_content = None
564 564 if not f.is_binary and not over_size_limit:
565 565 full_content = f.str_content
566 566
567 567 _data.update({
568 568 "content": full_content,
569 569 })
570 570 _files.append(_data)
571 571
572 572 for d in dirs:
573 573 _data = d_name = d.str_path
574 574 if not flat:
575 575 _data = {
576 576 "name": h.escape(d_name),
577 577 "type": "dir",
578 578 }
579 579 if extended_info:
580 580 _data.update({
581 581 "md5": "",
582 582 "binary": False,
583 583 "size": 0,
584 584 "extension": "",
585 585 })
586 586 if content:
587 587 _data.update({
588 588 "content": None
589 589 })
590 590 _dirs.append(_data)
591 591 except RepositoryError:
592 592 log.exception("Exception in get_nodes")
593 593 raise
594 594
595 595 return _dirs, _files
596 596
597 597 def get_quick_filter_nodes(self, repo_name, commit_id, root_path='/'):
598 598 """
599 599 Generate files for quick filter in files view
600 600 """
601 601
602 602 _files = list()
603 603 _dirs = list()
604 604 try:
605 605 _repo = self._get_repo(repo_name)
606 606 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
607 607 root_path = root_path.lstrip('/')
608 for __, dirs, files in commit.walk(root_path):
609 608
609 top_node = commit.get_node(root_path)
610 top_node.default_pre_load = []
611
612 for __, dirs, files in commit.walk(top_node):
610 613 for f in files:
611 614
612 615 _data = {
613 616 "name": h.escape(f.str_path),
614 617 "type": "file",
615 618 }
616 619
617 620 _files.append(_data)
618 621
619 622 for d in dirs:
620 623
621 624 _data = {
622 625 "name": h.escape(d.str_path),
623 626 "type": "dir",
624 627 }
625 628
626 629 _dirs.append(_data)
627 630 except RepositoryError:
628 631 log.exception("Exception in get_quick_filter_nodes")
629 632 raise
630 633
631 634 return _dirs, _files
632 635
633 636 def get_node(self, repo_name, commit_id, file_path,
634 637 extended_info=False, content=False, max_file_bytes=None, cache=True):
635 638 """
636 639 retrieve single node from commit
637 640 """
638 641
639 642 try:
640 643
641 644 _repo = self._get_repo(repo_name)
642 645 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
643 646
644 647 file_node = commit.get_node(file_path)
645 648 if file_node.is_dir():
646 649 raise RepositoryError('The given path is a directory')
647 650
648 651 _content = None
649 652 f_name = file_node.str_path
650 653
651 654 file_data = {
652 655 "name": h.escape(f_name),
653 656 "type": "file",
654 657 }
655 658
656 659 if extended_info:
657 660 file_data.update({
658 661 "extension": file_node.extension,
659 662 "mimetype": file_node.mimetype,
660 663 })
661 664
662 665 if cache:
663 666 md5 = file_node.md5
664 667 is_binary = file_node.is_binary
665 668 size = file_node.size
666 669 else:
667 670 is_binary, md5, size, _content = file_node.metadata_uncached()
668 671
669 672 file_data.update({
670 673 "md5": md5,
671 674 "binary": is_binary,
672 675 "size": size,
673 676 })
674 677
675 678 if content and cache:
676 679 # get content + cache
677 680 size = file_node.size
678 681 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
679 682 full_content = None
680 683 all_lines = 0
681 684 if not file_node.is_binary and not over_size_limit:
682 685 full_content = safe_str(file_node.content)
683 686 all_lines, empty_lines = file_node.count_lines(full_content)
684 687
685 688 file_data.update({
686 689 "content": full_content,
687 690 "lines": all_lines
688 691 })
689 692 elif content:
690 693 # get content *without* cache
691 694 if _content is None:
692 695 is_binary, md5, size, _content = file_node.metadata_uncached()
693 696
694 697 over_size_limit = (max_file_bytes is not None and size > max_file_bytes)
695 698 full_content = None
696 699 all_lines = 0
697 700 if not is_binary and not over_size_limit:
698 701 full_content = safe_str(_content)
699 702 all_lines, empty_lines = file_node.count_lines(full_content)
700 703
701 704 file_data.update({
702 705 "content": full_content,
703 706 "lines": all_lines
704 707 })
705 708
706 709 except RepositoryError:
707 710 log.exception("Exception in get_node")
708 711 raise
709 712
710 713 return file_data
711 714
712 715 def get_fts_data(self, repo_name, commit_id, root_path='/'):
713 716 """
714 717 Fetch node tree for usage in full text search
715 718 """
716 719
717 720 tree_info = list()
718 721
719 722 try:
720 723 _repo = self._get_repo(repo_name)
721 724 commit = _repo.scm_instance().get_commit(commit_id=commit_id)
722 725 root_path = root_path.lstrip('/')
723 726 top_node = commit.get_node(root_path)
724 727 top_node.default_pre_load = []
725 728
726 729 for __, dirs, files in commit.walk(top_node):
727 730
728 731 for f in files:
729 732 is_binary, md5, size, _content = f.metadata_uncached()
730 733 _data = {
731 734 "name": f.str_path,
732 735 "md5": md5,
733 736 "extension": f.extension,
734 737 "binary": is_binary,
735 738 "size": size
736 739 }
737 740
738 741 tree_info.append(_data)
739 742
740 743 except RepositoryError:
741 744 log.exception("Exception in get_nodes")
742 745 raise
743 746
744 747 return tree_info
745 748
746 749 def create_nodes(self, user, repo, message, nodes, parent_commit=None,
747 750 author=None, trigger_push_hook=True):
748 751 """
749 752 Commits given multiple nodes into repo
750 753
751 754 :param user: RhodeCode User object or user_id, the commiter
752 755 :param repo: RhodeCode Repository object
753 756 :param message: commit message
754 757 :param nodes: mapping {filename:{'content':content},...}
755 758 :param parent_commit: parent commit, can be empty than it's
756 759 initial commit
757 760 :param author: author of commit, cna be different that commiter
758 761 only for git
759 762 :param trigger_push_hook: trigger push hooks
760 763
761 764 :returns: new committed commit
762 765 """
763 766
764 767 user = self._get_user(user)
765 768 scm_instance = repo.scm_instance(cache=False)
766 769
767 770 message = safe_str(message)
768 771 commiter = user.full_contact
769 772 author = safe_str(author) if author else commiter
770 773
771 774 imc = scm_instance.in_memory_commit
772 775
773 776 if not parent_commit:
774 777 parent_commit = EmptyCommit(alias=scm_instance.alias)
775 778
776 779 if isinstance(parent_commit, EmptyCommit):
777 780 # EmptyCommit means we're editing empty repository
778 781 parents = None
779 782 else:
780 783 parents = [parent_commit]
781 784
782 785 upload_file_types = (io.BytesIO, io.BufferedRandom)
783 786 processed_nodes = []
784 787 for filename, content_dict in nodes.items():
785 788 if not isinstance(filename, bytes):
786 789 raise ValueError(f'filename key in nodes needs to be bytes , or {upload_file_types}')
787 790 content = content_dict['content']
788 791 if not isinstance(content, upload_file_types + (bytes,)):
789 792 raise ValueError('content key value in nodes needs to be bytes')
790 793
791 794 for f_path in nodes:
792 795 f_path = self._sanitize_path(f_path)
793 796 content = nodes[f_path]['content']
794 797
795 798 # decoding here will force that we have proper encoded values
796 799 # in any other case this will throw exceptions and deny commit
797 800
798 801 if isinstance(content, bytes):
799 802 pass
800 803 elif isinstance(content, upload_file_types):
801 804 content = content.read()
802 805 else:
803 806 raise Exception(f'Content is of unrecognized type {type(content)}, expected {upload_file_types}')
804 807 processed_nodes.append((f_path, content))
805 808
806 809 # add multiple nodes
807 810 for path, content in processed_nodes:
808 811 imc.add(FileNode(path, content=content))
809 812
810 813 # TODO: handle pre push scenario
811 814 tip = imc.commit(message=message,
812 815 author=author,
813 816 parents=parents,
814 817 branch=parent_commit.branch)
815 818
816 819 self.mark_for_invalidation(repo.repo_name)
817 820 if trigger_push_hook:
818 821 hooks_utils.trigger_post_push_hook(
819 822 username=user.username, action='push_local',
820 823 repo_name=repo.repo_name, repo_type=scm_instance.alias,
821 824 hook_type='post_push',
822 825 commit_ids=[tip.raw_id])
823 826 return tip
824 827
825 828 def update_nodes(self, user, repo, message, nodes, parent_commit=None,
826 829 author=None, trigger_push_hook=True):
827 830 user = self._get_user(user)
828 831 scm_instance = repo.scm_instance(cache=False)
829 832
830 833 message = safe_str(message)
831 834 commiter = user.full_contact
832 835 author = safe_str(author) if author else commiter
833 836
834 837 imc = scm_instance.in_memory_commit
835 838
836 839 if not parent_commit:
837 840 parent_commit = EmptyCommit(alias=scm_instance.alias)
838 841
839 842 if isinstance(parent_commit, EmptyCommit):
840 843 # EmptyCommit means we we're editing empty repository
841 844 parents = None
842 845 else:
843 846 parents = [parent_commit]
844 847
845 848 # add multiple nodes
846 849 for _filename, data in nodes.items():
847 850 # new filename, can be renamed from the old one, also sanitaze
848 851 # the path for any hack around relative paths like ../../ etc.
849 852 filename = self._sanitize_path(data['filename'])
850 853 old_filename = self._sanitize_path(_filename)
851 854 content = data['content']
852 855 file_mode = data.get('mode')
853 856 filenode = FileNode(old_filename, content=content, mode=file_mode)
854 857 op = data['op']
855 858 if op == 'add':
856 859 imc.add(filenode)
857 860 elif op == 'del':
858 861 imc.remove(filenode)
859 862 elif op == 'mod':
860 863 if filename != old_filename:
861 864 # TODO: handle renames more efficient, needs vcs lib changes
862 865 imc.remove(filenode)
863 866 imc.add(FileNode(filename, content=content, mode=file_mode))
864 867 else:
865 868 imc.change(filenode)
866 869
867 870 try:
868 871 # TODO: handle pre push scenario commit changes
869 872 tip = imc.commit(message=message,
870 873 author=author,
871 874 parents=parents,
872 875 branch=parent_commit.branch)
873 876 except NodeNotChangedError:
874 877 raise
875 878 except Exception as e:
876 879 log.exception("Unexpected exception during call to imc.commit")
877 880 raise IMCCommitError(str(e))
878 881 finally:
879 882 # always clear caches, if commit fails we want fresh object also
880 883 self.mark_for_invalidation(repo.repo_name)
881 884
882 885 if trigger_push_hook:
883 886 hooks_utils.trigger_post_push_hook(
884 887 username=user.username, action='push_local', hook_type='post_push',
885 888 repo_name=repo.repo_name, repo_type=scm_instance.alias,
886 889 commit_ids=[tip.raw_id])
887 890
888 891 return tip
889 892
890 893 def delete_nodes(self, user, repo, message, nodes, parent_commit=None,
891 894 author=None, trigger_push_hook=True):
892 895 """
893 896 Deletes given multiple nodes into `repo`
894 897
895 898 :param user: RhodeCode User object or user_id, the committer
896 899 :param repo: RhodeCode Repository object
897 900 :param message: commit message
898 901 :param nodes: mapping {filename:{'content':content},...}
899 902 :param parent_commit: parent commit, can be empty than it's initial
900 903 commit
901 904 :param author: author of commit, cna be different that commiter only
902 905 for git
903 906 :param trigger_push_hook: trigger push hooks
904 907
905 908 :returns: new commit after deletion
906 909 """
907 910
908 911 user = self._get_user(user)
909 912 scm_instance = repo.scm_instance(cache=False)
910 913
911 914 processed_nodes = []
912 915 for f_path in nodes:
913 916 f_path = self._sanitize_path(f_path)
914 917 # content can be empty but for compatibility it allows same dicts
915 918 # structure as add_nodes
916 919 content = nodes[f_path].get('content')
917 920 processed_nodes.append((safe_bytes(f_path), content))
918 921
919 922 message = safe_str(message)
920 923 commiter = user.full_contact
921 924 author = safe_str(author) if author else commiter
922 925
923 926 imc = scm_instance.in_memory_commit
924 927
925 928 if not parent_commit:
926 929 parent_commit = EmptyCommit(alias=scm_instance.alias)
927 930
928 931 if isinstance(parent_commit, EmptyCommit):
929 932 # EmptyCommit means we we're editing empty repository
930 933 parents = None
931 934 else:
932 935 parents = [parent_commit]
933 936 # add multiple nodes
934 937 for path, content in processed_nodes:
935 938 imc.remove(FileNode(path, content=content))
936 939
937 940 # TODO: handle pre push scenario
938 941 tip = imc.commit(message=message,
939 942 author=author,
940 943 parents=parents,
941 944 branch=parent_commit.branch)
942 945
943 946 self.mark_for_invalidation(repo.repo_name)
944 947 if trigger_push_hook:
945 948 hooks_utils.trigger_post_push_hook(
946 949 username=user.username, action='push_local', hook_type='post_push',
947 950 repo_name=repo.repo_name, repo_type=scm_instance.alias,
948 951 commit_ids=[tip.raw_id])
949 952 return tip
950 953
951 954 def strip(self, repo, commit_id, branch):
952 955 scm_instance = repo.scm_instance(cache=False)
953 956 scm_instance.config.clear_section('hooks')
954 957 scm_instance.strip(commit_id, branch)
955 958 self.mark_for_invalidation(repo.repo_name)
956 959
957 960 def get_unread_journal(self):
958 961 return self.sa.query(UserLog).count()
959 962
960 963 @classmethod
961 964 def backend_landing_ref(cls, repo_type):
962 965 """
963 966 Return a default landing ref based on a repository type.
964 967 """
965 968
966 969 landing_ref = {
967 970 'hg': ('branch:default', 'default'),
968 971 'git': ('branch:master', 'master'),
969 972 'svn': ('rev:tip', 'latest tip'),
970 973 'default': ('rev:tip', 'latest tip'),
971 974 }
972 975
973 976 return landing_ref.get(repo_type) or landing_ref['default']
974 977
975 978 def get_repo_landing_revs(self, translator, repo=None):
976 979 """
977 980 Generates select option with tags branches and bookmarks (for hg only)
978 981 grouped by type
979 982
980 983 :param repo:
981 984 """
982 985 from rhodecode.lib.vcs.backends.git import GitRepository
983 986
984 987 _ = translator
985 988 repo = self._get_repo(repo)
986 989
987 990 if repo:
988 991 repo_type = repo.repo_type
989 992 else:
990 993 repo_type = 'default'
991 994
992 995 default_landing_ref, landing_ref_lbl = self.backend_landing_ref(repo_type)
993 996
994 997 default_ref_options = [
995 998 [default_landing_ref, landing_ref_lbl]
996 999 ]
997 1000 default_choices = [
998 1001 default_landing_ref
999 1002 ]
1000 1003
1001 1004 if not repo:
1002 1005 # presented at NEW repo creation
1003 1006 return default_choices, default_ref_options
1004 1007
1005 1008 repo = repo.scm_instance()
1006 1009
1007 1010 ref_options = [(default_landing_ref, landing_ref_lbl)]
1008 1011 choices = [default_landing_ref]
1009 1012
1010 1013 # branches
1011 1014 branch_group = [(f'branch:{safe_str(b)}', safe_str(b)) for b in repo.branches]
1012 1015 if not branch_group:
1013 1016 # new repo, or without maybe a branch?
1014 1017 branch_group = default_ref_options
1015 1018
1016 1019 branches_group = (branch_group, _("Branches"))
1017 1020 ref_options.append(branches_group)
1018 1021 choices.extend([x[0] for x in branches_group[0]])
1019 1022
1020 1023 # bookmarks for HG
1021 1024 if repo.alias == 'hg':
1022 1025 bookmarks_group = (
1023 1026 [(f'book:{safe_str(b)}', safe_str(b))
1024 1027 for b in repo.bookmarks],
1025 1028 _("Bookmarks"))
1026 1029 ref_options.append(bookmarks_group)
1027 1030 choices.extend([x[0] for x in bookmarks_group[0]])
1028 1031
1029 1032 # tags
1030 1033 tags_group = (
1031 1034 [(f'tag:{safe_str(t)}', safe_str(t))
1032 1035 for t in repo.tags],
1033 1036 _("Tags"))
1034 1037 ref_options.append(tags_group)
1035 1038 choices.extend([x[0] for x in tags_group[0]])
1036 1039
1037 1040 return choices, ref_options
1038 1041
1039 1042 def get_server_info(self, environ=None):
1040 1043 server_info = get_system_info(environ)
1041 1044 return server_info
General Comments 0
You need to be logged in to leave comments. Login now