##// END OF EJS Templates
always update changeset cache after invalidation signal
marcink -
r3150:80abc924 beta
parent child Browse files
Show More
@@ -1,618 +1,621 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import re
28 28 import time
29 29 import traceback
30 30 import logging
31 31 import cStringIO
32 32 import pkg_resources
33 33 from os.path import dirname as dn, join as jn
34 34
35 35 from sqlalchemy import func
36 36 from pylons.i18n.translation import _
37 37
38 38 import rhodecode
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.exceptions import RepositoryError
41 41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 42 from rhodecode.lib.vcs.nodes import FileNode
43 43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 44
45 45 from rhodecode import BACKENDS
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.utils2 import safe_str, safe_unicode
48 48 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
49 49 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
50 50 action_logger, REMOVED_REPO_PAT
51 51 from rhodecode.model import BaseModel
52 52 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
53 53 UserFollowing, UserLog, User, RepoGroup, PullRequest
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 CachedRepoList(object):
75 75 """
76 76 Cached repo list, uses in-memory cache after initialization, that is
77 77 super fast
78 78 """
79 79
80 80 def __init__(self, db_repo_list, repos_path, order_by=None):
81 81 self.db_repo_list = db_repo_list
82 82 self.repos_path = repos_path
83 83 self.order_by = order_by
84 84 self.reversed = (order_by or '').startswith('-')
85 85
86 86 def __len__(self):
87 87 return len(self.db_repo_list)
88 88
89 89 def __repr__(self):
90 90 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
91 91
92 92 def __iter__(self):
93 93 # pre-propagated cache_map to save executing select statements
94 94 # for each repo
95 95 cache_map = CacheInvalidation.get_cache_map()
96 96
97 97 for dbr in self.db_repo_list:
98 98 scmr = dbr.scm_instance_cached(cache_map)
99 99 # check permission at this level
100 100 if not HasRepoPermissionAny(
101 101 'repository.read', 'repository.write', 'repository.admin'
102 102 )(dbr.repo_name, 'get repo check'):
103 103 continue
104 104
105 105 if scmr is None:
106 106 log.error(
107 107 '%s this repository is present in database but it '
108 108 'cannot be created as an scm instance' % dbr.repo_name
109 109 )
110 110 continue
111 111
112 112 last_change = scmr.last_change
113 113 tip = h.get_changeset_safe(scmr, 'tip')
114 114
115 115 tmp_d = {}
116 116 tmp_d['name'] = dbr.repo_name
117 117 tmp_d['name_sort'] = tmp_d['name'].lower()
118 118 tmp_d['raw_name'] = tmp_d['name'].lower()
119 119 tmp_d['description'] = dbr.description
120 120 tmp_d['description_sort'] = tmp_d['description'].lower()
121 121 tmp_d['last_change'] = last_change
122 122 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
123 123 tmp_d['tip'] = tip.raw_id
124 124 tmp_d['tip_sort'] = tip.revision
125 125 tmp_d['rev'] = tip.revision
126 126 tmp_d['contact'] = dbr.user.full_contact
127 127 tmp_d['contact_sort'] = tmp_d['contact']
128 128 tmp_d['owner_sort'] = tmp_d['contact']
129 129 tmp_d['repo_archives'] = list(scmr._get_archives())
130 130 tmp_d['last_msg'] = tip.message
131 131 tmp_d['author'] = tip.author
132 132 tmp_d['dbrepo'] = dbr.get_dict()
133 133 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
134 134 yield tmp_d
135 135
136 136
137 137 class SimpleCachedRepoList(CachedRepoList):
138 138 """
139 139 Lighter version of CachedRepoList without the scm initialisation
140 140 """
141 141
142 142 def __iter__(self):
143 143 for dbr in self.db_repo_list:
144 144 # check permission at this level
145 145 if not HasRepoPermissionAny(
146 146 'repository.read', 'repository.write', 'repository.admin'
147 147 )(dbr.repo_name, 'get repo check'):
148 148 continue
149 149
150 150 tmp_d = {}
151 151 tmp_d['name'] = dbr.repo_name
152 152 tmp_d['name_sort'] = tmp_d['name'].lower()
153 153 tmp_d['raw_name'] = tmp_d['name'].lower()
154 154 tmp_d['description'] = dbr.description
155 155 tmp_d['description_sort'] = tmp_d['description'].lower()
156 156 tmp_d['dbrepo'] = dbr.get_dict()
157 157 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
158 158 yield tmp_d
159 159
160 160
161 161 class GroupList(object):
162 162
163 163 def __init__(self, db_repo_group_list):
164 164 self.db_repo_group_list = db_repo_group_list
165 165
166 166 def __len__(self):
167 167 return len(self.db_repo_group_list)
168 168
169 169 def __repr__(self):
170 170 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
171 171
172 172 def __iter__(self):
173 173 for dbgr in self.db_repo_group_list:
174 174 # check permission at this level
175 175 if not HasReposGroupPermissionAny(
176 176 'group.read', 'group.write', 'group.admin'
177 177 )(dbgr.group_name, 'get group repo check'):
178 178 continue
179 179
180 180 yield dbgr
181 181
182 182
183 183 class ScmModel(BaseModel):
184 184 """
185 185 Generic Scm Model
186 186 """
187 187
188 188 def __get_repo(self, instance):
189 189 cls = Repository
190 190 if isinstance(instance, cls):
191 191 return instance
192 192 elif isinstance(instance, int) or safe_str(instance).isdigit():
193 193 return cls.get(instance)
194 194 elif isinstance(instance, basestring):
195 195 return cls.get_by_repo_name(instance)
196 196 elif instance:
197 197 raise Exception('given object must be int, basestr or Instance'
198 198 ' of %s got %s' % (type(cls), type(instance)))
199 199
200 200 @LazyProperty
201 201 def repos_path(self):
202 202 """
203 203 Get's the repositories root path from database
204 204 """
205 205
206 206 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
207 207
208 208 return q.ui_value
209 209
210 210 def repo_scan(self, repos_path=None):
211 211 """
212 212 Listing of repositories in given path. This path should not be a
213 213 repository itself. Return a dictionary of repository objects
214 214
215 215 :param repos_path: path to directory containing repositories
216 216 """
217 217
218 218 if repos_path is None:
219 219 repos_path = self.repos_path
220 220
221 221 log.info('scanning for repositories in %s' % repos_path)
222 222
223 223 baseui = make_ui('db')
224 224 repos = {}
225 225
226 226 for name, path in get_filesystem_repos(repos_path, recursive=True):
227 227 # skip removed repos
228 228 if REMOVED_REPO_PAT.match(name) or path[0] is None:
229 229 continue
230 230
231 231 # name need to be decomposed and put back together using the /
232 232 # since this is internal storage separator for rhodecode
233 233 name = Repository.url_sep().join(name.split(os.sep))
234 234
235 235 try:
236 236 if name in repos:
237 237 raise RepositoryError('Duplicate repository name %s '
238 238 'found in %s' % (name, path))
239 239 else:
240 240
241 241 klass = get_backend(path[0])
242 242
243 243 if path[0] == 'hg' and path[0] in BACKENDS.keys():
244 244 repos[name] = klass(safe_str(path[1]), baseui=baseui)
245 245
246 246 if path[0] == 'git' and path[0] in BACKENDS.keys():
247 247 repos[name] = klass(path[1])
248 248 except OSError:
249 249 continue
250 250
251 251 return repos
252 252
253 253 def get_repos(self, all_repos=None, sort_key=None, simple=False):
254 254 """
255 255 Get all repos from db and for each repo create it's
256 256 backend instance and fill that backed with information from database
257 257
258 258 :param all_repos: list of repository names as strings
259 259 give specific repositories list, good for filtering
260 260
261 261 :param sort_key: initial sorting of repos
262 262 :param simple: use SimpleCachedList - one without the SCM info
263 263 """
264 264 if all_repos is None:
265 265 all_repos = self.sa.query(Repository)\
266 266 .filter(Repository.group_id == None)\
267 267 .order_by(func.lower(Repository.repo_name)).all()
268 268 if simple:
269 269 repo_iter = SimpleCachedRepoList(all_repos,
270 270 repos_path=self.repos_path,
271 271 order_by=sort_key)
272 272 else:
273 273 repo_iter = CachedRepoList(all_repos,
274 274 repos_path=self.repos_path,
275 275 order_by=sort_key)
276 276
277 277 return repo_iter
278 278
279 279 def get_repos_groups(self, all_groups=None):
280 280 if all_groups is None:
281 281 all_groups = RepoGroup.query()\
282 282 .filter(RepoGroup.group_parent_id == None).all()
283 283 group_iter = GroupList(all_groups)
284 284
285 285 return group_iter
286 286
287 287 def mark_for_invalidation(self, repo_name):
288 288 """
289 289 Puts cache invalidation task into db for
290 290 further global cache invalidation
291 291
292 292 :param repo_name: this repo that should invalidation take place
293 293 """
294 294 CacheInvalidation.set_invalidate(repo_name=repo_name)
295 repo = Repository.get_by_repo_name(repo_name)
296 if repo:
297 repo.update_changeset_cache()
295 298
296 299 def toggle_following_repo(self, follow_repo_id, user_id):
297 300
298 301 f = self.sa.query(UserFollowing)\
299 302 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
300 303 .filter(UserFollowing.user_id == user_id).scalar()
301 304
302 305 if f is not None:
303 306 try:
304 307 self.sa.delete(f)
305 308 action_logger(UserTemp(user_id),
306 309 'stopped_following_repo',
307 310 RepoTemp(follow_repo_id))
308 311 return
309 312 except:
310 313 log.error(traceback.format_exc())
311 314 raise
312 315
313 316 try:
314 317 f = UserFollowing()
315 318 f.user_id = user_id
316 319 f.follows_repo_id = follow_repo_id
317 320 self.sa.add(f)
318 321
319 322 action_logger(UserTemp(user_id),
320 323 'started_following_repo',
321 324 RepoTemp(follow_repo_id))
322 325 except:
323 326 log.error(traceback.format_exc())
324 327 raise
325 328
326 329 def toggle_following_user(self, follow_user_id, user_id):
327 330 f = self.sa.query(UserFollowing)\
328 331 .filter(UserFollowing.follows_user_id == follow_user_id)\
329 332 .filter(UserFollowing.user_id == user_id).scalar()
330 333
331 334 if f is not None:
332 335 try:
333 336 self.sa.delete(f)
334 337 return
335 338 except:
336 339 log.error(traceback.format_exc())
337 340 raise
338 341
339 342 try:
340 343 f = UserFollowing()
341 344 f.user_id = user_id
342 345 f.follows_user_id = follow_user_id
343 346 self.sa.add(f)
344 347 except:
345 348 log.error(traceback.format_exc())
346 349 raise
347 350
348 351 def is_following_repo(self, repo_name, user_id, cache=False):
349 352 r = self.sa.query(Repository)\
350 353 .filter(Repository.repo_name == repo_name).scalar()
351 354
352 355 f = self.sa.query(UserFollowing)\
353 356 .filter(UserFollowing.follows_repository == r)\
354 357 .filter(UserFollowing.user_id == user_id).scalar()
355 358
356 359 return f is not None
357 360
358 361 def is_following_user(self, username, user_id, cache=False):
359 362 u = User.get_by_username(username)
360 363
361 364 f = self.sa.query(UserFollowing)\
362 365 .filter(UserFollowing.follows_user == u)\
363 366 .filter(UserFollowing.user_id == user_id).scalar()
364 367
365 368 return f is not None
366 369
367 370 def get_followers(self, repo):
368 371 repo = self._get_repo(repo)
369 372
370 373 return self.sa.query(UserFollowing)\
371 374 .filter(UserFollowing.follows_repository == repo).count()
372 375
373 376 def get_forks(self, repo):
374 377 repo = self._get_repo(repo)
375 378 return self.sa.query(Repository)\
376 379 .filter(Repository.fork == repo).count()
377 380
378 381 def get_pull_requests(self, repo):
379 382 repo = self._get_repo(repo)
380 383 return self.sa.query(PullRequest)\
381 384 .filter(PullRequest.other_repo == repo).count()
382 385
383 386 def mark_as_fork(self, repo, fork, user):
384 387 repo = self.__get_repo(repo)
385 388 fork = self.__get_repo(fork)
386 389 if fork and repo.repo_id == fork.repo_id:
387 390 raise Exception("Cannot set repository as fork of itself")
388 391 repo.fork = fork
389 392 self.sa.add(repo)
390 393 return repo
391 394
392 395 def pull_changes(self, repo, username):
393 396 dbrepo = self.__get_repo(repo)
394 397 clone_uri = dbrepo.clone_uri
395 398 if not clone_uri:
396 399 raise Exception("This repository doesn't have a clone uri")
397 400
398 401 repo = dbrepo.scm_instance
399 402 from rhodecode import CONFIG
400 403 try:
401 404 extras = {
402 405 'ip': '',
403 406 'username': username,
404 407 'action': 'push_remote',
405 408 'repository': dbrepo.repo_name,
406 409 'scm': repo.alias,
407 410 'config': CONFIG['__file__'],
408 411 'make_lock': None,
409 412 'locked_by': [None, None]
410 413 }
411 414
412 415 Repository.inject_ui(repo, extras=extras)
413 416
414 417 if repo.alias == 'git':
415 418 repo.fetch(clone_uri)
416 419 else:
417 420 repo.pull(clone_uri)
418 421 self.mark_for_invalidation(dbrepo.repo_name)
419 422 except:
420 423 log.error(traceback.format_exc())
421 424 raise
422 425
423 426 def commit_change(self, repo, repo_name, cs, user, author, message,
424 427 content, f_path):
425 428 """
426 429 Commits changes
427 430
428 431 :param repo: SCM instance
429 432
430 433 """
431 434
432 435 if repo.alias == 'hg':
433 436 from rhodecode.lib.vcs.backends.hg import \
434 437 MercurialInMemoryChangeset as IMC
435 438 elif repo.alias == 'git':
436 439 from rhodecode.lib.vcs.backends.git import \
437 440 GitInMemoryChangeset as IMC
438 441
439 442 # decoding here will force that we have proper encoded values
440 443 # in any other case this will throw exceptions and deny commit
441 444 content = safe_str(content)
442 445 path = safe_str(f_path)
443 446 # message and author needs to be unicode
444 447 # proper backend should then translate that into required type
445 448 message = safe_unicode(message)
446 449 author = safe_unicode(author)
447 450 m = IMC(repo)
448 451 m.change(FileNode(path, content))
449 452 tip = m.commit(message=message,
450 453 author=author,
451 454 parents=[cs], branch=cs.branch)
452 455
453 456 action = 'push_local:%s' % tip.raw_id
454 457 action_logger(user, action, repo_name)
455 458 self.mark_for_invalidation(repo_name)
456 459 return tip
457 460
458 461 def create_node(self, repo, repo_name, cs, user, author, message, content,
459 462 f_path):
460 463 if repo.alias == 'hg':
461 464 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
462 465 elif repo.alias == 'git':
463 466 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
464 467 # decoding here will force that we have proper encoded values
465 468 # in any other case this will throw exceptions and deny commit
466 469
467 470 if isinstance(content, (basestring,)):
468 471 content = safe_str(content)
469 472 elif isinstance(content, (file, cStringIO.OutputType,)):
470 473 content = content.read()
471 474 else:
472 475 raise Exception('Content is of unrecognized type %s' % (
473 476 type(content)
474 477 ))
475 478
476 479 message = safe_unicode(message)
477 480 author = safe_unicode(author)
478 481 path = safe_str(f_path)
479 482 m = IMC(repo)
480 483
481 484 if isinstance(cs, EmptyChangeset):
482 485 # EmptyChangeset means we we're editing empty repository
483 486 parents = None
484 487 else:
485 488 parents = [cs]
486 489
487 490 m.add(FileNode(path, content=content))
488 491 tip = m.commit(message=message,
489 492 author=author,
490 493 parents=parents, branch=cs.branch)
491 494
492 495 action = 'push_local:%s' % tip.raw_id
493 496 action_logger(user, action, repo_name)
494 497 self.mark_for_invalidation(repo_name)
495 498 return tip
496 499
497 500 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
498 501 """
499 502 recursive walk in root dir and return a set of all path in that dir
500 503 based on repository walk function
501 504
502 505 :param repo_name: name of repository
503 506 :param revision: revision for which to list nodes
504 507 :param root_path: root path to list
505 508 :param flat: return as a list, if False returns a dict with decription
506 509
507 510 """
508 511 _files = list()
509 512 _dirs = list()
510 513 try:
511 514 _repo = self.__get_repo(repo_name)
512 515 changeset = _repo.scm_instance.get_changeset(revision)
513 516 root_path = root_path.lstrip('/')
514 517 for topnode, dirs, files in changeset.walk(root_path):
515 518 for f in files:
516 519 _files.append(f.path if flat else {"name": f.path,
517 520 "type": "file"})
518 521 for d in dirs:
519 522 _dirs.append(d.path if flat else {"name": d.path,
520 523 "type": "dir"})
521 524 except RepositoryError:
522 525 log.debug(traceback.format_exc())
523 526 raise
524 527
525 528 return _dirs, _files
526 529
527 530 def get_unread_journal(self):
528 531 return self.sa.query(UserLog).count()
529 532
530 533 def get_repo_landing_revs(self, repo=None):
531 534 """
532 535 Generates select option with tags branches and bookmarks (for hg only)
533 536 grouped by type
534 537
535 538 :param repo:
536 539 :type repo:
537 540 """
538 541
539 542 hist_l = []
540 543 choices = []
541 544 repo = self.__get_repo(repo)
542 545 hist_l.append(['tip', _('latest tip')])
543 546 choices.append('tip')
544 547 if not repo:
545 548 return choices, hist_l
546 549
547 550 repo = repo.scm_instance
548 551
549 552 branches_group = ([(k, k) for k, v in
550 553 repo.branches.iteritems()], _("Branches"))
551 554 hist_l.append(branches_group)
552 555 choices.extend([x[0] for x in branches_group[0]])
553 556
554 557 if repo.alias == 'hg':
555 558 bookmarks_group = ([(k, k) for k, v in
556 559 repo.bookmarks.iteritems()], _("Bookmarks"))
557 560 hist_l.append(bookmarks_group)
558 561 choices.extend([x[0] for x in bookmarks_group[0]])
559 562
560 563 tags_group = ([(k, k) for k, v in
561 564 repo.tags.iteritems()], _("Tags"))
562 565 hist_l.append(tags_group)
563 566 choices.extend([x[0] for x in tags_group[0]])
564 567
565 568 return choices, hist_l
566 569
567 570 def install_git_hook(self, repo, force_create=False):
568 571 """
569 572 Creates a rhodecode hook inside a git repository
570 573
571 574 :param repo: Instance of VCS repo
572 575 :param force_create: Create even if same name hook exists
573 576 """
574 577
575 578 loc = jn(repo.path, 'hooks')
576 579 if not repo.bare:
577 580 loc = jn(repo.path, '.git', 'hooks')
578 581 if not os.path.isdir(loc):
579 582 os.makedirs(loc)
580 583
581 584 tmpl_post = pkg_resources.resource_string(
582 585 'rhodecode', jn('config', 'post_receive_tmpl.py')
583 586 )
584 587 tmpl_pre = pkg_resources.resource_string(
585 588 'rhodecode', jn('config', 'pre_receive_tmpl.py')
586 589 )
587 590
588 591 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
589 592 _hook_file = jn(loc, '%s-receive' % h_type)
590 593 _rhodecode_hook = False
591 594 log.debug('Installing git hook in repo %s' % repo)
592 595 if os.path.exists(_hook_file):
593 596 # let's take a look at this hook, maybe it's rhodecode ?
594 597 log.debug('hook exists, checking if it is from rhodecode')
595 598 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
596 599 with open(_hook_file, 'rb') as f:
597 600 data = f.read()
598 601 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
599 602 % 'RC_HOOK_VER').search(data)
600 603 if matches:
601 604 try:
602 605 ver = matches.groups()[0]
603 606 log.debug('got %s it is rhodecode' % (ver))
604 607 _rhodecode_hook = True
605 608 except:
606 609 log.error(traceback.format_exc())
607 610 else:
608 611 # there is no hook in this dir, so we want to create one
609 612 _rhodecode_hook = True
610 613
611 614 if _rhodecode_hook or force_create:
612 615 log.debug('writing %s hook file !' % h_type)
613 616 with open(_hook_file, 'wb') as f:
614 617 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
615 618 f.write(tmpl)
616 619 os.chmod(_hook_file, 0755)
617 620 else:
618 621 log.debug('skipping writing hook file')
General Comments 0
You need to be logged in to leave comments. Login now