##// END OF EJS Templates
invalidation: merge .invalidate and .set_valid as .test_and_set_valid...
Mads Kiilerich -
r3772:910ad1ff beta
parent child Browse files
Show More
@@ -1,181 +1,179 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.feed
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Feed controller for rhodecode
7 7
8 8 :created_on: Apr 23, 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
26 26 import logging
27 27
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 31 from beaker.cache import cache_region, region_invalidate
32 32 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
33 33
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 36 from rhodecode.lib.base import BaseRepoController
37 37 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
38 38 from rhodecode.model.db import CacheInvalidation
39 39 from rhodecode.lib.utils2 import safe_int, str2bool, safe_unicode
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class FeedController(BaseRepoController):
45 45
46 46 @LoginRequired(api_access=True)
47 47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 48 'repository.admin')
49 49 def __before__(self):
50 50 super(FeedController, self).__before__()
51 51 #common values for feeds
52 52 self.description = _('Changes on %s repository')
53 53 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
54 54 self.language = 'en-us'
55 55 self.ttl = "5"
56 56 import rhodecode
57 57 CONF = rhodecode.CONFIG
58 58 self.include_diff = str2bool(CONF.get('rss_include_diff', False))
59 59 self.feed_nr = safe_int(CONF.get('rss_items_per_page', 20))
60 60 # we need to protect from parsing huge diffs here other way
61 61 # we can kill the server
62 62 self.feed_diff_limit = safe_int(CONF.get('rss_cut_off_limit', 32 * 1024))
63 63
64 64 def _get_title(self, cs):
65 65 return "%s" % (
66 66 h.shorter(cs.message, 160)
67 67 )
68 68
69 69 def __changes(self, cs):
70 70 changes = []
71 71 diff_processor = DiffProcessor(cs.diff(),
72 72 diff_limit=self.feed_diff_limit)
73 73 _parsed = diff_processor.prepare(inline_diff=False)
74 74 limited_diff = False
75 75 if isinstance(_parsed, LimitedDiffContainer):
76 76 limited_diff = True
77 77
78 78 for st in _parsed:
79 79 st.update({'added': st['stats'][0],
80 80 'removed': st['stats'][1]})
81 81 changes.append('\n %(operation)s %(filename)s '
82 82 '(%(added)s lines added, %(removed)s lines removed)'
83 83 % st)
84 84 if limited_diff:
85 85 changes = changes + ['\n ' +
86 86 _('Changeset was too big and was cut off...')]
87 87 return diff_processor, changes
88 88
89 89 def __get_desc(self, cs):
90 90 desc_msg = []
91 91 desc_msg.append((_('%s committed on %s')
92 92 % (h.person(cs.author), h.fmt_date(cs.date))) + '<br/>')
93 93 #branches, tags, bookmarks
94 94 if cs.branch:
95 95 desc_msg.append('branch: %s<br/>' % cs.branch)
96 96 if h.is_hg(c.rhodecode_repo):
97 97 for book in cs.bookmarks:
98 98 desc_msg.append('bookmark: %s<br/>' % book)
99 99 for tag in cs.tags:
100 100 desc_msg.append('tag: %s<br/>' % tag)
101 101 diff_processor, changes = self.__changes(cs)
102 102 # rev link
103 103 _url = url('changeset_home', repo_name=cs.repository.name,
104 104 revision=cs.raw_id, qualified=True)
105 105 desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
106 106
107 107 desc_msg.append('<pre>')
108 108 desc_msg.append(cs.message)
109 109 desc_msg.append('\n')
110 110 desc_msg.extend(changes)
111 111 if self.include_diff:
112 112 desc_msg.append('\n\n')
113 113 desc_msg.append(diff_processor.as_raw())
114 114 desc_msg.append('</pre>')
115 115 return map(safe_unicode, desc_msg)
116 116
117 117 def atom(self, repo_name):
118 118 """Produce an atom-1.0 feed via feedgenerator module"""
119 119
120 120 @cache_region('long_term')
121 121 def _get_feed_from_cache(key):
122 122 feed = Atom1Feed(
123 123 title=self.title % repo_name,
124 124 link=url('summary_home', repo_name=repo_name,
125 125 qualified=True),
126 126 description=self.description % repo_name,
127 127 language=self.language,
128 128 ttl=self.ttl
129 129 )
130 130
131 131 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
132 132 feed.add_item(title=self._get_title(cs),
133 133 link=url('changeset_home', repo_name=repo_name,
134 134 revision=cs.raw_id, qualified=True),
135 135 author_name=cs.author,
136 136 description=''.join(self.__get_desc(cs)),
137 137 pubdate=cs.date,
138 138 )
139 139
140 140 response.content_type = feed.mime_type
141 141 return feed.writeString('utf-8')
142 142
143 143 key = repo_name + '_ATOM'
144 inv = CacheInvalidation.invalidate(key)
145 if inv is not None:
144 valid = CacheInvalidation.test_and_set_valid(key)
145 if not valid:
146 146 region_invalidate(_get_feed_from_cache, None, key)
147 CacheInvalidation.set_valid(inv.cache_key)
148 147 return _get_feed_from_cache(key)
149 148
150 149 def rss(self, repo_name):
151 150 """Produce an rss2 feed via feedgenerator module"""
152 151
153 152 @cache_region('long_term')
154 153 def _get_feed_from_cache(key):
155 154 feed = Rss201rev2Feed(
156 155 title=self.title % repo_name,
157 156 link=url('summary_home', repo_name=repo_name,
158 157 qualified=True),
159 158 description=self.description % repo_name,
160 159 language=self.language,
161 160 ttl=self.ttl
162 161 )
163 162
164 163 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
165 164 feed.add_item(title=self._get_title(cs),
166 165 link=url('changeset_home', repo_name=repo_name,
167 166 revision=cs.raw_id, qualified=True),
168 167 author_name=cs.author,
169 168 description=''.join(self.__get_desc(cs)),
170 169 pubdate=cs.date,
171 170 )
172 171
173 172 response.content_type = feed.mime_type
174 173 return feed.writeString('utf-8')
175 174
176 175 key = repo_name + '_RSS'
177 inv = CacheInvalidation.invalidate(key)
178 if inv is not None:
176 valid = CacheInvalidation.test_and_set_valid(key)
177 if not valid:
179 178 region_invalidate(_get_feed_from_cache, None, key)
180 CacheInvalidation.set_valid(inv.cache_key)
181 179 return _get_feed_from_cache(key)
@@ -1,244 +1,242 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.summary
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Summary controller for Rhodecode
7 7
8 8 :created_on: Apr 18, 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
26 26 import traceback
27 27 import calendar
28 28 import logging
29 29 import urllib
30 30 from time import mktime
31 31 from datetime import timedelta, date
32 32 from urlparse import urlparse
33 33
34 34 from pylons import tmpl_context as c, request, url, config
35 35 from pylons.i18n.translation import _
36 36 from webob.exc import HTTPBadRequest
37 37
38 38 from beaker.cache import cache_region, region_invalidate
39 39
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.lib.compat import product
42 42 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
43 43 NodeDoesNotExistError
44 44 from rhodecode.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
45 45 from rhodecode.model.db import Statistics, CacheInvalidation
46 46 from rhodecode.lib.utils import jsonify
47 47 from rhodecode.lib.utils2 import safe_unicode, safe_str
48 48 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
49 49 NotAnonymous
50 50 from rhodecode.lib.base import BaseRepoController, render
51 51 from rhodecode.lib.vcs.backends.base import EmptyChangeset
52 52 from rhodecode.lib.markup_renderer import MarkupRenderer
53 53 from rhodecode.lib.celerylib import run_task
54 54 from rhodecode.lib.celerylib.tasks import get_commits_stats
55 55 from rhodecode.lib.helpers import RepoPage
56 56 from rhodecode.lib.compat import json, OrderedDict
57 57 from rhodecode.lib.vcs.nodes import FileNode
58 58 from rhodecode.controllers.changelog import _load_changelog_summary
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
63 63 sorted(list(product(ALL_READMES, ALL_EXTS)),
64 64 key=lambda y:y[0][1] + y[1][1])]
65 65
66 66
67 67 class SummaryController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(SummaryController, self).__before__()
71 71
72 72 def _get_download_links(self, repo):
73 73
74 74 download_l = []
75 75
76 76 branches_group = ([], _("Branches"))
77 77 tags_group = ([], _("Tags"))
78 78
79 79 for name, chs in c.rhodecode_repo.branches.items():
80 80 #chs = chs.split(':')[-1]
81 81 branches_group[0].append((chs, name),)
82 82 download_l.append(branches_group)
83 83
84 84 for name, chs in c.rhodecode_repo.tags.items():
85 85 #chs = chs.split(':')[-1]
86 86 tags_group[0].append((chs, name),)
87 87 download_l.append(tags_group)
88 88
89 89 return download_l
90 90
91
92 91 def __get_readme_data(self, db_repo):
93 92 repo_name = db_repo.repo_name
94 93
95 94 @cache_region('long_term')
96 95 def _get_readme_from_cache(key):
97 96 readme_data = None
98 97 readme_file = None
99 98 log.debug('Looking for README file')
100 99 try:
101 100 # get's the landing revision! or tip if fails
102 101 cs = db_repo.get_landing_changeset()
103 102 if isinstance(cs, EmptyChangeset):
104 103 raise EmptyRepositoryError()
105 104 renderer = MarkupRenderer()
106 105 for f in README_FILES:
107 106 try:
108 107 readme = cs.get_node(f)
109 108 if not isinstance(readme, FileNode):
110 109 continue
111 110 readme_file = f
112 111 log.debug('Found README file `%s` rendering...' %
113 112 readme_file)
114 113 readme_data = renderer.render(readme.content, f)
115 114 break
116 115 except NodeDoesNotExistError:
117 116 continue
118 117 except ChangesetError:
119 118 log.error(traceback.format_exc())
120 119 pass
121 120 except EmptyRepositoryError:
122 121 pass
123 122 except Exception:
124 123 log.error(traceback.format_exc())
125 124
126 125 return readme_data, readme_file
127 126
128 127 key = repo_name + '_README'
129 inv = CacheInvalidation.invalidate(key)
130 if inv is not None:
128 valid = CacheInvalidation.test_and_set_valid(key)
129 if not valid:
131 130 region_invalidate(_get_readme_from_cache, None, key)
132 CacheInvalidation.set_valid(inv.cache_key)
133 131 return _get_readme_from_cache(key)
134 132
135 133 @LoginRequired()
136 134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
137 135 'repository.admin')
138 136 def index(self, repo_name):
139 137 c.dbrepo = dbrepo = c.rhodecode_db_repo
140 138 _load_changelog_summary()
141 139 if self.rhodecode_user.username == 'default':
142 140 # for default(anonymous) user we don't need to pass credentials
143 141 username = ''
144 142 password = ''
145 143 else:
146 144 username = str(self.rhodecode_user.username)
147 145 password = '@'
148 146
149 147 parsed_url = urlparse(url.current(qualified=True))
150 148
151 149 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
152 150
153 151 uri_tmpl = config.get('clone_uri', default_clone_uri)
154 152 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
155 153 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
156 154 uri_dict = {
157 155 'user': urllib.quote(username),
158 156 'pass': password,
159 157 'scheme': parsed_url.scheme,
160 158 'netloc': parsed_url.netloc,
161 159 'path': urllib.quote(safe_str(decoded_path))
162 160 }
163 161
164 162 uri = (uri_tmpl % uri_dict)
165 163 # generate another clone url by id
166 164 uri_dict.update(
167 165 {'path': decoded_path.replace(repo_name, '_%s' % c.dbrepo.repo_id)}
168 166 )
169 167 uri_id = uri_tmpl % uri_dict
170 168
171 169 c.clone_repo_url = uri
172 170 c.clone_repo_url_id = uri_id
173 171
174 172 td = date.today() + timedelta(days=1)
175 173 td_1m = td - timedelta(days=calendar.mdays[td.month])
176 174 td_1y = td - timedelta(days=365)
177 175
178 176 ts_min_m = mktime(td_1m.timetuple())
179 177 ts_min_y = mktime(td_1y.timetuple())
180 178 ts_max_y = mktime(td.timetuple())
181 179
182 180 if dbrepo.enable_statistics:
183 181 c.show_stats = True
184 182 c.no_data_msg = _('No data loaded yet')
185 183 recurse_limit = 500 # don't recurse more than 500 times when parsing
186 184 run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y,
187 185 ts_max_y, recurse_limit)
188 186 else:
189 187 c.show_stats = False
190 188 c.no_data_msg = _('Statistics are disabled for this repository')
191 189 c.ts_min = ts_min_m
192 190 c.ts_max = ts_max_y
193 191
194 192 stats = self.sa.query(Statistics)\
195 193 .filter(Statistics.repository == dbrepo)\
196 194 .scalar()
197 195
198 196 c.stats_percentage = 0
199 197
200 198 if stats and stats.languages:
201 199 c.no_data = False is dbrepo.enable_statistics
202 200 lang_stats_d = json.loads(stats.languages)
203 201 c.commit_data = stats.commit_activity
204 202 c.overview_data = stats.commit_activity_combined
205 203
206 204 lang_stats = ((x, {"count": y,
207 205 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
208 206 for x, y in lang_stats_d.items())
209 207
210 208 c.trending_languages = json.dumps(
211 209 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
212 210 )
213 211 last_rev = stats.stat_on_revision + 1
214 212 c.repo_last_rev = c.rhodecode_repo.count()\
215 213 if c.rhodecode_repo.revisions else 0
216 214 if last_rev == 0 or c.repo_last_rev == 0:
217 215 pass
218 216 else:
219 217 c.stats_percentage = '%.2f' % ((float((last_rev)) /
220 218 c.repo_last_rev) * 100)
221 219 else:
222 220 c.commit_data = json.dumps({})
223 221 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
224 222 c.trending_languages = json.dumps({})
225 223 c.no_data = True
226 224
227 225 c.enable_downloads = dbrepo.enable_downloads
228 226 if c.enable_downloads:
229 227 c.download_options = self._get_download_links(c.rhodecode_repo)
230 228
231 229 c.readme_data, c.readme_file = \
232 230 self.__get_readme_data(c.rhodecode_db_repo)
233 231 return render('summary/summary.html')
234 232
235 233 @LoginRequired()
236 234 @NotAnonymous()
237 235 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 236 'repository.admin')
239 237 @jsonify
240 238 def repo_size(self, repo_name):
241 239 if request.is_xhr:
242 240 return c.rhodecode_db_repo._repo_size()
243 241 else:
244 242 raise HTTPBadRequest()
@@ -1,799 +1,799 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 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
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 35 import decorator
36 36 import warnings
37 37 from os.path import abspath
38 38 from os.path import dirname as dn, join as jn
39 39
40 40 from paste.script.command import Command, BadCommand
41 41
42 42 from mercurial import ui, config
43 43
44 44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 45
46 46 from rhodecode.lib.vcs import get_backend
47 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
48 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
49 49 from rhodecode.lib.vcs.utils.helpers import get_scm
50 50 from rhodecode.lib.vcs.exceptions import VCSError
51 51
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model import meta
55 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
56 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation, UserGroup
57 57 from rhodecode.model.meta import Session
58 58 from rhodecode.model.repos_group import ReposGroupModel
59 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
60 60 from rhodecode.lib.vcs.utils.fakemod import create_module
61 61 from rhodecode.model.users_group import UserGroupModel
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
66 66
67 67
68 68 def recursive_replace(str_, replace=' '):
69 69 """
70 70 Recursive replace of given sign to just one instance
71 71
72 72 :param str_: given string
73 73 :param replace: char to find and replace multiple instances
74 74
75 75 Examples::
76 76 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
77 77 'Mighty-Mighty-Bo-sstones'
78 78 """
79 79
80 80 if str_.find(replace * 2) == -1:
81 81 return str_
82 82 else:
83 83 str_ = str_.replace(replace * 2, replace)
84 84 return recursive_replace(str_, replace)
85 85
86 86
87 87 def repo_name_slug(value):
88 88 """
89 89 Return slug of name of repository
90 90 This function is called on each creation/modification
91 91 of repository to prevent bad names in repo
92 92 """
93 93
94 94 slug = remove_formatting(value)
95 95 slug = strip_tags(slug)
96 96
97 97 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
98 98 slug = slug.replace(c, '-')
99 99 slug = recursive_replace(slug, '-')
100 100 slug = collapse(slug, '-')
101 101 return slug
102 102
103 103
104 104 #==============================================================================
105 105 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
106 106 #==============================================================================
107 107 def get_repo_slug(request):
108 108 _repo = request.environ['pylons.routes_dict'].get('repo_name')
109 109 if _repo:
110 110 _repo = _repo.rstrip('/')
111 111 return _repo
112 112
113 113
114 114 def get_repos_group_slug(request):
115 115 _group = request.environ['pylons.routes_dict'].get('group_name')
116 116 if _group:
117 117 _group = _group.rstrip('/')
118 118 return _group
119 119
120 120
121 121 def get_user_group_slug(request):
122 122 _group = request.environ['pylons.routes_dict'].get('id')
123 123 try:
124 124 _group = UserGroup.get(_group)
125 125 if _group:
126 126 _group = _group.users_group_name
127 127 except Exception:
128 128 log.debug(traceback.format_exc())
129 129 #catch all failures here
130 130 pass
131 131
132 132 return _group
133 133
134 134
135 135 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
136 136 """
137 137 Action logger for various actions made by users
138 138
139 139 :param user: user that made this action, can be a unique username string or
140 140 object containing user_id attribute
141 141 :param action: action to log, should be on of predefined unique actions for
142 142 easy translations
143 143 :param repo: string name of repository or object containing repo_id,
144 144 that action was made on
145 145 :param ipaddr: optional ip address from what the action was made
146 146 :param sa: optional sqlalchemy session
147 147
148 148 """
149 149
150 150 if not sa:
151 151 sa = meta.Session()
152 152
153 153 try:
154 154 if hasattr(user, 'user_id'):
155 155 user_obj = User.get(user.user_id)
156 156 elif isinstance(user, basestring):
157 157 user_obj = User.get_by_username(user)
158 158 else:
159 159 raise Exception('You have to provide a user object or a username')
160 160
161 161 if hasattr(repo, 'repo_id'):
162 162 repo_obj = Repository.get(repo.repo_id)
163 163 repo_name = repo_obj.repo_name
164 164 elif isinstance(repo, basestring):
165 165 repo_name = repo.lstrip('/')
166 166 repo_obj = Repository.get_by_repo_name(repo_name)
167 167 else:
168 168 repo_obj = None
169 169 repo_name = ''
170 170
171 171 user_log = UserLog()
172 172 user_log.user_id = user_obj.user_id
173 173 user_log.username = user_obj.username
174 174 user_log.action = safe_unicode(action)
175 175
176 176 user_log.repository = repo_obj
177 177 user_log.repository_name = repo_name
178 178
179 179 user_log.action_date = datetime.datetime.now()
180 180 user_log.user_ip = ipaddr
181 181 sa.add(user_log)
182 182
183 183 log.info('Logging action:%s on %s by user:%s ip:%s' %
184 184 (action, safe_unicode(repo), user_obj, ipaddr))
185 185 if commit:
186 186 sa.commit()
187 187 except Exception:
188 188 log.error(traceback.format_exc())
189 189 raise
190 190
191 191
192 192 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
193 193 """
194 194 Scans given path for repos and return (name,(type,path)) tuple
195 195
196 196 :param path: path to scan for repositories
197 197 :param recursive: recursive search and return names with subdirs in front
198 198 """
199 199
200 200 # remove ending slash for better results
201 201 path = path.rstrip(os.sep)
202 202 log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
203 203
204 204 def _get_repos(p):
205 205 if not os.access(p, os.W_OK):
206 206 log.warn('ignoring repo path without write access: %s', p)
207 207 return
208 208 for dirpath in os.listdir(p):
209 209 if os.path.isfile(os.path.join(p, dirpath)):
210 210 continue
211 211 cur_path = os.path.join(p, dirpath)
212 212
213 213 # skip removed repos
214 214 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
215 215 continue
216 216
217 217 #skip .<somethin> dirs
218 218 if dirpath.startswith('.'):
219 219 continue
220 220
221 221 try:
222 222 scm_info = get_scm(cur_path)
223 223 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
224 224 except VCSError:
225 225 if not recursive:
226 226 continue
227 227 #check if this dir containts other repos for recursive scan
228 228 rec_path = os.path.join(p, dirpath)
229 229 if os.path.isdir(rec_path):
230 230 for inner_scm in _get_repos(rec_path):
231 231 yield inner_scm
232 232
233 233 return _get_repos(path)
234 234
235 235
236 236 def is_valid_repo(repo_name, base_path, scm=None):
237 237 """
238 238 Returns True if given path is a valid repository False otherwise.
239 239 If scm param is given also compare if given scm is the same as expected
240 240 from scm parameter
241 241
242 242 :param repo_name:
243 243 :param base_path:
244 244 :param scm:
245 245
246 246 :return True: if given path is a valid repository
247 247 """
248 248 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
249 249
250 250 try:
251 251 scm_ = get_scm(full_path)
252 252 if scm:
253 253 return scm_[0] == scm
254 254 return True
255 255 except VCSError:
256 256 return False
257 257
258 258
259 259 def is_valid_repos_group(repos_group_name, base_path, skip_path_check=False):
260 260 """
261 261 Returns True if given path is a repository group False otherwise
262 262
263 263 :param repo_name:
264 264 :param base_path:
265 265 """
266 266 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
267 267
268 268 # check if it's not a repo
269 269 if is_valid_repo(repos_group_name, base_path):
270 270 return False
271 271
272 272 try:
273 273 # we need to check bare git repos at higher level
274 274 # since we might match branches/hooks/info/objects or possible
275 275 # other things inside bare git repo
276 276 get_scm(os.path.dirname(full_path))
277 277 return False
278 278 except VCSError:
279 279 pass
280 280
281 281 # check if it's a valid path
282 282 if skip_path_check or os.path.isdir(full_path):
283 283 return True
284 284
285 285 return False
286 286
287 287
288 288 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
289 289 while True:
290 290 ok = raw_input(prompt)
291 291 if ok in ('y', 'ye', 'yes'):
292 292 return True
293 293 if ok in ('n', 'no', 'nop', 'nope'):
294 294 return False
295 295 retries = retries - 1
296 296 if retries < 0:
297 297 raise IOError
298 298 print complaint
299 299
300 300 #propagated from mercurial documentation
301 301 ui_sections = ['alias', 'auth',
302 302 'decode/encode', 'defaults',
303 303 'diff', 'email',
304 304 'extensions', 'format',
305 305 'merge-patterns', 'merge-tools',
306 306 'hooks', 'http_proxy',
307 307 'smtp', 'patch',
308 308 'paths', 'profiling',
309 309 'server', 'trusted',
310 310 'ui', 'web', ]
311 311
312 312
313 313 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
314 314 """
315 315 A function that will read python rc files or database
316 316 and make an mercurial ui object from read options
317 317
318 318 :param path: path to mercurial config file
319 319 :param checkpaths: check the path
320 320 :param read_from: read from 'file' or 'db'
321 321 """
322 322
323 323 baseui = ui.ui()
324 324
325 325 # clean the baseui object
326 326 baseui._ocfg = config.config()
327 327 baseui._ucfg = config.config()
328 328 baseui._tcfg = config.config()
329 329
330 330 if read_from == 'file':
331 331 if not os.path.isfile(path):
332 332 log.debug('hgrc file is not present at %s, skipping...' % path)
333 333 return False
334 334 log.debug('reading hgrc from %s' % path)
335 335 cfg = config.config()
336 336 cfg.read(path)
337 337 for section in ui_sections:
338 338 for k, v in cfg.items(section):
339 339 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
340 340 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
341 341
342 342 elif read_from == 'db':
343 343 sa = meta.Session()
344 344 ret = sa.query(RhodeCodeUi)\
345 345 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
346 346 .all()
347 347
348 348 hg_ui = ret
349 349 for ui_ in hg_ui:
350 350 if ui_.ui_active:
351 351 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
352 352 ui_.ui_key, ui_.ui_value)
353 353 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
354 354 safe_str(ui_.ui_value))
355 355 if ui_.ui_key == 'push_ssl':
356 356 # force set push_ssl requirement to False, rhodecode
357 357 # handles that
358 358 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
359 359 False)
360 360 if clear_session:
361 361 meta.Session.remove()
362 362 return baseui
363 363
364 364
365 365 def set_rhodecode_config(config):
366 366 """
367 367 Updates pylons config with new settings from database
368 368
369 369 :param config:
370 370 """
371 371 hgsettings = RhodeCodeSetting.get_app_settings()
372 372
373 373 for k, v in hgsettings.items():
374 374 config[k] = v
375 375
376 376
377 377 def map_groups(path):
378 378 """
379 379 Given a full path to a repository, create all nested groups that this
380 380 repo is inside. This function creates parent-child relationships between
381 381 groups and creates default perms for all new groups.
382 382
383 383 :param paths: full path to repository
384 384 """
385 385 sa = meta.Session()
386 386 groups = path.split(Repository.url_sep())
387 387 parent = None
388 388 group = None
389 389
390 390 # last element is repo in nested groups structure
391 391 groups = groups[:-1]
392 392 rgm = ReposGroupModel(sa)
393 393 owner = User.get_first_admin()
394 394 for lvl, group_name in enumerate(groups):
395 395 group_name = '/'.join(groups[:lvl] + [group_name])
396 396 group = RepoGroup.get_by_group_name(group_name)
397 397 desc = '%s group' % group_name
398 398
399 399 # skip folders that are now removed repos
400 400 if REMOVED_REPO_PAT.match(group_name):
401 401 break
402 402
403 403 if group is None:
404 404 log.debug('creating group level: %s group_name: %s'
405 405 % (lvl, group_name))
406 406 group = RepoGroup(group_name, parent)
407 407 group.group_description = desc
408 408 group.user = owner
409 409 sa.add(group)
410 410 perm_obj = rgm._create_default_perms(group)
411 411 sa.add(perm_obj)
412 412 sa.flush()
413 413
414 414 parent = group
415 415 return group
416 416
417 417
418 418 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
419 419 install_git_hook=False):
420 420 """
421 421 maps all repos given in initial_repo_list, non existing repositories
422 422 are created, if remove_obsolete is True it also check for db entries
423 423 that are not in initial_repo_list and removes them.
424 424
425 425 :param initial_repo_list: list of repositories found by scanning methods
426 426 :param remove_obsolete: check for obsolete entries in database
427 427 :param install_git_hook: if this is True, also check and install githook
428 428 for a repo if missing
429 429 """
430 430 from rhodecode.model.repo import RepoModel
431 431 from rhodecode.model.scm import ScmModel
432 432 sa = meta.Session()
433 433 rm = RepoModel()
434 434 user = User.get_first_admin()
435 435 added = []
436 436
437 437 ##creation defaults
438 438 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
439 439 enable_statistics = defs.get('repo_enable_statistics')
440 440 enable_locking = defs.get('repo_enable_locking')
441 441 enable_downloads = defs.get('repo_enable_downloads')
442 442 private = defs.get('repo_private')
443 443
444 444 for name, repo in initial_repo_list.items():
445 445 group = map_groups(name)
446 446 db_repo = rm.get_by_repo_name(name)
447 447 # found repo that is on filesystem not in RhodeCode database
448 448 if not db_repo:
449 449 log.info('repository %s not found, creating now' % name)
450 450 added.append(name)
451 451 desc = (repo.description
452 452 if repo.description != 'unknown'
453 453 else '%s repository' % name)
454 454
455 455 new_repo = rm.create_repo(
456 456 repo_name=name,
457 457 repo_type=repo.alias,
458 458 description=desc,
459 459 repos_group=getattr(group, 'group_id', None),
460 460 owner=user,
461 461 just_db=True,
462 462 enable_locking=enable_locking,
463 463 enable_downloads=enable_downloads,
464 464 enable_statistics=enable_statistics,
465 465 private=private
466 466 )
467 467 # we added that repo just now, and make sure it has githook
468 468 # installed
469 469 if new_repo.repo_type == 'git':
470 470 ScmModel().install_git_hook(new_repo.scm_instance)
471 471 new_repo.update_changeset_cache()
472 472 elif install_git_hook:
473 473 if db_repo.repo_type == 'git':
474 474 ScmModel().install_git_hook(db_repo.scm_instance)
475 475 # during starting install all cache keys for all repositories in the
476 476 # system, this will register all repos and multiple instances
477 477 cache_key = CacheInvalidation._get_cache_key(name)
478 478 log.debug("Creating invalidation cache key for %s: %s", name, cache_key)
479 CacheInvalidation.invalidate(name)
479 CacheInvalidation.test_and_set_valid(name)
480 480
481 481 sa.commit()
482 482 removed = []
483 483 if remove_obsolete:
484 484 # remove from database those repositories that are not in the filesystem
485 485 for repo in sa.query(Repository).all():
486 486 if repo.repo_name not in initial_repo_list.keys():
487 487 log.debug("Removing non-existing repository found in db `%s`" %
488 488 repo.repo_name)
489 489 try:
490 490 removed.append(repo.repo_name)
491 491 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
492 492 sa.commit()
493 493 except Exception:
494 494 #don't hold further removals on error
495 495 log.error(traceback.format_exc())
496 496 sa.rollback()
497 497 return added, removed
498 498
499 499
500 500 # set cache regions for beaker so celery can utilise it
501 501 def add_cache(settings):
502 502 cache_settings = {'regions': None}
503 503 for key in settings.keys():
504 504 for prefix in ['beaker.cache.', 'cache.']:
505 505 if key.startswith(prefix):
506 506 name = key.split(prefix)[1].strip()
507 507 cache_settings[name] = settings[key].strip()
508 508 if cache_settings['regions']:
509 509 for region in cache_settings['regions'].split(','):
510 510 region = region.strip()
511 511 region_settings = {}
512 512 for key, value in cache_settings.items():
513 513 if key.startswith(region):
514 514 region_settings[key.split('.')[1]] = value
515 515 region_settings['expire'] = int(region_settings.get('expire',
516 516 60))
517 517 region_settings.setdefault('lock_dir',
518 518 cache_settings.get('lock_dir'))
519 519 region_settings.setdefault('data_dir',
520 520 cache_settings.get('data_dir'))
521 521
522 522 if 'type' not in region_settings:
523 523 region_settings['type'] = cache_settings.get('type',
524 524 'memory')
525 525 beaker.cache.cache_regions[region] = region_settings
526 526
527 527
528 528 def load_rcextensions(root_path):
529 529 import rhodecode
530 530 from rhodecode.config import conf
531 531
532 532 path = os.path.join(root_path, 'rcextensions', '__init__.py')
533 533 if os.path.isfile(path):
534 534 rcext = create_module('rc', path)
535 535 EXT = rhodecode.EXTENSIONS = rcext
536 536 log.debug('Found rcextensions now loading %s...' % rcext)
537 537
538 538 # Additional mappings that are not present in the pygments lexers
539 539 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
540 540
541 541 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
542 542
543 543 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
544 544 log.debug('settings custom INDEX_EXTENSIONS')
545 545 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
546 546
547 547 #ADDITIONAL MAPPINGS
548 548 log.debug('adding extra into INDEX_EXTENSIONS')
549 549 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
550 550
551 551 # auto check if the module is not missing any data, set to default if is
552 552 # this will help autoupdate new feature of rcext module
553 553 from rhodecode.config import rcextensions
554 554 for k in dir(rcextensions):
555 555 if not k.startswith('_') and not hasattr(EXT, k):
556 556 setattr(EXT, k, getattr(rcextensions, k))
557 557
558 558
559 559 def get_custom_lexer(extension):
560 560 """
561 561 returns a custom lexer if it's defined in rcextensions module, or None
562 562 if there's no custom lexer defined
563 563 """
564 564 import rhodecode
565 565 from pygments import lexers
566 566 #check if we didn't define this extension as other lexer
567 567 if rhodecode.EXTENSIONS and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
568 568 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
569 569 return lexers.get_lexer_by_name(_lexer_name)
570 570
571 571
572 572 #==============================================================================
573 573 # TEST FUNCTIONS AND CREATORS
574 574 #==============================================================================
575 575 def create_test_index(repo_location, config, full_index):
576 576 """
577 577 Makes default test index
578 578
579 579 :param config: test config
580 580 :param full_index:
581 581 """
582 582
583 583 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
584 584 from rhodecode.lib.pidlock import DaemonLock, LockHeld
585 585
586 586 repo_location = repo_location
587 587
588 588 index_location = os.path.join(config['app_conf']['index_dir'])
589 589 if not os.path.exists(index_location):
590 590 os.makedirs(index_location)
591 591
592 592 try:
593 593 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
594 594 WhooshIndexingDaemon(index_location=index_location,
595 595 repo_location=repo_location)\
596 596 .run(full_index=full_index)
597 597 l.release()
598 598 except LockHeld:
599 599 pass
600 600
601 601
602 602 def create_test_env(repos_test_path, config):
603 603 """
604 604 Makes a fresh database and
605 605 install test repository into tmp dir
606 606 """
607 607 from rhodecode.lib.db_manage import DbManage
608 608 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
609 609
610 610 # PART ONE create db
611 611 dbconf = config['sqlalchemy.db1.url']
612 612 log.debug('making test db %s' % dbconf)
613 613
614 614 # create test dir if it doesn't exist
615 615 if not os.path.isdir(repos_test_path):
616 616 log.debug('Creating testdir %s' % repos_test_path)
617 617 os.makedirs(repos_test_path)
618 618
619 619 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
620 620 tests=True)
621 621 dbmanage.create_tables(override=True)
622 622 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
623 623 dbmanage.create_default_user()
624 624 dbmanage.admin_prompt()
625 625 dbmanage.create_permissions()
626 626 dbmanage.populate_default_permissions()
627 627 Session().commit()
628 628 # PART TWO make test repo
629 629 log.debug('making test vcs repositories')
630 630
631 631 idx_path = config['app_conf']['index_dir']
632 632 data_path = config['app_conf']['cache_dir']
633 633
634 634 #clean index and data
635 635 if idx_path and os.path.exists(idx_path):
636 636 log.debug('remove %s' % idx_path)
637 637 shutil.rmtree(idx_path)
638 638
639 639 if data_path and os.path.exists(data_path):
640 640 log.debug('remove %s' % data_path)
641 641 shutil.rmtree(data_path)
642 642
643 643 #CREATE DEFAULT TEST REPOS
644 644 cur_dir = dn(dn(abspath(__file__)))
645 645 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
646 646 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
647 647 tar.close()
648 648
649 649 cur_dir = dn(dn(abspath(__file__)))
650 650 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
651 651 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
652 652 tar.close()
653 653
654 654 #LOAD VCS test stuff
655 655 from rhodecode.tests.vcs import setup_package
656 656 setup_package()
657 657
658 658
659 659 #==============================================================================
660 660 # PASTER COMMANDS
661 661 #==============================================================================
662 662 class BasePasterCommand(Command):
663 663 """
664 664 Abstract Base Class for paster commands.
665 665
666 666 The celery commands are somewhat aggressive about loading
667 667 celery.conf, and since our module sets the `CELERY_LOADER`
668 668 environment variable to our loader, we have to bootstrap a bit and
669 669 make sure we've had a chance to load the pylons config off of the
670 670 command line, otherwise everything fails.
671 671 """
672 672 min_args = 1
673 673 min_args_error = "Please provide a paster config file as an argument."
674 674 takes_config_file = 1
675 675 requires_config_file = True
676 676
677 677 def notify_msg(self, msg, log=False):
678 678 """Make a notification to user, additionally if logger is passed
679 679 it logs this action using given logger
680 680
681 681 :param msg: message that will be printed to user
682 682 :param log: logging instance, to use to additionally log this message
683 683
684 684 """
685 685 if log and isinstance(log, logging):
686 686 log(msg)
687 687
688 688 def run(self, args):
689 689 """
690 690 Overrides Command.run
691 691
692 692 Checks for a config file argument and loads it.
693 693 """
694 694 if len(args) < self.min_args:
695 695 raise BadCommand(
696 696 self.min_args_error % {'min_args': self.min_args,
697 697 'actual_args': len(args)})
698 698
699 699 # Decrement because we're going to lob off the first argument.
700 700 # @@ This is hacky
701 701 self.min_args -= 1
702 702 self.bootstrap_config(args[0])
703 703 self.update_parser()
704 704 return super(BasePasterCommand, self).run(args[1:])
705 705
706 706 def update_parser(self):
707 707 """
708 708 Abstract method. Allows for the class's parser to be updated
709 709 before the superclass's `run` method is called. Necessary to
710 710 allow options/arguments to be passed through to the underlying
711 711 celery command.
712 712 """
713 713 raise NotImplementedError("Abstract Method.")
714 714
715 715 def bootstrap_config(self, conf):
716 716 """
717 717 Loads the pylons configuration.
718 718 """
719 719 from pylons import config as pylonsconfig
720 720
721 721 self.path_to_ini_file = os.path.realpath(conf)
722 722 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
723 723 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
724 724
725 725 def _init_session(self):
726 726 """
727 727 Inits SqlAlchemy Session
728 728 """
729 729 logging.config.fileConfig(self.path_to_ini_file)
730 730 from pylons import config
731 731 from rhodecode.model import init_model
732 732 from rhodecode.lib.utils2 import engine_from_config
733 733
734 734 #get to remove repos !!
735 735 add_cache(config)
736 736 engine = engine_from_config(config, 'sqlalchemy.db1.')
737 737 init_model(engine)
738 738
739 739
740 740 def check_git_version():
741 741 """
742 742 Checks what version of git is installed in system, and issues a warning
743 743 if it's too old for RhodeCode to properly work.
744 744 """
745 745 from rhodecode import BACKENDS
746 746 from rhodecode.lib.vcs.backends.git.repository import GitRepository
747 747 from distutils.version import StrictVersion
748 748
749 749 stdout, stderr = GitRepository._run_git_command('--version', _bare=True,
750 750 _safe=True)
751 751
752 752 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
753 753 if len(ver.split('.')) > 3:
754 754 #StrictVersion needs to be only 3 element type
755 755 ver = '.'.join(ver.split('.')[:3])
756 756 try:
757 757 _ver = StrictVersion(ver)
758 758 except Exception:
759 759 _ver = StrictVersion('0.0.0')
760 760 stderr = traceback.format_exc()
761 761
762 762 req_ver = '1.7.4'
763 763 to_old_git = False
764 764 if _ver < StrictVersion(req_ver):
765 765 to_old_git = True
766 766
767 767 if 'git' in BACKENDS:
768 768 log.debug('GIT version detected: %s' % stdout)
769 769 if stderr:
770 770 log.warning('Unable to detect git version, org error was: %r' % stderr)
771 771 elif to_old_git:
772 772 log.warning('RhodeCode detected git version %s, which is too old '
773 773 'for the system to function properly. Make sure '
774 774 'its version is at least %s' % (ver, req_ver))
775 775 return _ver
776 776
777 777
778 778 @decorator.decorator
779 779 def jsonify(func, *args, **kwargs):
780 780 """Action decorator that formats output for JSON
781 781
782 782 Given a function that will return content, this decorator will turn
783 783 the result into JSON, with a content-type of 'application/json' and
784 784 output it.
785 785
786 786 """
787 787 from pylons.decorators.util import get_pylons
788 788 from rhodecode.lib.compat import json
789 789 pylons = get_pylons(args)
790 790 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
791 791 data = func(*args, **kwargs)
792 792 if isinstance(data, (list, tuple)):
793 793 msg = "JSON responses with Array envelopes are susceptible to " \
794 794 "cross-site data leak attacks, see " \
795 795 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
796 796 warnings.warn(msg, Warning, 2)
797 797 log.warning(msg)
798 798 log.debug("Returning JSON wrapped action output")
799 799 return json.dumps(data, encoding='utf-8')
@@ -1,2176 +1,2130 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 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
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48
49 49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime, _set_extras
51 51 from rhodecode.lib.compat import json
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model.meta import Base, Session
55 55
56 56 URL_SEP = '/'
57 57 log = logging.getLogger(__name__)
58 58
59 59 #==============================================================================
60 60 # BASE CLASSES
61 61 #==============================================================================
62 62
63 63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64 64
65 65
66 66 class BaseModel(object):
67 67 """
68 68 Base Model for all classess
69 69 """
70 70
71 71 @classmethod
72 72 def _get_keys(cls):
73 73 """return column names for this model """
74 74 return class_mapper(cls).c.keys()
75 75
76 76 def get_dict(self):
77 77 """
78 78 return dict with keys and values corresponding
79 79 to this model data """
80 80
81 81 d = {}
82 82 for k in self._get_keys():
83 83 d[k] = getattr(self, k)
84 84
85 85 # also use __json__() if present to get additional fields
86 86 _json_attr = getattr(self, '__json__', None)
87 87 if _json_attr:
88 88 # update with attributes from __json__
89 89 if callable(_json_attr):
90 90 _json_attr = _json_attr()
91 91 for k, val in _json_attr.iteritems():
92 92 d[k] = val
93 93 return d
94 94
95 95 def get_appstruct(self):
96 96 """return list with keys and values tupples corresponding
97 97 to this model data """
98 98
99 99 l = []
100 100 for k in self._get_keys():
101 101 l.append((k, getattr(self, k),))
102 102 return l
103 103
104 104 def populate_obj(self, populate_dict):
105 105 """populate model with data from given populate_dict"""
106 106
107 107 for k in self._get_keys():
108 108 if k in populate_dict:
109 109 setattr(self, k, populate_dict[k])
110 110
111 111 @classmethod
112 112 def query(cls):
113 113 return Session().query(cls)
114 114
115 115 @classmethod
116 116 def get(cls, id_):
117 117 if id_:
118 118 return cls.query().get(id_)
119 119
120 120 @classmethod
121 121 def get_or_404(cls, id_):
122 122 try:
123 123 id_ = int(id_)
124 124 except (TypeError, ValueError):
125 125 raise HTTPNotFound
126 126
127 127 res = cls.query().get(id_)
128 128 if not res:
129 129 raise HTTPNotFound
130 130 return res
131 131
132 132 @classmethod
133 133 def getAll(cls):
134 134 # deprecated and left for backward compatibility
135 135 return cls.get_all()
136 136
137 137 @classmethod
138 138 def get_all(cls):
139 139 return cls.query().all()
140 140
141 141 @classmethod
142 142 def delete(cls, id_):
143 143 obj = cls.query().get(id_)
144 144 Session().delete(obj)
145 145
146 146 def __repr__(self):
147 147 if hasattr(self, '__unicode__'):
148 148 # python repr needs to return str
149 149 return safe_str(self.__unicode__())
150 150 return '<DB:%s>' % (self.__class__.__name__)
151 151
152 152
153 153 class RhodeCodeSetting(Base, BaseModel):
154 154 __tablename__ = 'rhodecode_settings'
155 155 __table_args__ = (
156 156 UniqueConstraint('app_settings_name'),
157 157 {'extend_existing': True, 'mysql_engine': 'InnoDB',
158 158 'mysql_charset': 'utf8'}
159 159 )
160 160 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
161 161 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
162 162 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
163 163
164 164 def __init__(self, k='', v=''):
165 165 self.app_settings_name = k
166 166 self.app_settings_value = v
167 167
168 168 @validates('_app_settings_value')
169 169 def validate_settings_value(self, key, val):
170 170 assert type(val) == unicode
171 171 return val
172 172
173 173 @hybrid_property
174 174 def app_settings_value(self):
175 175 v = self._app_settings_value
176 176 if self.app_settings_name in ["ldap_active",
177 177 "default_repo_enable_statistics",
178 178 "default_repo_enable_locking",
179 179 "default_repo_private",
180 180 "default_repo_enable_downloads"]:
181 181 v = str2bool(v)
182 182 return v
183 183
184 184 @app_settings_value.setter
185 185 def app_settings_value(self, val):
186 186 """
187 187 Setter that will always make sure we use unicode in app_settings_value
188 188
189 189 :param val:
190 190 """
191 191 self._app_settings_value = safe_unicode(val)
192 192
193 193 def __unicode__(self):
194 194 return u"<%s('%s:%s')>" % (
195 195 self.__class__.__name__,
196 196 self.app_settings_name, self.app_settings_value
197 197 )
198 198
199 199 @classmethod
200 200 def get_by_name(cls, key):
201 201 return cls.query()\
202 202 .filter(cls.app_settings_name == key).scalar()
203 203
204 204 @classmethod
205 205 def get_by_name_or_create(cls, key):
206 206 res = cls.get_by_name(key)
207 207 if not res:
208 208 res = cls(key)
209 209 return res
210 210
211 211 @classmethod
212 212 def get_app_settings(cls, cache=False):
213 213
214 214 ret = cls.query()
215 215
216 216 if cache:
217 217 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
218 218
219 219 if not ret:
220 220 raise Exception('Could not get application settings !')
221 221 settings = {}
222 222 for each in ret:
223 223 settings['rhodecode_' + each.app_settings_name] = \
224 224 each.app_settings_value
225 225
226 226 return settings
227 227
228 228 @classmethod
229 229 def get_ldap_settings(cls, cache=False):
230 230 ret = cls.query()\
231 231 .filter(cls.app_settings_name.startswith('ldap_')).all()
232 232 fd = {}
233 233 for row in ret:
234 234 fd.update({row.app_settings_name: row.app_settings_value})
235 235
236 236 return fd
237 237
238 238 @classmethod
239 239 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
240 240 ret = cls.query()\
241 241 .filter(cls.app_settings_name.startswith('default_')).all()
242 242 fd = {}
243 243 for row in ret:
244 244 key = row.app_settings_name
245 245 if strip_prefix:
246 246 key = remove_prefix(key, prefix='default_')
247 247 fd.update({key: row.app_settings_value})
248 248
249 249 return fd
250 250
251 251
252 252 class RhodeCodeUi(Base, BaseModel):
253 253 __tablename__ = 'rhodecode_ui'
254 254 __table_args__ = (
255 255 UniqueConstraint('ui_key'),
256 256 {'extend_existing': True, 'mysql_engine': 'InnoDB',
257 257 'mysql_charset': 'utf8'}
258 258 )
259 259
260 260 HOOK_UPDATE = 'changegroup.update'
261 261 HOOK_REPO_SIZE = 'changegroup.repo_size'
262 262 HOOK_PUSH = 'changegroup.push_logger'
263 263 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
264 264 HOOK_PULL = 'outgoing.pull_logger'
265 265 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
266 266
267 267 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
268 268 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 269 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
270 270 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
271 271 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
272 272
273 273 @classmethod
274 274 def get_by_key(cls, key):
275 275 return cls.query().filter(cls.ui_key == key).scalar()
276 276
277 277 @classmethod
278 278 def get_builtin_hooks(cls):
279 279 q = cls.query()
280 280 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
281 281 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
282 282 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
283 283 return q.all()
284 284
285 285 @classmethod
286 286 def get_custom_hooks(cls):
287 287 q = cls.query()
288 288 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
289 289 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
290 290 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
291 291 q = q.filter(cls.ui_section == 'hooks')
292 292 return q.all()
293 293
294 294 @classmethod
295 295 def get_repos_location(cls):
296 296 return cls.get_by_key('/').ui_value
297 297
298 298 @classmethod
299 299 def create_or_update_hook(cls, key, val):
300 300 new_ui = cls.get_by_key(key) or cls()
301 301 new_ui.ui_section = 'hooks'
302 302 new_ui.ui_active = True
303 303 new_ui.ui_key = key
304 304 new_ui.ui_value = val
305 305
306 306 Session().add(new_ui)
307 307
308 308 def __repr__(self):
309 309 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
310 310 self.ui_value)
311 311
312 312
313 313 class User(Base, BaseModel):
314 314 __tablename__ = 'users'
315 315 __table_args__ = (
316 316 UniqueConstraint('username'), UniqueConstraint('email'),
317 317 Index('u_username_idx', 'username'),
318 318 Index('u_email_idx', 'email'),
319 319 {'extend_existing': True, 'mysql_engine': 'InnoDB',
320 320 'mysql_charset': 'utf8'}
321 321 )
322 322 DEFAULT_USER = 'default'
323 323
324 324 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
325 325 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
326 326 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 327 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
328 328 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
329 329 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 330 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 331 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
333 333 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
334 334 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
335 335 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
336 336
337 337 user_log = relationship('UserLog')
338 338 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
339 339
340 340 repositories = relationship('Repository')
341 341 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
342 342 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
343 343
344 344 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
345 345 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
346 346
347 347 group_member = relationship('UserGroupMember', cascade='all')
348 348
349 349 notifications = relationship('UserNotification', cascade='all')
350 350 # notifications assigned to this user
351 351 user_created_notifications = relationship('Notification', cascade='all')
352 352 # comments created by this user
353 353 user_comments = relationship('ChangesetComment', cascade='all')
354 354 #extra emails for this user
355 355 user_emails = relationship('UserEmailMap', cascade='all')
356 356
357 357 @hybrid_property
358 358 def email(self):
359 359 return self._email
360 360
361 361 @email.setter
362 362 def email(self, val):
363 363 self._email = val.lower() if val else None
364 364
365 365 @property
366 366 def firstname(self):
367 367 # alias for future
368 368 return self.name
369 369
370 370 @property
371 371 def emails(self):
372 372 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
373 373 return [self.email] + [x.email for x in other]
374 374
375 375 @property
376 376 def ip_addresses(self):
377 377 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
378 378 return [x.ip_addr for x in ret]
379 379
380 380 @property
381 381 def username_and_name(self):
382 382 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
383 383
384 384 @property
385 385 def full_name(self):
386 386 return '%s %s' % (self.firstname, self.lastname)
387 387
388 388 @property
389 389 def full_name_or_username(self):
390 390 return ('%s %s' % (self.firstname, self.lastname)
391 391 if (self.firstname and self.lastname) else self.username)
392 392
393 393 @property
394 394 def full_contact(self):
395 395 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
396 396
397 397 @property
398 398 def short_contact(self):
399 399 return '%s %s' % (self.firstname, self.lastname)
400 400
401 401 @property
402 402 def is_admin(self):
403 403 return self.admin
404 404
405 405 @property
406 406 def AuthUser(self):
407 407 """
408 408 Returns instance of AuthUser for this user
409 409 """
410 410 from rhodecode.lib.auth import AuthUser
411 411 return AuthUser(user_id=self.user_id, api_key=self.api_key,
412 412 username=self.username)
413 413
414 414 def __unicode__(self):
415 415 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
416 416 self.user_id, self.username)
417 417
418 418 @classmethod
419 419 def get_by_username(cls, username, case_insensitive=False, cache=False):
420 420 if case_insensitive:
421 421 q = cls.query().filter(cls.username.ilike(username))
422 422 else:
423 423 q = cls.query().filter(cls.username == username)
424 424
425 425 if cache:
426 426 q = q.options(FromCache(
427 427 "sql_cache_short",
428 428 "get_user_%s" % _hash_key(username)
429 429 )
430 430 )
431 431 return q.scalar()
432 432
433 433 @classmethod
434 434 def get_by_api_key(cls, api_key, cache=False):
435 435 q = cls.query().filter(cls.api_key == api_key)
436 436
437 437 if cache:
438 438 q = q.options(FromCache("sql_cache_short",
439 439 "get_api_key_%s" % api_key))
440 440 return q.scalar()
441 441
442 442 @classmethod
443 443 def get_by_email(cls, email, case_insensitive=False, cache=False):
444 444 if case_insensitive:
445 445 q = cls.query().filter(cls.email.ilike(email))
446 446 else:
447 447 q = cls.query().filter(cls.email == email)
448 448
449 449 if cache:
450 450 q = q.options(FromCache("sql_cache_short",
451 451 "get_email_key_%s" % email))
452 452
453 453 ret = q.scalar()
454 454 if ret is None:
455 455 q = UserEmailMap.query()
456 456 # try fetching in alternate email map
457 457 if case_insensitive:
458 458 q = q.filter(UserEmailMap.email.ilike(email))
459 459 else:
460 460 q = q.filter(UserEmailMap.email == email)
461 461 q = q.options(joinedload(UserEmailMap.user))
462 462 if cache:
463 463 q = q.options(FromCache("sql_cache_short",
464 464 "get_email_map_key_%s" % email))
465 465 ret = getattr(q.scalar(), 'user', None)
466 466
467 467 return ret
468 468
469 469 @classmethod
470 470 def get_from_cs_author(cls, author):
471 471 """
472 472 Tries to get User objects out of commit author string
473 473
474 474 :param author:
475 475 """
476 476 from rhodecode.lib.helpers import email, author_name
477 477 # Valid email in the attribute passed, see if they're in the system
478 478 _email = email(author)
479 479 if _email:
480 480 user = cls.get_by_email(_email, case_insensitive=True)
481 481 if user:
482 482 return user
483 483 # Maybe we can match by username?
484 484 _author = author_name(author)
485 485 user = cls.get_by_username(_author, case_insensitive=True)
486 486 if user:
487 487 return user
488 488
489 489 def update_lastlogin(self):
490 490 """Update user lastlogin"""
491 491 self.last_login = datetime.datetime.now()
492 492 Session().add(self)
493 493 log.debug('updated user %s lastlogin' % self.username)
494 494
495 495 @classmethod
496 496 def get_first_admin(cls):
497 497 user = User.query().filter(User.admin == True).first()
498 498 if user is None:
499 499 raise Exception('Missing administrative account!')
500 500 return user
501 501
502 502 @classmethod
503 503 def get_default_user(cls, cache=False):
504 504 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
505 505 if user is None:
506 506 raise Exception('Missing default account!')
507 507 return user
508 508
509 509 def get_api_data(self):
510 510 """
511 511 Common function for generating user related data for API
512 512 """
513 513 user = self
514 514 data = dict(
515 515 user_id=user.user_id,
516 516 username=user.username,
517 517 firstname=user.name,
518 518 lastname=user.lastname,
519 519 email=user.email,
520 520 emails=user.emails,
521 521 api_key=user.api_key,
522 522 active=user.active,
523 523 admin=user.admin,
524 524 ldap_dn=user.ldap_dn,
525 525 last_login=user.last_login,
526 526 ip_addresses=user.ip_addresses
527 527 )
528 528 return data
529 529
530 530 def __json__(self):
531 531 data = dict(
532 532 full_name=self.full_name,
533 533 full_name_or_username=self.full_name_or_username,
534 534 short_contact=self.short_contact,
535 535 full_contact=self.full_contact
536 536 )
537 537 data.update(self.get_api_data())
538 538 return data
539 539
540 540
541 541 class UserEmailMap(Base, BaseModel):
542 542 __tablename__ = 'user_email_map'
543 543 __table_args__ = (
544 544 Index('uem_email_idx', 'email'),
545 545 UniqueConstraint('email'),
546 546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
547 547 'mysql_charset': 'utf8'}
548 548 )
549 549 __mapper_args__ = {}
550 550
551 551 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
552 552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
553 553 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
554 554 user = relationship('User', lazy='joined')
555 555
556 556 @validates('_email')
557 557 def validate_email(self, key, email):
558 558 # check if this email is not main one
559 559 main_email = Session().query(User).filter(User.email == email).scalar()
560 560 if main_email is not None:
561 561 raise AttributeError('email %s is present is user table' % email)
562 562 return email
563 563
564 564 @hybrid_property
565 565 def email(self):
566 566 return self._email
567 567
568 568 @email.setter
569 569 def email(self, val):
570 570 self._email = val.lower() if val else None
571 571
572 572
573 573 class UserIpMap(Base, BaseModel):
574 574 __tablename__ = 'user_ip_map'
575 575 __table_args__ = (
576 576 UniqueConstraint('user_id', 'ip_addr'),
577 577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
578 578 'mysql_charset': 'utf8'}
579 579 )
580 580 __mapper_args__ = {}
581 581
582 582 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
583 583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
584 584 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
585 585 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
586 586 user = relationship('User', lazy='joined')
587 587
588 588 @classmethod
589 589 def _get_ip_range(cls, ip_addr):
590 590 from rhodecode.lib import ipaddr
591 591 net = ipaddr.IPNetwork(address=ip_addr)
592 592 return [str(net.network), str(net.broadcast)]
593 593
594 594 def __json__(self):
595 595 return dict(
596 596 ip_addr=self.ip_addr,
597 597 ip_range=self._get_ip_range(self.ip_addr)
598 598 )
599 599
600 600
601 601 class UserLog(Base, BaseModel):
602 602 __tablename__ = 'user_logs'
603 603 __table_args__ = (
604 604 {'extend_existing': True, 'mysql_engine': 'InnoDB',
605 605 'mysql_charset': 'utf8'},
606 606 )
607 607 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
608 608 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
609 609 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
610 610 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
611 611 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
612 612 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
613 613 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
614 614 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
615 615
616 616 @property
617 617 def action_as_day(self):
618 618 return datetime.date(*self.action_date.timetuple()[:3])
619 619
620 620 user = relationship('User')
621 621 repository = relationship('Repository', cascade='')
622 622
623 623
624 624 class UserGroup(Base, BaseModel):
625 625 __tablename__ = 'users_groups'
626 626 __table_args__ = (
627 627 {'extend_existing': True, 'mysql_engine': 'InnoDB',
628 628 'mysql_charset': 'utf8'},
629 629 )
630 630
631 631 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
632 632 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
633 633 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
634 634 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
635 635 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
636 636
637 637 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
638 638 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
639 639 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
640 640 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
641 641 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
642 642 user = relationship('User')
643 643
644 644 def __unicode__(self):
645 645 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
646 646 self.users_group_id,
647 647 self.users_group_name)
648 648
649 649 @classmethod
650 650 def get_by_group_name(cls, group_name, cache=False,
651 651 case_insensitive=False):
652 652 if case_insensitive:
653 653 q = cls.query().filter(cls.users_group_name.ilike(group_name))
654 654 else:
655 655 q = cls.query().filter(cls.users_group_name == group_name)
656 656 if cache:
657 657 q = q.options(FromCache(
658 658 "sql_cache_short",
659 659 "get_user_%s" % _hash_key(group_name)
660 660 )
661 661 )
662 662 return q.scalar()
663 663
664 664 @classmethod
665 665 def get(cls, users_group_id, cache=False):
666 666 users_group = cls.query()
667 667 if cache:
668 668 users_group = users_group.options(FromCache("sql_cache_short",
669 669 "get_users_group_%s" % users_group_id))
670 670 return users_group.get(users_group_id)
671 671
672 672 def get_api_data(self):
673 673 users_group = self
674 674
675 675 data = dict(
676 676 users_group_id=users_group.users_group_id,
677 677 group_name=users_group.users_group_name,
678 678 active=users_group.users_group_active,
679 679 )
680 680
681 681 return data
682 682
683 683
684 684 class UserGroupMember(Base, BaseModel):
685 685 __tablename__ = 'users_groups_members'
686 686 __table_args__ = (
687 687 {'extend_existing': True, 'mysql_engine': 'InnoDB',
688 688 'mysql_charset': 'utf8'},
689 689 )
690 690
691 691 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
692 692 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
693 693 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
694 694
695 695 user = relationship('User', lazy='joined')
696 696 users_group = relationship('UserGroup')
697 697
698 698 def __init__(self, gr_id='', u_id=''):
699 699 self.users_group_id = gr_id
700 700 self.user_id = u_id
701 701
702 702
703 703 class RepositoryField(Base, BaseModel):
704 704 __tablename__ = 'repositories_fields'
705 705 __table_args__ = (
706 706 UniqueConstraint('repository_id', 'field_key'), # no-multi field
707 707 {'extend_existing': True, 'mysql_engine': 'InnoDB',
708 708 'mysql_charset': 'utf8'},
709 709 )
710 710 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
711 711
712 712 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
713 713 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
714 714 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
715 715 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
716 716 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
717 717 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
718 718 field_type = Column("field_type", String(256), nullable=False, unique=None)
719 719 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
720 720
721 721 repository = relationship('Repository')
722 722
723 723 @property
724 724 def field_key_prefixed(self):
725 725 return 'ex_%s' % self.field_key
726 726
727 727 @classmethod
728 728 def un_prefix_key(cls, key):
729 729 if key.startswith(cls.PREFIX):
730 730 return key[len(cls.PREFIX):]
731 731 return key
732 732
733 733 @classmethod
734 734 def get_by_key_name(cls, key, repo):
735 735 row = cls.query()\
736 736 .filter(cls.repository == repo)\
737 737 .filter(cls.field_key == key).scalar()
738 738 return row
739 739
740 740
741 741 class Repository(Base, BaseModel):
742 742 __tablename__ = 'repositories'
743 743 __table_args__ = (
744 744 UniqueConstraint('repo_name'),
745 745 Index('r_repo_name_idx', 'repo_name'),
746 746 {'extend_existing': True, 'mysql_engine': 'InnoDB',
747 747 'mysql_charset': 'utf8'},
748 748 )
749 749
750 750 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
751 751 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
752 752 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
753 753 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
754 754 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
755 755 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
756 756 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
757 757 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
758 758 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
759 759 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
760 760 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
761 761 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
762 762 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
763 763 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
764 764 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
765 765
766 766 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
767 767 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
768 768
769 769 user = relationship('User')
770 770 fork = relationship('Repository', remote_side=repo_id)
771 771 group = relationship('RepoGroup')
772 772 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
773 773 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
774 774 stats = relationship('Statistics', cascade='all', uselist=False)
775 775
776 776 followers = relationship('UserFollowing',
777 777 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
778 778 cascade='all')
779 779 extra_fields = relationship('RepositoryField',
780 780 cascade="all, delete, delete-orphan")
781 781
782 782 logs = relationship('UserLog')
783 783 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
784 784
785 785 pull_requests_org = relationship('PullRequest',
786 786 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
787 787 cascade="all, delete, delete-orphan")
788 788
789 789 pull_requests_other = relationship('PullRequest',
790 790 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
791 791 cascade="all, delete, delete-orphan")
792 792
793 793 def __unicode__(self):
794 794 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
795 795 self.repo_name)
796 796
797 797 @hybrid_property
798 798 def locked(self):
799 799 # always should return [user_id, timelocked]
800 800 if self._locked:
801 801 _lock_info = self._locked.split(':')
802 802 return int(_lock_info[0]), _lock_info[1]
803 803 return [None, None]
804 804
805 805 @locked.setter
806 806 def locked(self, val):
807 807 if val and isinstance(val, (list, tuple)):
808 808 self._locked = ':'.join(map(str, val))
809 809 else:
810 810 self._locked = None
811 811
812 812 @hybrid_property
813 813 def changeset_cache(self):
814 814 from rhodecode.lib.vcs.backends.base import EmptyChangeset
815 815 dummy = EmptyChangeset().__json__()
816 816 if not self._changeset_cache:
817 817 return dummy
818 818 try:
819 819 return json.loads(self._changeset_cache)
820 820 except TypeError:
821 821 return dummy
822 822
823 823 @changeset_cache.setter
824 824 def changeset_cache(self, val):
825 825 try:
826 826 self._changeset_cache = json.dumps(val)
827 827 except Exception:
828 828 log.error(traceback.format_exc())
829 829
830 830 @classmethod
831 831 def url_sep(cls):
832 832 return URL_SEP
833 833
834 834 @classmethod
835 835 def normalize_repo_name(cls, repo_name):
836 836 """
837 837 Normalizes os specific repo_name to the format internally stored inside
838 838 dabatabase using URL_SEP
839 839
840 840 :param cls:
841 841 :param repo_name:
842 842 """
843 843 return cls.url_sep().join(repo_name.split(os.sep))
844 844
845 845 @classmethod
846 846 def get_by_repo_name(cls, repo_name):
847 847 q = Session().query(cls).filter(cls.repo_name == repo_name)
848 848 q = q.options(joinedload(Repository.fork))\
849 849 .options(joinedload(Repository.user))\
850 850 .options(joinedload(Repository.group))
851 851 return q.scalar()
852 852
853 853 @classmethod
854 854 def get_by_full_path(cls, repo_full_path):
855 855 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
856 856 repo_name = cls.normalize_repo_name(repo_name)
857 857 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
858 858
859 859 @classmethod
860 860 def get_repo_forks(cls, repo_id):
861 861 return cls.query().filter(Repository.fork_id == repo_id)
862 862
863 863 @classmethod
864 864 def base_path(cls):
865 865 """
866 866 Returns base path when all repos are stored
867 867
868 868 :param cls:
869 869 """
870 870 q = Session().query(RhodeCodeUi)\
871 871 .filter(RhodeCodeUi.ui_key == cls.url_sep())
872 872 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
873 873 return q.one().ui_value
874 874
875 875 @property
876 876 def forks(self):
877 877 """
878 878 Return forks of this repo
879 879 """
880 880 return Repository.get_repo_forks(self.repo_id)
881 881
882 882 @property
883 883 def parent(self):
884 884 """
885 885 Returns fork parent
886 886 """
887 887 return self.fork
888 888
889 889 @property
890 890 def just_name(self):
891 891 return self.repo_name.split(Repository.url_sep())[-1]
892 892
893 893 @property
894 894 def groups_with_parents(self):
895 895 groups = []
896 896 if self.group is None:
897 897 return groups
898 898
899 899 cur_gr = self.group
900 900 groups.insert(0, cur_gr)
901 901 while 1:
902 902 gr = getattr(cur_gr, 'parent_group', None)
903 903 cur_gr = cur_gr.parent_group
904 904 if gr is None:
905 905 break
906 906 groups.insert(0, gr)
907 907
908 908 return groups
909 909
910 910 @property
911 911 def groups_and_repo(self):
912 912 return self.groups_with_parents, self.just_name, self.repo_name
913 913
914 914 @LazyProperty
915 915 def repo_path(self):
916 916 """
917 917 Returns base full path for that repository means where it actually
918 918 exists on a filesystem
919 919 """
920 920 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
921 921 Repository.url_sep())
922 922 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
923 923 return q.one().ui_value
924 924
925 925 @property
926 926 def repo_full_path(self):
927 927 p = [self.repo_path]
928 928 # we need to split the name by / since this is how we store the
929 929 # names in the database, but that eventually needs to be converted
930 930 # into a valid system path
931 931 p += self.repo_name.split(Repository.url_sep())
932 932 return os.path.join(*map(safe_unicode, p))
933 933
934 934 @property
935 935 def cache_keys(self):
936 936 """
937 937 Returns associated cache keys for that repo
938 938 """
939 939 return CacheInvalidation.query()\
940 940 .filter(CacheInvalidation.cache_args == self.repo_name)\
941 941 .order_by(CacheInvalidation.cache_key)\
942 942 .all()
943 943
944 944 def get_new_name(self, repo_name):
945 945 """
946 946 returns new full repository name based on assigned group and new new
947 947
948 948 :param group_name:
949 949 """
950 950 path_prefix = self.group.full_path_splitted if self.group else []
951 951 return Repository.url_sep().join(path_prefix + [repo_name])
952 952
953 953 @property
954 954 def _ui(self):
955 955 """
956 956 Creates an db based ui object for this repository
957 957 """
958 958 from rhodecode.lib.utils import make_ui
959 959 return make_ui('db', clear_session=False)
960 960
961 961 @classmethod
962 962 def is_valid(cls, repo_name):
963 963 """
964 964 returns True if given repo name is a valid filesystem repository
965 965
966 966 :param cls:
967 967 :param repo_name:
968 968 """
969 969 from rhodecode.lib.utils import is_valid_repo
970 970
971 971 return is_valid_repo(repo_name, cls.base_path())
972 972
973 973 def get_api_data(self):
974 974 """
975 975 Common function for generating repo api data
976 976
977 977 """
978 978 repo = self
979 979 data = dict(
980 980 repo_id=repo.repo_id,
981 981 repo_name=repo.repo_name,
982 982 repo_type=repo.repo_type,
983 983 clone_uri=repo.clone_uri,
984 984 private=repo.private,
985 985 created_on=repo.created_on,
986 986 description=repo.description,
987 987 landing_rev=repo.landing_rev,
988 988 owner=repo.user.username,
989 989 fork_of=repo.fork.repo_name if repo.fork else None,
990 990 enable_statistics=repo.enable_statistics,
991 991 enable_locking=repo.enable_locking,
992 992 enable_downloads=repo.enable_downloads,
993 993 last_changeset=repo.changeset_cache,
994 994 locked_by=User.get(self.locked[0]).get_api_data() \
995 995 if self.locked[0] else None,
996 996 locked_date=time_to_datetime(self.locked[1]) \
997 997 if self.locked[1] else None
998 998 )
999 999 rc_config = RhodeCodeSetting.get_app_settings()
1000 1000 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
1001 1001 if repository_fields:
1002 1002 for f in self.extra_fields:
1003 1003 data[f.field_key_prefixed] = f.field_value
1004 1004
1005 1005 return data
1006 1006
1007 1007 @classmethod
1008 1008 def lock(cls, repo, user_id):
1009 1009 repo.locked = [user_id, time.time()]
1010 1010 Session().add(repo)
1011 1011 Session().commit()
1012 1012
1013 1013 @classmethod
1014 1014 def unlock(cls, repo):
1015 1015 repo.locked = None
1016 1016 Session().add(repo)
1017 1017 Session().commit()
1018 1018
1019 1019 @classmethod
1020 1020 def getlock(cls, repo):
1021 1021 return repo.locked
1022 1022
1023 1023 @property
1024 1024 def last_db_change(self):
1025 1025 return self.updated_on
1026 1026
1027 1027 def clone_url(self, **override):
1028 1028 from pylons import url
1029 1029 from urlparse import urlparse
1030 1030 import urllib
1031 1031 parsed_url = urlparse(url('home', qualified=True))
1032 1032 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1033 1033 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1034 1034 args = {
1035 1035 'user': '',
1036 1036 'pass': '',
1037 1037 'scheme': parsed_url.scheme,
1038 1038 'netloc': parsed_url.netloc,
1039 1039 'prefix': decoded_path,
1040 1040 'path': self.repo_name
1041 1041 }
1042 1042
1043 1043 args.update(override)
1044 1044 return default_clone_uri % args
1045 1045
1046 1046 #==========================================================================
1047 1047 # SCM PROPERTIES
1048 1048 #==========================================================================
1049 1049
1050 1050 def get_changeset(self, rev=None):
1051 1051 return get_changeset_safe(self.scm_instance, rev)
1052 1052
1053 1053 def get_landing_changeset(self):
1054 1054 """
1055 1055 Returns landing changeset, or if that doesn't exist returns the tip
1056 1056 """
1057 1057 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1058 1058 return cs
1059 1059
1060 1060 def update_changeset_cache(self, cs_cache=None):
1061 1061 """
1062 1062 Update cache of last changeset for repository, keys should be::
1063 1063
1064 1064 short_id
1065 1065 raw_id
1066 1066 revision
1067 1067 message
1068 1068 date
1069 1069 author
1070 1070
1071 1071 :param cs_cache:
1072 1072 """
1073 1073 from rhodecode.lib.vcs.backends.base import BaseChangeset
1074 1074 if cs_cache is None:
1075 1075 cs_cache = EmptyChangeset()
1076 1076 # use no-cache version here
1077 1077 scm_repo = self.scm_instance_no_cache()
1078 1078 if scm_repo:
1079 1079 cs_cache = scm_repo.get_changeset()
1080 1080
1081 1081 if isinstance(cs_cache, BaseChangeset):
1082 1082 cs_cache = cs_cache.__json__()
1083 1083
1084 1084 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1085 1085 _default = datetime.datetime.fromtimestamp(0)
1086 1086 last_change = cs_cache.get('date') or _default
1087 1087 log.debug('updated repo %s with new cs cache %s'
1088 1088 % (self.repo_name, cs_cache))
1089 1089 self.updated_on = last_change
1090 1090 self.changeset_cache = cs_cache
1091 1091 Session().add(self)
1092 1092 Session().commit()
1093 1093 else:
1094 1094 log.debug('Skipping repo:%s already with latest changes'
1095 1095 % self.repo_name)
1096 1096
1097 1097 @property
1098 1098 def tip(self):
1099 1099 return self.get_changeset('tip')
1100 1100
1101 1101 @property
1102 1102 def author(self):
1103 1103 return self.tip.author
1104 1104
1105 1105 @property
1106 1106 def last_change(self):
1107 1107 return self.scm_instance.last_change
1108 1108
1109 1109 def get_comments(self, revisions=None):
1110 1110 """
1111 1111 Returns comments for this repository grouped by revisions
1112 1112
1113 1113 :param revisions: filter query by revisions only
1114 1114 """
1115 1115 cmts = ChangesetComment.query()\
1116 1116 .filter(ChangesetComment.repo == self)
1117 1117 if revisions:
1118 1118 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1119 1119 grouped = defaultdict(list)
1120 1120 for cmt in cmts.all():
1121 1121 grouped[cmt.revision].append(cmt)
1122 1122 return grouped
1123 1123
1124 1124 def statuses(self, revisions=None):
1125 1125 """
1126 1126 Returns statuses for this repository
1127 1127
1128 1128 :param revisions: list of revisions to get statuses for
1129 1129 :type revisions: list
1130 1130 """
1131 1131
1132 1132 statuses = ChangesetStatus.query()\
1133 1133 .filter(ChangesetStatus.repo == self)\
1134 1134 .filter(ChangesetStatus.version == 0)
1135 1135 if revisions:
1136 1136 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1137 1137 grouped = {}
1138 1138
1139 1139 #maybe we have open new pullrequest without a status ?
1140 1140 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1141 1141 status_lbl = ChangesetStatus.get_status_lbl(stat)
1142 1142 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1143 1143 for rev in pr.revisions:
1144 1144 pr_id = pr.pull_request_id
1145 1145 pr_repo = pr.other_repo.repo_name
1146 1146 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1147 1147
1148 1148 for stat in statuses.all():
1149 1149 pr_id = pr_repo = None
1150 1150 if stat.pull_request:
1151 1151 pr_id = stat.pull_request.pull_request_id
1152 1152 pr_repo = stat.pull_request.other_repo.repo_name
1153 1153 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1154 1154 pr_id, pr_repo]
1155 1155 return grouped
1156 1156
1157 1157 def _repo_size(self):
1158 1158 from rhodecode.lib import helpers as h
1159 1159 log.debug('calculating repository size...')
1160 1160 return h.format_byte_size(self.scm_instance.size)
1161 1161
1162 1162 #==========================================================================
1163 1163 # SCM CACHE INSTANCE
1164 1164 #==========================================================================
1165 1165
1166 @property
1167 def invalidate(self):
1168 return CacheInvalidation.invalidate(self.repo_name)
1169
1170 1166 def set_invalidate(self):
1171 1167 """
1172 1168 Mark caches of this repo as invalid.
1173 1169 """
1174 1170 CacheInvalidation.set_invalidate(self.repo_name)
1175 1171
1176 1172 def scm_instance_no_cache(self):
1177 1173 return self.__get_instance()
1178 1174
1179 1175 @property
1180 1176 def scm_instance(self):
1181 1177 import rhodecode
1182 1178 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1183 1179 if full_cache:
1184 1180 return self.scm_instance_cached()
1185 1181 return self.__get_instance()
1186 1182
1187 def scm_instance_cached(self, cache_map=None):
1183 def scm_instance_cached(self, valid_cache_keys=None):
1188 1184 @cache_region('long_term')
1189 1185 def _c(repo_name):
1190 1186 return self.__get_instance()
1191 1187 rn = self.repo_name
1192 1188
1193 if cache_map:
1194 # get using prefilled cache_map
1195 invalidate_repo = cache_map[self.repo_name]
1196 if invalidate_repo:
1197 invalidate_repo = (None if invalidate_repo.cache_active
1198 else invalidate_repo)
1199 else:
1200 # get from invalidate
1201 invalidate_repo = self.invalidate
1202
1203 if invalidate_repo is not None:
1189 valid = CacheInvalidation.test_and_set_valid(rn, valid_cache_keys=valid_cache_keys)
1190 if not valid:
1191 log.debug('Cache for %s invalidated, getting new object' % (rn))
1204 1192 region_invalidate(_c, None, rn)
1205 log.debug('Cache for %s invalidated, getting new object' % (rn))
1206 # update our cache
1207 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1208 1193 else:
1209 1194 log.debug('Getting obj for %s from cache' % (rn))
1210 1195 return _c(rn)
1211 1196
1212 1197 def __get_instance(self):
1213 1198 repo_full_path = self.repo_full_path
1214 1199 try:
1215 1200 alias = get_scm(repo_full_path)[0]
1216 1201 log.debug('Creating instance of %s repository from %s'
1217 1202 % (alias, repo_full_path))
1218 1203 backend = get_backend(alias)
1219 1204 except VCSError:
1220 1205 log.error(traceback.format_exc())
1221 1206 log.error('Perhaps this repository is in db and not in '
1222 1207 'filesystem run rescan repositories with '
1223 1208 '"destroy old data " option from admin panel')
1224 1209 return
1225 1210
1226 1211 if alias == 'hg':
1227 1212
1228 1213 repo = backend(safe_str(repo_full_path), create=False,
1229 1214 baseui=self._ui)
1230 1215 # skip hidden web repository
1231 1216 if repo._get_hidden():
1232 1217 return
1233 1218 else:
1234 1219 repo = backend(repo_full_path, create=False)
1235 1220
1236 1221 return repo
1237 1222
1238 1223
1239 1224 class RepoGroup(Base, BaseModel):
1240 1225 __tablename__ = 'groups'
1241 1226 __table_args__ = (
1242 1227 UniqueConstraint('group_name', 'group_parent_id'),
1243 1228 CheckConstraint('group_id != group_parent_id'),
1244 1229 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1245 1230 'mysql_charset': 'utf8'},
1246 1231 )
1247 1232 __mapper_args__ = {'order_by': 'group_name'}
1248 1233
1249 1234 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1250 1235 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1251 1236 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1252 1237 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1253 1238 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1254 1239 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1255 1240
1256 1241 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1257 1242 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1258 1243 parent_group = relationship('RepoGroup', remote_side=group_id)
1259 1244 user = relationship('User')
1260 1245
1261 1246 def __init__(self, group_name='', parent_group=None):
1262 1247 self.group_name = group_name
1263 1248 self.parent_group = parent_group
1264 1249
1265 1250 def __unicode__(self):
1266 1251 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1267 1252 self.group_name)
1268 1253
1269 1254 @classmethod
1270 1255 def groups_choices(cls, groups=None, show_empty_group=True):
1271 1256 from webhelpers.html import literal as _literal
1272 1257 if not groups:
1273 1258 groups = cls.query().all()
1274 1259
1275 1260 repo_groups = []
1276 1261 if show_empty_group:
1277 1262 repo_groups = [('-1', '-- %s --' % _('top level'))]
1278 1263 sep = ' &raquo; '
1279 1264 _name = lambda k: _literal(sep.join(k))
1280 1265
1281 1266 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1282 1267 for x in groups])
1283 1268
1284 1269 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1285 1270 return repo_groups
1286 1271
1287 1272 @classmethod
1288 1273 def url_sep(cls):
1289 1274 return URL_SEP
1290 1275
1291 1276 @classmethod
1292 1277 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1293 1278 if case_insensitive:
1294 1279 gr = cls.query()\
1295 1280 .filter(cls.group_name.ilike(group_name))
1296 1281 else:
1297 1282 gr = cls.query()\
1298 1283 .filter(cls.group_name == group_name)
1299 1284 if cache:
1300 1285 gr = gr.options(FromCache(
1301 1286 "sql_cache_short",
1302 1287 "get_group_%s" % _hash_key(group_name)
1303 1288 )
1304 1289 )
1305 1290 return gr.scalar()
1306 1291
1307 1292 @property
1308 1293 def parents(self):
1309 1294 parents_recursion_limit = 5
1310 1295 groups = []
1311 1296 if self.parent_group is None:
1312 1297 return groups
1313 1298 cur_gr = self.parent_group
1314 1299 groups.insert(0, cur_gr)
1315 1300 cnt = 0
1316 1301 while 1:
1317 1302 cnt += 1
1318 1303 gr = getattr(cur_gr, 'parent_group', None)
1319 1304 cur_gr = cur_gr.parent_group
1320 1305 if gr is None:
1321 1306 break
1322 1307 if cnt == parents_recursion_limit:
1323 1308 # this will prevent accidental infinit loops
1324 1309 log.error('group nested more than %s' %
1325 1310 parents_recursion_limit)
1326 1311 break
1327 1312
1328 1313 groups.insert(0, gr)
1329 1314 return groups
1330 1315
1331 1316 @property
1332 1317 def children(self):
1333 1318 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1334 1319
1335 1320 @property
1336 1321 def name(self):
1337 1322 return self.group_name.split(RepoGroup.url_sep())[-1]
1338 1323
1339 1324 @property
1340 1325 def full_path(self):
1341 1326 return self.group_name
1342 1327
1343 1328 @property
1344 1329 def full_path_splitted(self):
1345 1330 return self.group_name.split(RepoGroup.url_sep())
1346 1331
1347 1332 @property
1348 1333 def repositories(self):
1349 1334 return Repository.query()\
1350 1335 .filter(Repository.group == self)\
1351 1336 .order_by(Repository.repo_name)
1352 1337
1353 1338 @property
1354 1339 def repositories_recursive_count(self):
1355 1340 cnt = self.repositories.count()
1356 1341
1357 1342 def children_count(group):
1358 1343 cnt = 0
1359 1344 for child in group.children:
1360 1345 cnt += child.repositories.count()
1361 1346 cnt += children_count(child)
1362 1347 return cnt
1363 1348
1364 1349 return cnt + children_count(self)
1365 1350
1366 1351 def _recursive_objects(self, include_repos=True):
1367 1352 all_ = []
1368 1353
1369 1354 def _get_members(root_gr):
1370 1355 if include_repos:
1371 1356 for r in root_gr.repositories:
1372 1357 all_.append(r)
1373 1358 childs = root_gr.children.all()
1374 1359 if childs:
1375 1360 for gr in childs:
1376 1361 all_.append(gr)
1377 1362 _get_members(gr)
1378 1363
1379 1364 _get_members(self)
1380 1365 return [self] + all_
1381 1366
1382 1367 def recursive_groups_and_repos(self):
1383 1368 """
1384 1369 Recursive return all groups, with repositories in those groups
1385 1370 """
1386 1371 return self._recursive_objects()
1387 1372
1388 1373 def recursive_groups(self):
1389 1374 """
1390 1375 Returns all children groups for this group including children of children
1391 1376 """
1392 1377 return self._recursive_objects(include_repos=False)
1393 1378
1394 1379 def get_new_name(self, group_name):
1395 1380 """
1396 1381 returns new full group name based on parent and new name
1397 1382
1398 1383 :param group_name:
1399 1384 """
1400 1385 path_prefix = (self.parent_group.full_path_splitted if
1401 1386 self.parent_group else [])
1402 1387 return RepoGroup.url_sep().join(path_prefix + [group_name])
1403 1388
1404 1389
1405 1390 class Permission(Base, BaseModel):
1406 1391 __tablename__ = 'permissions'
1407 1392 __table_args__ = (
1408 1393 Index('p_perm_name_idx', 'permission_name'),
1409 1394 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1410 1395 'mysql_charset': 'utf8'},
1411 1396 )
1412 1397 PERMS = [
1413 1398 ('hg.admin', _('RhodeCode Administrator')),
1414 1399
1415 1400 ('repository.none', _('Repository no access')),
1416 1401 ('repository.read', _('Repository read access')),
1417 1402 ('repository.write', _('Repository write access')),
1418 1403 ('repository.admin', _('Repository admin access')),
1419 1404
1420 1405 ('group.none', _('Repository group no access')),
1421 1406 ('group.read', _('Repository group read access')),
1422 1407 ('group.write', _('Repository group write access')),
1423 1408 ('group.admin', _('Repository group admin access')),
1424 1409
1425 1410 ('usergroup.none', _('User group no access')),
1426 1411 ('usergroup.read', _('User group read access')),
1427 1412 ('usergroup.write', _('User group write access')),
1428 1413 ('usergroup.admin', _('User group admin access')),
1429 1414
1430 1415 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
1431 1416 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
1432 1417
1433 1418 ('hg.usergroup.create.false', _('User Group creation disabled')),
1434 1419 ('hg.usergroup.create.true', _('User Group creation enabled')),
1435 1420
1436 1421 ('hg.create.none', _('Repository creation disabled')),
1437 1422 ('hg.create.repository', _('Repository creation enabled')),
1438 1423
1439 1424 ('hg.fork.none', _('Repository forking disabled')),
1440 1425 ('hg.fork.repository', _('Repository forking enabled')),
1441 1426
1442 1427 ('hg.register.none', _('Register disabled')),
1443 1428 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1444 1429 'with manual activation')),
1445 1430
1446 1431 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1447 1432 'with auto activation')),
1448 1433 ]
1449 1434
1450 1435 #definition of system default permissions for DEFAULT user
1451 1436 DEFAULT_USER_PERMISSIONS = [
1452 1437 'repository.read',
1453 1438 'group.read',
1454 1439 'usergroup.read',
1455 1440 'hg.create.repository',
1456 1441 'hg.fork.repository',
1457 1442 'hg.register.manual_activate',
1458 1443 ]
1459 1444
1460 1445 # defines which permissions are more important higher the more important
1461 1446 # Weight defines which permissions are more important.
1462 1447 # The higher number the more important.
1463 1448 PERM_WEIGHTS = {
1464 1449 'repository.none': 0,
1465 1450 'repository.read': 1,
1466 1451 'repository.write': 3,
1467 1452 'repository.admin': 4,
1468 1453
1469 1454 'group.none': 0,
1470 1455 'group.read': 1,
1471 1456 'group.write': 3,
1472 1457 'group.admin': 4,
1473 1458
1474 1459 'usergroup.none': 0,
1475 1460 'usergroup.read': 1,
1476 1461 'usergroup.write': 3,
1477 1462 'usergroup.admin': 4,
1478 1463 'hg.repogroup.create.false': 0,
1479 1464 'hg.repogroup.create.true': 1,
1480 1465
1481 1466 'hg.usergroup.create.false': 0,
1482 1467 'hg.usergroup.create.true': 1,
1483 1468
1484 1469 'hg.fork.none': 0,
1485 1470 'hg.fork.repository': 1,
1486 1471 'hg.create.none': 0,
1487 1472 'hg.create.repository': 1
1488 1473 }
1489 1474
1490 1475 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1491 1476 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1492 1477 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1493 1478
1494 1479 def __unicode__(self):
1495 1480 return u"<%s('%s:%s')>" % (
1496 1481 self.__class__.__name__, self.permission_id, self.permission_name
1497 1482 )
1498 1483
1499 1484 @classmethod
1500 1485 def get_by_key(cls, key):
1501 1486 return cls.query().filter(cls.permission_name == key).scalar()
1502 1487
1503 1488 @classmethod
1504 1489 def get_default_perms(cls, default_user_id):
1505 1490 q = Session().query(UserRepoToPerm, Repository, cls)\
1506 1491 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1507 1492 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1508 1493 .filter(UserRepoToPerm.user_id == default_user_id)
1509 1494
1510 1495 return q.all()
1511 1496
1512 1497 @classmethod
1513 1498 def get_default_group_perms(cls, default_user_id):
1514 1499 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1515 1500 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1516 1501 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1517 1502 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1518 1503
1519 1504 return q.all()
1520 1505
1521 1506 @classmethod
1522 1507 def get_default_user_group_perms(cls, default_user_id):
1523 1508 q = Session().query(UserUserGroupToPerm, UserGroup, cls)\
1524 1509 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
1525 1510 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id))\
1526 1511 .filter(UserUserGroupToPerm.user_id == default_user_id)
1527 1512
1528 1513 return q.all()
1529 1514
1530 1515
1531 1516 class UserRepoToPerm(Base, BaseModel):
1532 1517 __tablename__ = 'repo_to_perm'
1533 1518 __table_args__ = (
1534 1519 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1535 1520 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1536 1521 'mysql_charset': 'utf8'}
1537 1522 )
1538 1523 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1539 1524 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1540 1525 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1541 1526 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1542 1527
1543 1528 user = relationship('User')
1544 1529 repository = relationship('Repository')
1545 1530 permission = relationship('Permission')
1546 1531
1547 1532 @classmethod
1548 1533 def create(cls, user, repository, permission):
1549 1534 n = cls()
1550 1535 n.user = user
1551 1536 n.repository = repository
1552 1537 n.permission = permission
1553 1538 Session().add(n)
1554 1539 return n
1555 1540
1556 1541 def __unicode__(self):
1557 1542 return u'<%s => %s >' % (self.user, self.repository)
1558 1543
1559 1544
1560 1545 class UserUserGroupToPerm(Base, BaseModel):
1561 1546 __tablename__ = 'user_user_group_to_perm'
1562 1547 __table_args__ = (
1563 1548 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1564 1549 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1565 1550 'mysql_charset': 'utf8'}
1566 1551 )
1567 1552 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1568 1553 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1569 1554 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1570 1555 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1571 1556
1572 1557 user = relationship('User')
1573 1558 user_group = relationship('UserGroup')
1574 1559 permission = relationship('Permission')
1575 1560
1576 1561 @classmethod
1577 1562 def create(cls, user, user_group, permission):
1578 1563 n = cls()
1579 1564 n.user = user
1580 1565 n.user_group = user_group
1581 1566 n.permission = permission
1582 1567 Session().add(n)
1583 1568 return n
1584 1569
1585 1570 def __unicode__(self):
1586 1571 return u'<%s => %s >' % (self.user, self.user_group)
1587 1572
1588 1573
1589 1574 class UserToPerm(Base, BaseModel):
1590 1575 __tablename__ = 'user_to_perm'
1591 1576 __table_args__ = (
1592 1577 UniqueConstraint('user_id', 'permission_id'),
1593 1578 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1594 1579 'mysql_charset': 'utf8'}
1595 1580 )
1596 1581 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1597 1582 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1598 1583 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1599 1584
1600 1585 user = relationship('User')
1601 1586 permission = relationship('Permission', lazy='joined')
1602 1587
1603 1588 def __unicode__(self):
1604 1589 return u'<%s => %s >' % (self.user, self.permission)
1605 1590
1606 1591
1607 1592 class UserGroupRepoToPerm(Base, BaseModel):
1608 1593 __tablename__ = 'users_group_repo_to_perm'
1609 1594 __table_args__ = (
1610 1595 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1611 1596 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1612 1597 'mysql_charset': 'utf8'}
1613 1598 )
1614 1599 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1615 1600 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1616 1601 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1617 1602 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1618 1603
1619 1604 users_group = relationship('UserGroup')
1620 1605 permission = relationship('Permission')
1621 1606 repository = relationship('Repository')
1622 1607
1623 1608 @classmethod
1624 1609 def create(cls, users_group, repository, permission):
1625 1610 n = cls()
1626 1611 n.users_group = users_group
1627 1612 n.repository = repository
1628 1613 n.permission = permission
1629 1614 Session().add(n)
1630 1615 return n
1631 1616
1632 1617 def __unicode__(self):
1633 1618 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1634 1619
1635 1620
1636 1621 #TODO; not sure if this will be ever used
1637 1622 class UserGroupUserGroupToPerm(Base, BaseModel):
1638 1623 __tablename__ = 'user_group_user_group_to_perm'
1639 1624 __table_args__ = (
1640 1625 UniqueConstraint('user_group_id', 'user_group_id', 'permission_id'),
1641 1626 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1642 1627 'mysql_charset': 'utf8'}
1643 1628 )
1644 1629 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1645 1630 target_user_group_id = Column("target_users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1646 1631 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1647 1632 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1648 1633
1649 1634 target_user_group = relationship('UserGroup', remote_side=target_user_group_id, primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1650 1635 user_group = relationship('UserGroup', remote_side=user_group_id, primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1651 1636 permission = relationship('Permission')
1652 1637
1653 1638 @classmethod
1654 1639 def create(cls, target_user_group, user_group, permission):
1655 1640 n = cls()
1656 1641 n.target_user_group = target_user_group
1657 1642 n.user_group = user_group
1658 1643 n.permission = permission
1659 1644 Session().add(n)
1660 1645 return n
1661 1646
1662 1647 def __unicode__(self):
1663 1648 return u'<UserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1664 1649
1665 1650
1666 1651 class UserGroupToPerm(Base, BaseModel):
1667 1652 __tablename__ = 'users_group_to_perm'
1668 1653 __table_args__ = (
1669 1654 UniqueConstraint('users_group_id', 'permission_id',),
1670 1655 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1671 1656 'mysql_charset': 'utf8'}
1672 1657 )
1673 1658 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1674 1659 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1675 1660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1676 1661
1677 1662 users_group = relationship('UserGroup')
1678 1663 permission = relationship('Permission')
1679 1664
1680 1665
1681 1666 class UserRepoGroupToPerm(Base, BaseModel):
1682 1667 __tablename__ = 'user_repo_group_to_perm'
1683 1668 __table_args__ = (
1684 1669 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1685 1670 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1686 1671 'mysql_charset': 'utf8'}
1687 1672 )
1688 1673
1689 1674 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1690 1675 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1691 1676 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1692 1677 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1693 1678
1694 1679 user = relationship('User')
1695 1680 group = relationship('RepoGroup')
1696 1681 permission = relationship('Permission')
1697 1682
1698 1683
1699 1684 class UserGroupRepoGroupToPerm(Base, BaseModel):
1700 1685 __tablename__ = 'users_group_repo_group_to_perm'
1701 1686 __table_args__ = (
1702 1687 UniqueConstraint('users_group_id', 'group_id'),
1703 1688 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1704 1689 'mysql_charset': 'utf8'}
1705 1690 )
1706 1691
1707 1692 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1708 1693 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1709 1694 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1710 1695 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1711 1696
1712 1697 users_group = relationship('UserGroup')
1713 1698 permission = relationship('Permission')
1714 1699 group = relationship('RepoGroup')
1715 1700
1716 1701
1717 1702 class Statistics(Base, BaseModel):
1718 1703 __tablename__ = 'statistics'
1719 1704 __table_args__ = (
1720 1705 UniqueConstraint('repository_id'),
1721 1706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1722 1707 'mysql_charset': 'utf8'}
1723 1708 )
1724 1709 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1725 1710 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1726 1711 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1727 1712 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1728 1713 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1729 1714 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1730 1715
1731 1716 repository = relationship('Repository', single_parent=True)
1732 1717
1733 1718
1734 1719 class UserFollowing(Base, BaseModel):
1735 1720 __tablename__ = 'user_followings'
1736 1721 __table_args__ = (
1737 1722 UniqueConstraint('user_id', 'follows_repository_id'),
1738 1723 UniqueConstraint('user_id', 'follows_user_id'),
1739 1724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1740 1725 'mysql_charset': 'utf8'}
1741 1726 )
1742 1727
1743 1728 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1744 1729 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1745 1730 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1746 1731 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1747 1732 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1748 1733
1749 1734 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1750 1735
1751 1736 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1752 1737 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1753 1738
1754 1739 @classmethod
1755 1740 def get_repo_followers(cls, repo_id):
1756 1741 return cls.query().filter(cls.follows_repo_id == repo_id)
1757 1742
1758 1743
1759 1744 class CacheInvalidation(Base, BaseModel):
1760 1745 __tablename__ = 'cache_invalidation'
1761 1746 __table_args__ = (
1762 1747 UniqueConstraint('cache_key'),
1763 1748 Index('key_idx', 'cache_key'),
1764 1749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1765 1750 'mysql_charset': 'utf8'},
1766 1751 )
1767 1752 # cache_id, not used
1768 1753 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1769 1754 # cache_key as created by _get_cache_key
1770 1755 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1771 1756 # cache_args is a repo_name
1772 1757 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1773 1758 # instance sets cache_active True when it is caching,
1774 1759 # other instances set cache_active to False to indicate that this cache is invalid
1775 1760 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1776 1761
1777 1762 def __init__(self, cache_key, repo_name=''):
1778 1763 self.cache_key = cache_key
1779 1764 self.cache_args = repo_name
1780 1765 self.cache_active = False
1781 1766
1782 1767 def __unicode__(self):
1783 1768 return u"<%s('%s:%s[%s]')>" % (self.__class__.__name__,
1784 1769 self.cache_id, self.cache_key, self.cache_active)
1785 1770
1786 1771 def _cache_key_partition(self):
1787 1772 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
1788 1773 return prefix, repo_name, suffix
1789 1774
1790 1775 def get_prefix(self):
1791 1776 """
1792 1777 get prefix that might have been used in _get_cache_key to
1793 1778 generate self.cache_key. Only used for informational purposes
1794 1779 in repo_edit.html.
1795 1780 """
1796 1781 # prefix, repo_name, suffix
1797 1782 return self._cache_key_partition()[0]
1798 1783
1799 1784 def get_suffix(self):
1800 1785 """
1801 1786 get suffix that might have been used in _get_cache_key to
1802 1787 generate self.cache_key. Only used for informational purposes
1803 1788 in repo_edit.html.
1804 1789 """
1805 1790 # prefix, repo_name, suffix
1806 1791 return self._cache_key_partition()[2]
1807 1792
1808 1793 @classmethod
1809 1794 def clear_cache(cls):
1810 1795 """
1811 1796 Delete all cache keys from database.
1812 1797 Should only be run when all instances are down and all entries thus stale.
1813 1798 """
1814 1799 cls.query().delete()
1815 1800 Session().commit()
1816 1801
1817 1802 @classmethod
1818 1803 def _get_cache_key(cls, key):
1819 1804 """
1820 1805 Wrapper for generating a unique cache key for this instance and "key".
1821 1806 key must / will start with a repo_name which will be stored in .cache_args .
1822 1807 """
1823 1808 import rhodecode
1824 1809 prefix = rhodecode.CONFIG.get('instance_id', '')
1825 1810 return "%s%s" % (prefix, key)
1826 1811
1827 1812 @classmethod
1828 def invalidate(cls, key):
1829 """
1830 Returns Invalidation object if the local cache with the given key is invalid,
1831 None otherwise.
1832 """
1833 repo_name = key
1834 repo_name = remove_suffix(repo_name, '_README')
1835 repo_name = remove_suffix(repo_name, '_RSS')
1836 repo_name = remove_suffix(repo_name, '_ATOM')
1837
1838 cache_key = cls._get_cache_key(key)
1839 inv_obj = Session().query(cls).filter(cls.cache_key == cache_key).scalar()
1840 if not inv_obj:
1841 try:
1842 inv_obj = CacheInvalidation(cache_key, repo_name)
1843 Session().add(inv_obj)
1844 Session().commit()
1845 except Exception:
1846 log.error(traceback.format_exc())
1847 Session().rollback()
1848 return
1849
1850 if not inv_obj.cache_active:
1851 # `cache_active = False` means that this cache
1852 # no longer is valid
1853 return inv_obj
1854
1855 @classmethod
1856 1813 def set_invalidate(cls, repo_name):
1857 1814 """
1858 1815 Mark all caches of a repo as invalid in the database.
1859 1816 """
1860 1817 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1861 1818
1862 1819 try:
1863 1820 for inv_obj in inv_objs:
1864 1821 log.debug('marking %s key for invalidation based on repo_name=%s'
1865 1822 % (inv_obj, safe_str(repo_name)))
1866 1823 inv_obj.cache_active = False
1867 1824 Session().add(inv_obj)
1868 1825 Session().commit()
1869 1826 except Exception:
1870 1827 log.error(traceback.format_exc())
1871 1828 Session().rollback()
1872 1829
1873 1830 @classmethod
1874 def set_valid(cls, cache_key):
1831 def test_and_set_valid(cls, key, valid_cache_keys=None):
1875 1832 """
1876 Mark this cache key as active and currently cached
1833 Mark this cache key as active and currently cached.
1834 Return True if the existing cache registration still was valid.
1835 Return False to indicate that it had been invalidated and caches should be refreshed.
1877 1836 """
1878 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
1879 inv_obj.cache_active = True
1880 Session().add(inv_obj)
1881 Session().commit()
1837 cache_key = cls._get_cache_key(key)
1838
1839 if valid_cache_keys and cache_key in valid_cache_keys:
1840 return True
1841
1842 repo_name = key
1843 repo_name = remove_suffix(repo_name, '_README')
1844 repo_name = remove_suffix(repo_name, '_RSS')
1845 repo_name = remove_suffix(repo_name, '_ATOM')
1846
1847 try:
1848 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
1849 if not inv_obj:
1850 inv_obj = CacheInvalidation(cache_key, repo_name)
1851 was_valid = inv_obj.cache_active
1852 inv_obj.cache_active = True
1853 Session().add(inv_obj)
1854 Session().commit()
1855 return was_valid
1856 except Exception:
1857 log.error(traceback.format_exc())
1858 Session().rollback()
1859 return False
1882 1860
1883 1861 @classmethod
1884 def get_cache_map(cls):
1885
1886 class cachemapdict(dict):
1887
1888 def __init__(self, *args, **kwargs):
1889 self.fixkey = kwargs.pop('fixkey', False)
1890 super(cachemapdict, self).__init__(*args, **kwargs)
1891
1892 def __getattr__(self, name):
1893 cache_key = name
1894 if self.fixkey:
1895 cache_key = cls._get_cache_key(name)
1896 if cache_key in self.__dict__:
1897 return self.__dict__[cache_key]
1898 else:
1899 return self[cache_key]
1900
1901 def __getitem__(self, name):
1902 cache_key = name
1903 if self.fixkey:
1904 cache_key = cls._get_cache_key(name)
1905 try:
1906 return super(cachemapdict, self).__getitem__(cache_key)
1907 except KeyError:
1908 return None
1909
1910 cache_map = cachemapdict(fixkey=True)
1911 for obj in cls.query().all():
1912 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1913 return cache_map
1862 def get_valid_cache_keys(cls):
1863 """
1864 Return opaque object with information of which caches still are valid
1865 and can be used without checking for invalidation.
1866 """
1867 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
1914 1868
1915 1869
1916 1870 class ChangesetComment(Base, BaseModel):
1917 1871 __tablename__ = 'changeset_comments'
1918 1872 __table_args__ = (
1919 1873 Index('cc_revision_idx', 'revision'),
1920 1874 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1921 1875 'mysql_charset': 'utf8'},
1922 1876 )
1923 1877 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1924 1878 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1925 1879 revision = Column('revision', String(40), nullable=True)
1926 1880 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1927 1881 line_no = Column('line_no', Unicode(10), nullable=True)
1928 1882 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1929 1883 f_path = Column('f_path', Unicode(1000), nullable=True)
1930 1884 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1931 1885 text = Column('text', UnicodeText(25000), nullable=False)
1932 1886 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1933 1887 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1934 1888
1935 1889 author = relationship('User', lazy='joined')
1936 1890 repo = relationship('Repository')
1937 1891 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1938 1892 pull_request = relationship('PullRequest', lazy='joined')
1939 1893
1940 1894 @classmethod
1941 1895 def get_users(cls, revision=None, pull_request_id=None):
1942 1896 """
1943 1897 Returns user associated with this ChangesetComment. ie those
1944 1898 who actually commented
1945 1899
1946 1900 :param cls:
1947 1901 :param revision:
1948 1902 """
1949 1903 q = Session().query(User)\
1950 1904 .join(ChangesetComment.author)
1951 1905 if revision:
1952 1906 q = q.filter(cls.revision == revision)
1953 1907 elif pull_request_id:
1954 1908 q = q.filter(cls.pull_request_id == pull_request_id)
1955 1909 return q.all()
1956 1910
1957 1911
1958 1912 class ChangesetStatus(Base, BaseModel):
1959 1913 __tablename__ = 'changeset_statuses'
1960 1914 __table_args__ = (
1961 1915 Index('cs_revision_idx', 'revision'),
1962 1916 Index('cs_version_idx', 'version'),
1963 1917 UniqueConstraint('repo_id', 'revision', 'version'),
1964 1918 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1965 1919 'mysql_charset': 'utf8'}
1966 1920 )
1967 1921 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1968 1922 STATUS_APPROVED = 'approved'
1969 1923 STATUS_REJECTED = 'rejected'
1970 1924 STATUS_UNDER_REVIEW = 'under_review'
1971 1925
1972 1926 STATUSES = [
1973 1927 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1974 1928 (STATUS_APPROVED, _("Approved")),
1975 1929 (STATUS_REJECTED, _("Rejected")),
1976 1930 (STATUS_UNDER_REVIEW, _("Under Review")),
1977 1931 ]
1978 1932
1979 1933 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1980 1934 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1981 1935 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1982 1936 revision = Column('revision', String(40), nullable=False)
1983 1937 status = Column('status', String(128), nullable=False, default=DEFAULT)
1984 1938 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1985 1939 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1986 1940 version = Column('version', Integer(), nullable=False, default=0)
1987 1941 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1988 1942
1989 1943 author = relationship('User', lazy='joined')
1990 1944 repo = relationship('Repository')
1991 1945 comment = relationship('ChangesetComment', lazy='joined')
1992 1946 pull_request = relationship('PullRequest', lazy='joined')
1993 1947
1994 1948 def __unicode__(self):
1995 1949 return u"<%s('%s:%s')>" % (
1996 1950 self.__class__.__name__,
1997 1951 self.status, self.author
1998 1952 )
1999 1953
2000 1954 @classmethod
2001 1955 def get_status_lbl(cls, value):
2002 1956 return dict(cls.STATUSES).get(value)
2003 1957
2004 1958 @property
2005 1959 def status_lbl(self):
2006 1960 return ChangesetStatus.get_status_lbl(self.status)
2007 1961
2008 1962
2009 1963 class PullRequest(Base, BaseModel):
2010 1964 __tablename__ = 'pull_requests'
2011 1965 __table_args__ = (
2012 1966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2013 1967 'mysql_charset': 'utf8'},
2014 1968 )
2015 1969
2016 1970 STATUS_NEW = u'new'
2017 1971 STATUS_OPEN = u'open'
2018 1972 STATUS_CLOSED = u'closed'
2019 1973
2020 1974 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
2021 1975 title = Column('title', Unicode(256), nullable=True)
2022 1976 description = Column('description', UnicodeText(10240), nullable=True)
2023 1977 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
2024 1978 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2025 1979 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2026 1980 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2027 1981 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
2028 1982 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2029 1983 org_ref = Column('org_ref', Unicode(256), nullable=False)
2030 1984 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2031 1985 other_ref = Column('other_ref', Unicode(256), nullable=False)
2032 1986
2033 1987 @hybrid_property
2034 1988 def revisions(self):
2035 1989 return self._revisions.split(':')
2036 1990
2037 1991 @revisions.setter
2038 1992 def revisions(self, val):
2039 1993 self._revisions = ':'.join(val)
2040 1994
2041 1995 @property
2042 1996 def org_ref_parts(self):
2043 1997 return self.org_ref.split(':')
2044 1998
2045 1999 @property
2046 2000 def other_ref_parts(self):
2047 2001 return self.other_ref.split(':')
2048 2002
2049 2003 author = relationship('User', lazy='joined')
2050 2004 reviewers = relationship('PullRequestReviewers',
2051 2005 cascade="all, delete, delete-orphan")
2052 2006 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2053 2007 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2054 2008 statuses = relationship('ChangesetStatus')
2055 2009 comments = relationship('ChangesetComment',
2056 2010 cascade="all, delete, delete-orphan")
2057 2011
2058 2012 def is_closed(self):
2059 2013 return self.status == self.STATUS_CLOSED
2060 2014
2061 2015 @property
2062 2016 def last_review_status(self):
2063 2017 return self.statuses[-1].status if self.statuses else ''
2064 2018
2065 2019 def __json__(self):
2066 2020 return dict(
2067 2021 revisions=self.revisions
2068 2022 )
2069 2023
2070 2024
2071 2025 class PullRequestReviewers(Base, BaseModel):
2072 2026 __tablename__ = 'pull_request_reviewers'
2073 2027 __table_args__ = (
2074 2028 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2075 2029 'mysql_charset': 'utf8'},
2076 2030 )
2077 2031
2078 2032 def __init__(self, user=None, pull_request=None):
2079 2033 self.user = user
2080 2034 self.pull_request = pull_request
2081 2035
2082 2036 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
2083 2037 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2084 2038 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
2085 2039
2086 2040 user = relationship('User')
2087 2041 pull_request = relationship('PullRequest')
2088 2042
2089 2043
2090 2044 class Notification(Base, BaseModel):
2091 2045 __tablename__ = 'notifications'
2092 2046 __table_args__ = (
2093 2047 Index('notification_type_idx', 'type'),
2094 2048 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2095 2049 'mysql_charset': 'utf8'},
2096 2050 )
2097 2051
2098 2052 TYPE_CHANGESET_COMMENT = u'cs_comment'
2099 2053 TYPE_MESSAGE = u'message'
2100 2054 TYPE_MENTION = u'mention'
2101 2055 TYPE_REGISTRATION = u'registration'
2102 2056 TYPE_PULL_REQUEST = u'pull_request'
2103 2057 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
2104 2058
2105 2059 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
2106 2060 subject = Column('subject', Unicode(512), nullable=True)
2107 2061 body = Column('body', UnicodeText(50000), nullable=True)
2108 2062 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
2109 2063 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2110 2064 type_ = Column('type', Unicode(256))
2111 2065
2112 2066 created_by_user = relationship('User')
2113 2067 notifications_to_users = relationship('UserNotification', lazy='joined',
2114 2068 cascade="all, delete, delete-orphan")
2115 2069
2116 2070 @property
2117 2071 def recipients(self):
2118 2072 return [x.user for x in UserNotification.query()\
2119 2073 .filter(UserNotification.notification == self)\
2120 2074 .order_by(UserNotification.user_id.asc()).all()]
2121 2075
2122 2076 @classmethod
2123 2077 def create(cls, created_by, subject, body, recipients, type_=None):
2124 2078 if type_ is None:
2125 2079 type_ = Notification.TYPE_MESSAGE
2126 2080
2127 2081 notification = cls()
2128 2082 notification.created_by_user = created_by
2129 2083 notification.subject = subject
2130 2084 notification.body = body
2131 2085 notification.type_ = type_
2132 2086 notification.created_on = datetime.datetime.now()
2133 2087
2134 2088 for u in recipients:
2135 2089 assoc = UserNotification()
2136 2090 assoc.notification = notification
2137 2091 u.notifications.append(assoc)
2138 2092 Session().add(notification)
2139 2093 return notification
2140 2094
2141 2095 @property
2142 2096 def description(self):
2143 2097 from rhodecode.model.notification import NotificationModel
2144 2098 return NotificationModel().make_description(self)
2145 2099
2146 2100
2147 2101 class UserNotification(Base, BaseModel):
2148 2102 __tablename__ = 'user_to_notification'
2149 2103 __table_args__ = (
2150 2104 UniqueConstraint('user_id', 'notification_id'),
2151 2105 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2152 2106 'mysql_charset': 'utf8'}
2153 2107 )
2154 2108 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2155 2109 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2156 2110 read = Column('read', Boolean, default=False)
2157 2111 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2158 2112
2159 2113 user = relationship('User', lazy="joined")
2160 2114 notification = relationship('Notification', lazy="joined",
2161 2115 order_by=lambda: Notification.created_on.desc(),)
2162 2116
2163 2117 def mark_as_read(self):
2164 2118 self.read = True
2165 2119 Session().add(self)
2166 2120
2167 2121
2168 2122 class DbMigrateVersion(Base, BaseModel):
2169 2123 __tablename__ = 'db_migrate_version'
2170 2124 __table_args__ = (
2171 2125 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2172 2126 'mysql_charset': 'utf8'},
2173 2127 )
2174 2128 repository_id = Column('repository_id', String(250), primary_key=True)
2175 2129 repository_path = Column('repository_path', Text)
2176 2130 version = Column('version', Integer)
@@ -1,693 +1,693 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, get_server_url,\
48 48 _set_extras
49 49 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny,\
50 50 HasUserGroupPermissionAnyDecorator, HasUserGroupPermissionAny
51 51 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
52 52 action_logger, REMOVED_REPO_PAT
53 53 from rhodecode.model import BaseModel
54 54 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
55 55 UserFollowing, UserLog, User, RepoGroup, PullRequest
56 56 from rhodecode.lib.hooks import log_push_action
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 class UserTemp(object):
62 62 def __init__(self, user_id):
63 63 self.user_id = user_id
64 64
65 65 def __repr__(self):
66 66 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
67 67
68 68
69 69 class RepoTemp(object):
70 70 def __init__(self, repo_id):
71 71 self.repo_id = repo_id
72 72
73 73 def __repr__(self):
74 74 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
75 75
76 76
77 77 class CachedRepoList(object):
78 78 """
79 79 Cached repo list, uses in-memory cache after initialization, that is
80 80 super fast
81 81 """
82 82
83 83 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
84 84 self.db_repo_list = db_repo_list
85 85 self.repos_path = repos_path
86 86 self.order_by = order_by
87 87 self.reversed = (order_by or '').startswith('-')
88 88 if not perm_set:
89 89 perm_set = ['repository.read', 'repository.write',
90 90 'repository.admin']
91 91 self.perm_set = perm_set
92 92
93 93 def __len__(self):
94 94 return len(self.db_repo_list)
95 95
96 96 def __repr__(self):
97 97 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
98 98
99 99 def __iter__(self):
100 # pre-propagated cache_map to save executing select statements
100 # pre-propagated valid_cache_keys to save executing select statements
101 101 # for each repo
102 cache_map = CacheInvalidation.get_cache_map()
102 valid_cache_keys = CacheInvalidation.get_valid_cache_keys()
103 103
104 104 for dbr in self.db_repo_list:
105 scmr = dbr.scm_instance_cached(cache_map)
105 scmr = dbr.scm_instance_cached(valid_cache_keys)
106 106 # check permission at this level
107 107 if not HasRepoPermissionAny(
108 108 *self.perm_set)(dbr.repo_name, 'get repo check'):
109 109 continue
110 110
111 111 try:
112 112 last_change = scmr.last_change
113 113 tip = h.get_changeset_safe(scmr, 'tip')
114 114 except Exception:
115 115 log.error(
116 116 '%s this repository is present in database but it '
117 117 'cannot be created as an scm instance, org_exc:%s'
118 118 % (dbr.repo_name, traceback.format_exc())
119 119 )
120 120 continue
121 121
122 122 tmp_d = {}
123 123 tmp_d['name'] = dbr.repo_name
124 124 tmp_d['name_sort'] = tmp_d['name'].lower()
125 125 tmp_d['raw_name'] = tmp_d['name'].lower()
126 126 tmp_d['description'] = dbr.description
127 127 tmp_d['description_sort'] = tmp_d['description'].lower()
128 128 tmp_d['last_change'] = last_change
129 129 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
130 130 tmp_d['tip'] = tip.raw_id
131 131 tmp_d['tip_sort'] = tip.revision
132 132 tmp_d['rev'] = tip.revision
133 133 tmp_d['contact'] = dbr.user.full_contact
134 134 tmp_d['contact_sort'] = tmp_d['contact']
135 135 tmp_d['owner_sort'] = tmp_d['contact']
136 136 tmp_d['repo_archives'] = list(scmr._get_archives())
137 137 tmp_d['last_msg'] = tip.message
138 138 tmp_d['author'] = tip.author
139 139 tmp_d['dbrepo'] = dbr.get_dict()
140 140 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
141 141 yield tmp_d
142 142
143 143
144 144 class SimpleCachedRepoList(CachedRepoList):
145 145 """
146 146 Lighter version of CachedRepoList without the scm initialisation
147 147 """
148 148
149 149 def __iter__(self):
150 150 for dbr in self.db_repo_list:
151 151 # check permission at this level
152 152 if not HasRepoPermissionAny(
153 153 *self.perm_set)(dbr.repo_name, 'get repo check'):
154 154 continue
155 155
156 156 tmp_d = {}
157 157 tmp_d['name'] = dbr.repo_name
158 158 tmp_d['name_sort'] = tmp_d['name'].lower()
159 159 tmp_d['raw_name'] = tmp_d['name'].lower()
160 160 tmp_d['description'] = dbr.description
161 161 tmp_d['description_sort'] = tmp_d['description'].lower()
162 162 tmp_d['dbrepo'] = dbr.get_dict()
163 163 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
164 164 yield tmp_d
165 165
166 166
167 167 class _PermCheckIterator(object):
168 168 def __init__(self, obj_list, obj_attr, perm_set, perm_checker):
169 169 """
170 170 Creates iterator from given list of objects, additionally
171 171 checking permission for them from perm_set var
172 172
173 173 :param obj_list: list of db objects
174 174 :param obj_attr: attribute of object to pass into perm_checker
175 175 :param perm_set: list of permissions to check
176 176 :param perm_checker: callable to check permissions against
177 177 """
178 178 self.obj_list = obj_list
179 179 self.obj_attr = obj_attr
180 180 self.perm_set = perm_set
181 181 self.perm_checker = perm_checker
182 182
183 183 def __len__(self):
184 184 return len(self.obj_list)
185 185
186 186 def __repr__(self):
187 187 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
188 188
189 189 def __iter__(self):
190 190 for db_obj in self.obj_list:
191 191 # check permission at this level
192 192 name = getattr(db_obj, self.obj_attr, None)
193 193 if not self.perm_checker(*self.perm_set)(name, self.__class__.__name__):
194 194 continue
195 195
196 196 yield db_obj
197 197
198 198
199 199 class RepoGroupList(_PermCheckIterator):
200 200
201 201 def __init__(self, db_repo_group_list, perm_set=None):
202 202 if not perm_set:
203 203 perm_set = ['group.read', 'group.write', 'group.admin']
204 204
205 205 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
206 206 obj_attr='group_name', perm_set=perm_set,
207 207 perm_checker=HasReposGroupPermissionAny)
208 208
209 209
210 210 class UserGroupList(_PermCheckIterator):
211 211
212 212 def __init__(self, db_user_group_list, perm_set=None):
213 213 if not perm_set:
214 214 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
215 215
216 216 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
217 217 obj_attr='users_group_name', perm_set=perm_set,
218 218 perm_checker=HasUserGroupPermissionAny)
219 219
220 220
221 221 class ScmModel(BaseModel):
222 222 """
223 223 Generic Scm Model
224 224 """
225 225
226 226 def __get_repo(self, instance):
227 227 cls = Repository
228 228 if isinstance(instance, cls):
229 229 return instance
230 230 elif isinstance(instance, int) or safe_str(instance).isdigit():
231 231 return cls.get(instance)
232 232 elif isinstance(instance, basestring):
233 233 return cls.get_by_repo_name(instance)
234 234 elif instance:
235 235 raise Exception('given object must be int, basestr or Instance'
236 236 ' of %s got %s' % (type(cls), type(instance)))
237 237
238 238 @LazyProperty
239 239 def repos_path(self):
240 240 """
241 241 Get's the repositories root path from database
242 242 """
243 243
244 244 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
245 245
246 246 return q.ui_value
247 247
248 248 def repo_scan(self, repos_path=None):
249 249 """
250 250 Listing of repositories in given path. This path should not be a
251 251 repository itself. Return a dictionary of repository objects
252 252
253 253 :param repos_path: path to directory containing repositories
254 254 """
255 255
256 256 if repos_path is None:
257 257 repos_path = self.repos_path
258 258
259 259 log.info('scanning for repositories in %s' % repos_path)
260 260
261 261 baseui = make_ui('db')
262 262 repos = {}
263 263
264 264 for name, path in get_filesystem_repos(repos_path, recursive=True):
265 265 # name need to be decomposed and put back together using the /
266 266 # since this is internal storage separator for rhodecode
267 267 name = Repository.normalize_repo_name(name)
268 268
269 269 try:
270 270 if name in repos:
271 271 raise RepositoryError('Duplicate repository name %s '
272 272 'found in %s' % (name, path))
273 273 else:
274 274
275 275 klass = get_backend(path[0])
276 276
277 277 if path[0] == 'hg' and path[0] in BACKENDS.keys():
278 278 repos[name] = klass(safe_str(path[1]), baseui=baseui)
279 279
280 280 if path[0] == 'git' and path[0] in BACKENDS.keys():
281 281 repos[name] = klass(path[1])
282 282 except OSError:
283 283 continue
284 284 log.debug('found %s paths with repositories' % (len(repos)))
285 285 return repos
286 286
287 287 def get_repos(self, all_repos=None, sort_key=None, simple=False):
288 288 """
289 289 Get all repos from db and for each repo create it's
290 290 backend instance and fill that backed with information from database
291 291
292 292 :param all_repos: list of repository names as strings
293 293 give specific repositories list, good for filtering
294 294
295 295 :param sort_key: initial sorting of repos
296 296 :param simple: use SimpleCachedList - one without the SCM info
297 297 """
298 298 if all_repos is None:
299 299 all_repos = self.sa.query(Repository)\
300 300 .filter(Repository.group_id == None)\
301 301 .order_by(func.lower(Repository.repo_name)).all()
302 302 if simple:
303 303 repo_iter = SimpleCachedRepoList(all_repos,
304 304 repos_path=self.repos_path,
305 305 order_by=sort_key)
306 306 else:
307 307 repo_iter = CachedRepoList(all_repos,
308 308 repos_path=self.repos_path,
309 309 order_by=sort_key)
310 310
311 311 return repo_iter
312 312
313 313 def get_repos_groups(self, all_groups=None):
314 314 if all_groups is None:
315 315 all_groups = RepoGroup.query()\
316 316 .filter(RepoGroup.group_parent_id == None).all()
317 317 return [x for x in RepoGroupList(all_groups)]
318 318
319 319 def mark_for_invalidation(self, repo_name):
320 320 """
321 321 Mark caches of this repo invalid in the database.
322 322
323 323 :param repo_name: the repo for which caches should be marked invalid
324 324 """
325 325 CacheInvalidation.set_invalidate(repo_name)
326 326 repo = Repository.get_by_repo_name(repo_name)
327 327 if repo:
328 328 repo.update_changeset_cache()
329 329
330 330 def toggle_following_repo(self, follow_repo_id, user_id):
331 331
332 332 f = self.sa.query(UserFollowing)\
333 333 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
334 334 .filter(UserFollowing.user_id == user_id).scalar()
335 335
336 336 if f is not None:
337 337 try:
338 338 self.sa.delete(f)
339 339 action_logger(UserTemp(user_id),
340 340 'stopped_following_repo',
341 341 RepoTemp(follow_repo_id))
342 342 return
343 343 except Exception:
344 344 log.error(traceback.format_exc())
345 345 raise
346 346
347 347 try:
348 348 f = UserFollowing()
349 349 f.user_id = user_id
350 350 f.follows_repo_id = follow_repo_id
351 351 self.sa.add(f)
352 352
353 353 action_logger(UserTemp(user_id),
354 354 'started_following_repo',
355 355 RepoTemp(follow_repo_id))
356 356 except Exception:
357 357 log.error(traceback.format_exc())
358 358 raise
359 359
360 360 def toggle_following_user(self, follow_user_id, user_id):
361 361 f = self.sa.query(UserFollowing)\
362 362 .filter(UserFollowing.follows_user_id == follow_user_id)\
363 363 .filter(UserFollowing.user_id == user_id).scalar()
364 364
365 365 if f is not None:
366 366 try:
367 367 self.sa.delete(f)
368 368 return
369 369 except Exception:
370 370 log.error(traceback.format_exc())
371 371 raise
372 372
373 373 try:
374 374 f = UserFollowing()
375 375 f.user_id = user_id
376 376 f.follows_user_id = follow_user_id
377 377 self.sa.add(f)
378 378 except Exception:
379 379 log.error(traceback.format_exc())
380 380 raise
381 381
382 382 def is_following_repo(self, repo_name, user_id, cache=False):
383 383 r = self.sa.query(Repository)\
384 384 .filter(Repository.repo_name == repo_name).scalar()
385 385
386 386 f = self.sa.query(UserFollowing)\
387 387 .filter(UserFollowing.follows_repository == r)\
388 388 .filter(UserFollowing.user_id == user_id).scalar()
389 389
390 390 return f is not None
391 391
392 392 def is_following_user(self, username, user_id, cache=False):
393 393 u = User.get_by_username(username)
394 394
395 395 f = self.sa.query(UserFollowing)\
396 396 .filter(UserFollowing.follows_user == u)\
397 397 .filter(UserFollowing.user_id == user_id).scalar()
398 398
399 399 return f is not None
400 400
401 401 def get_followers(self, repo):
402 402 repo = self._get_repo(repo)
403 403
404 404 return self.sa.query(UserFollowing)\
405 405 .filter(UserFollowing.follows_repository == repo).count()
406 406
407 407 def get_forks(self, repo):
408 408 repo = self._get_repo(repo)
409 409 return self.sa.query(Repository)\
410 410 .filter(Repository.fork == repo).count()
411 411
412 412 def get_pull_requests(self, repo):
413 413 repo = self._get_repo(repo)
414 414 return self.sa.query(PullRequest)\
415 415 .filter(PullRequest.other_repo == repo)\
416 416 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
417 417
418 418 def mark_as_fork(self, repo, fork, user):
419 419 repo = self.__get_repo(repo)
420 420 fork = self.__get_repo(fork)
421 421 if fork and repo.repo_id == fork.repo_id:
422 422 raise Exception("Cannot set repository as fork of itself")
423 423 repo.fork = fork
424 424 self.sa.add(repo)
425 425 return repo
426 426
427 427 def _handle_push(self, repo, username, action, repo_name, revisions):
428 428 """
429 429 Triggers push action hooks
430 430
431 431 :param repo: SCM repo
432 432 :param username: username who pushes
433 433 :param action: push/push_loca/push_remote
434 434 :param repo_name: name of repo
435 435 :param revisions: list of revisions that we pushed
436 436 """
437 437 from rhodecode import CONFIG
438 438 from rhodecode.lib.base import _get_ip_addr
439 439 try:
440 440 from pylons import request
441 441 environ = request.environ
442 442 except TypeError:
443 443 # we might use this outside of request context, let's fake the
444 444 # environ data
445 445 from webob import Request
446 446 environ = Request.blank('').environ
447 447
448 448 #trigger push hook
449 449 extras = {
450 450 'ip': _get_ip_addr(environ),
451 451 'username': username,
452 452 'action': 'push_local',
453 453 'repository': repo_name,
454 454 'scm': repo.alias,
455 455 'config': CONFIG['__file__'],
456 456 'server_url': get_server_url(environ),
457 457 'make_lock': None,
458 458 'locked_by': [None, None]
459 459 }
460 460 _scm_repo = repo._repo
461 461 _set_extras(extras)
462 462 if repo.alias == 'hg':
463 463 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
464 464 elif repo.alias == 'git':
465 465 log_push_action(None, _scm_repo, _git_revs=revisions)
466 466
467 467 def _get_IMC_module(self, scm_type):
468 468 """
469 469 Returns InMemoryCommit class based on scm_type
470 470
471 471 :param scm_type:
472 472 """
473 473 if scm_type == 'hg':
474 474 from rhodecode.lib.vcs.backends.hg import \
475 475 MercurialInMemoryChangeset as IMC
476 476 elif scm_type == 'git':
477 477 from rhodecode.lib.vcs.backends.git import \
478 478 GitInMemoryChangeset as IMC
479 479 return IMC
480 480
481 481 def pull_changes(self, repo, username):
482 482 dbrepo = self.__get_repo(repo)
483 483 clone_uri = dbrepo.clone_uri
484 484 if not clone_uri:
485 485 raise Exception("This repository doesn't have a clone uri")
486 486
487 487 repo = dbrepo.scm_instance
488 488 repo_name = dbrepo.repo_name
489 489 try:
490 490 if repo.alias == 'git':
491 491 repo.fetch(clone_uri)
492 492 else:
493 493 repo.pull(clone_uri)
494 494 self.mark_for_invalidation(repo_name)
495 495 except Exception:
496 496 log.error(traceback.format_exc())
497 497 raise
498 498
499 499 def commit_change(self, repo, repo_name, cs, user, author, message,
500 500 content, f_path):
501 501 """
502 502 Commits changes
503 503
504 504 :param repo: SCM instance
505 505
506 506 """
507 507 user = self._get_user(user)
508 508 IMC = self._get_IMC_module(repo.alias)
509 509
510 510 # decoding here will force that we have proper encoded values
511 511 # in any other case this will throw exceptions and deny commit
512 512 content = safe_str(content)
513 513 path = safe_str(f_path)
514 514 # message and author needs to be unicode
515 515 # proper backend should then translate that into required type
516 516 message = safe_unicode(message)
517 517 author = safe_unicode(author)
518 518 m = IMC(repo)
519 519 m.change(FileNode(path, content))
520 520 tip = m.commit(message=message,
521 521 author=author,
522 522 parents=[cs], branch=cs.branch)
523 523
524 524 self.mark_for_invalidation(repo_name)
525 525 self._handle_push(repo,
526 526 username=user.username,
527 527 action='push_local',
528 528 repo_name=repo_name,
529 529 revisions=[tip.raw_id])
530 530 return tip
531 531
532 532 def create_node(self, repo, repo_name, cs, user, author, message, content,
533 533 f_path):
534 534 user = self._get_user(user)
535 535 IMC = self._get_IMC_module(repo.alias)
536 536
537 537 # decoding here will force that we have proper encoded values
538 538 # in any other case this will throw exceptions and deny commit
539 539 if isinstance(content, (basestring,)):
540 540 content = safe_str(content)
541 541 elif isinstance(content, (file, cStringIO.OutputType,)):
542 542 content = content.read()
543 543 else:
544 544 raise Exception('Content is of unrecognized type %s' % (
545 545 type(content)
546 546 ))
547 547
548 548 message = safe_unicode(message)
549 549 author = safe_unicode(author)
550 550 path = safe_str(f_path)
551 551 m = IMC(repo)
552 552
553 553 if isinstance(cs, EmptyChangeset):
554 554 # EmptyChangeset means we we're editing empty repository
555 555 parents = None
556 556 else:
557 557 parents = [cs]
558 558
559 559 m.add(FileNode(path, content=content))
560 560 tip = m.commit(message=message,
561 561 author=author,
562 562 parents=parents, branch=cs.branch)
563 563
564 564 self.mark_for_invalidation(repo_name)
565 565 self._handle_push(repo,
566 566 username=user.username,
567 567 action='push_local',
568 568 repo_name=repo_name,
569 569 revisions=[tip.raw_id])
570 570 return tip
571 571
572 572 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
573 573 """
574 574 recursive walk in root dir and return a set of all path in that dir
575 575 based on repository walk function
576 576
577 577 :param repo_name: name of repository
578 578 :param revision: revision for which to list nodes
579 579 :param root_path: root path to list
580 580 :param flat: return as a list, if False returns a dict with decription
581 581
582 582 """
583 583 _files = list()
584 584 _dirs = list()
585 585 try:
586 586 _repo = self.__get_repo(repo_name)
587 587 changeset = _repo.scm_instance.get_changeset(revision)
588 588 root_path = root_path.lstrip('/')
589 589 for topnode, dirs, files in changeset.walk(root_path):
590 590 for f in files:
591 591 _files.append(f.path if flat else {"name": f.path,
592 592 "type": "file"})
593 593 for d in dirs:
594 594 _dirs.append(d.path if flat else {"name": d.path,
595 595 "type": "dir"})
596 596 except RepositoryError:
597 597 log.debug(traceback.format_exc())
598 598 raise
599 599
600 600 return _dirs, _files
601 601
602 602 def get_unread_journal(self):
603 603 return self.sa.query(UserLog).count()
604 604
605 605 def get_repo_landing_revs(self, repo=None):
606 606 """
607 607 Generates select option with tags branches and bookmarks (for hg only)
608 608 grouped by type
609 609
610 610 :param repo:
611 611 :type repo:
612 612 """
613 613
614 614 hist_l = []
615 615 choices = []
616 616 repo = self.__get_repo(repo)
617 617 hist_l.append(['tip', _('latest tip')])
618 618 choices.append('tip')
619 619 if not repo:
620 620 return choices, hist_l
621 621
622 622 repo = repo.scm_instance
623 623
624 624 branches_group = ([(k, k) for k, v in
625 625 repo.branches.iteritems()], _("Branches"))
626 626 hist_l.append(branches_group)
627 627 choices.extend([x[0] for x in branches_group[0]])
628 628
629 629 if repo.alias == 'hg':
630 630 bookmarks_group = ([(k, k) for k, v in
631 631 repo.bookmarks.iteritems()], _("Bookmarks"))
632 632 hist_l.append(bookmarks_group)
633 633 choices.extend([x[0] for x in bookmarks_group[0]])
634 634
635 635 tags_group = ([(k, k) for k, v in
636 636 repo.tags.iteritems()], _("Tags"))
637 637 hist_l.append(tags_group)
638 638 choices.extend([x[0] for x in tags_group[0]])
639 639
640 640 return choices, hist_l
641 641
642 642 def install_git_hook(self, repo, force_create=False):
643 643 """
644 644 Creates a rhodecode hook inside a git repository
645 645
646 646 :param repo: Instance of VCS repo
647 647 :param force_create: Create even if same name hook exists
648 648 """
649 649
650 650 loc = jn(repo.path, 'hooks')
651 651 if not repo.bare:
652 652 loc = jn(repo.path, '.git', 'hooks')
653 653 if not os.path.isdir(loc):
654 654 os.makedirs(loc)
655 655
656 656 tmpl_post = pkg_resources.resource_string(
657 657 'rhodecode', jn('config', 'post_receive_tmpl.py')
658 658 )
659 659 tmpl_pre = pkg_resources.resource_string(
660 660 'rhodecode', jn('config', 'pre_receive_tmpl.py')
661 661 )
662 662
663 663 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
664 664 _hook_file = jn(loc, '%s-receive' % h_type)
665 665 _rhodecode_hook = False
666 666 log.debug('Installing git hook in repo %s' % repo)
667 667 if os.path.exists(_hook_file):
668 668 # let's take a look at this hook, maybe it's rhodecode ?
669 669 log.debug('hook exists, checking if it is from rhodecode')
670 670 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
671 671 with open(_hook_file, 'rb') as f:
672 672 data = f.read()
673 673 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
674 674 % 'RC_HOOK_VER').search(data)
675 675 if matches:
676 676 try:
677 677 ver = matches.groups()[0]
678 678 log.debug('got %s it is rhodecode' % (ver))
679 679 _rhodecode_hook = True
680 680 except Exception:
681 681 log.error(traceback.format_exc())
682 682 else:
683 683 # there is no hook in this dir, so we want to create one
684 684 _rhodecode_hook = True
685 685
686 686 if _rhodecode_hook or force_create:
687 687 log.debug('writing %s hook file !' % h_type)
688 688 with open(_hook_file, 'wb') as f:
689 689 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
690 690 f.write(tmpl)
691 691 os.chmod(_hook_file, 0755)
692 692 else:
693 693 log.debug('skipping writing hook file')
General Comments 0
You need to be logged in to leave comments. Login now