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