##// END OF EJS Templates
Switched handling of RhodeCode extra params in consistent way...
marcink -
r3577:238486bb beta
parent child Browse files
Show More
@@ -1,468 +1,394 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.hooks
3 rhodecode.lib.hooks
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Hooks runned by rhodecode
6 Hooks runned by rhodecode
7
7
8 :created_on: Aug 6, 2010
8 :created_on: Aug 6, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import os
25 import os
26 import sys
26 import sys
27 import time
27 import time
28 import binascii
28 import binascii
29 import traceback
29 from inspect import isfunction
30 from inspect import isfunction
30
31
31 from mercurial.scmutil import revrange
32 from mercurial.scmutil import revrange
32 from mercurial.node import nullrev
33 from mercurial.node import nullrev
33
34
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib.utils import action_logger
36 from rhodecode.lib.utils import action_logger
36 from rhodecode.lib.vcs.backends.base import EmptyChangeset
37 from rhodecode.lib.vcs.backends.base import EmptyChangeset
37 from rhodecode.lib.compat import json
38 from rhodecode.lib.compat import json
38 from rhodecode.lib.exceptions import HTTPLockedRC
39 from rhodecode.lib.exceptions import HTTPLockedRC
39 from rhodecode.lib.utils2 import safe_str
40 from rhodecode.lib.utils2 import safe_str, _extract_extras
40 from rhodecode.model.db import Repository, User
41 from rhodecode.model.db import Repository, User
41
42
42
43
43 def _get_scm_size(alias, root_path):
44 def _get_scm_size(alias, root_path):
44
45
45 if not alias.startswith('.'):
46 if not alias.startswith('.'):
46 alias += '.'
47 alias += '.'
47
48
48 size_scm, size_root = 0, 0
49 size_scm, size_root = 0, 0
49 for path, dirs, files in os.walk(safe_str(root_path)):
50 for path, dirs, files in os.walk(safe_str(root_path)):
50 if path.find(alias) != -1:
51 if path.find(alias) != -1:
51 for f in files:
52 for f in files:
52 try:
53 try:
53 size_scm += os.path.getsize(os.path.join(path, f))
54 size_scm += os.path.getsize(os.path.join(path, f))
54 except OSError:
55 except OSError:
55 pass
56 pass
56 else:
57 else:
57 for f in files:
58 for f in files:
58 try:
59 try:
59 size_root += os.path.getsize(os.path.join(path, f))
60 size_root += os.path.getsize(os.path.join(path, f))
60 except OSError:
61 except OSError:
61 pass
62 pass
62
63
63 size_scm_f = h.format_byte_size(size_scm)
64 size_scm_f = h.format_byte_size(size_scm)
64 size_root_f = h.format_byte_size(size_root)
65 size_root_f = h.format_byte_size(size_root)
65 size_total_f = h.format_byte_size(size_root + size_scm)
66 size_total_f = h.format_byte_size(size_root + size_scm)
66
67
67 return size_scm_f, size_root_f, size_total_f
68 return size_scm_f, size_root_f, size_total_f
68
69
69
70
70 def repo_size(ui, repo, hooktype=None, **kwargs):
71 def repo_size(ui, repo, hooktype=None, **kwargs):
71 """
72 """
72 Presents size of repository after push
73 Presents size of repository after push
73
74
74 :param ui:
75 :param ui:
75 :param repo:
76 :param repo:
76 :param hooktype:
77 :param hooktype:
77 """
78 """
78
79
79 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
80 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
80
81
81 last_cs = repo[len(repo) - 1]
82 last_cs = repo[len(repo) - 1]
82
83
83 msg = ('Repository size .hg:%s repo:%s total:%s\n'
84 msg = ('Repository size .hg:%s repo:%s total:%s\n'
84 'Last revision is now r%s:%s\n') % (
85 'Last revision is now r%s:%s\n') % (
85 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
86 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
86 )
87 )
87
88
88 sys.stdout.write(msg)
89 sys.stdout.write(msg)
89
90
90
91
91 def pre_push(ui, repo, **kwargs):
92 def pre_push(ui, repo, **kwargs):
92 # pre push function, currently used to ban pushing when
93 # pre push function, currently used to ban pushing when
93 # repository is locked
94 # repository is locked
94 try:
95 ex = _extract_extras()
95 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
96 except:
97 rc_extras = {}
98 extras = dict(repo.ui.configitems('rhodecode_extras'))
99
96
100 if 'username' in extras:
97 usr = User.get_by_username(ex.username)
101 username = extras['username']
98 if ex.locked_by[0] and usr.user_id != int(ex.locked_by[0]):
102 repository = extras['repository']
99 locked_by = User.get(ex.locked_by[0]).username
103 scm = extras['scm']
104 locked_by = extras['locked_by']
105 elif 'username' in rc_extras:
106 username = rc_extras['username']
107 repository = rc_extras['repository']
108 scm = rc_extras['scm']
109 locked_by = rc_extras['locked_by']
110 else:
111 raise Exception('Missing data in repo.ui and os.environ')
112
113 usr = User.get_by_username(username)
114 if locked_by[0] and usr.user_id != int(locked_by[0]):
115 locked_by = User.get(locked_by[0]).username
116 # this exception is interpreted in git/hg middlewares and based
100 # this exception is interpreted in git/hg middlewares and based
117 # on that proper return code is server to client
101 # on that proper return code is server to client
118 _http_ret = HTTPLockedRC(repository, locked_by)
102 _http_ret = HTTPLockedRC(ex.repository, locked_by)
119 if str(_http_ret.code).startswith('2'):
103 if str(_http_ret.code).startswith('2'):
120 #2xx Codes don't raise exceptions
104 #2xx Codes don't raise exceptions
121 sys.stdout.write(_http_ret.title)
105 sys.stdout.write(_http_ret.title)
122 else:
106 else:
123 raise _http_ret
107 raise _http_ret
124
108
125
109
126 def pre_pull(ui, repo, **kwargs):
110 def pre_pull(ui, repo, **kwargs):
127 # pre push function, currently used to ban pushing when
111 # pre push function, currently used to ban pushing when
128 # repository is locked
112 # repository is locked
129 try:
113 ex = _extract_extras()
130 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
114 if ex.locked_by[0]:
131 except:
115 locked_by = User.get(ex.locked_by[0]).username
132 rc_extras = {}
133 extras = dict(repo.ui.configitems('rhodecode_extras'))
134 if 'username' in extras:
135 username = extras['username']
136 repository = extras['repository']
137 scm = extras['scm']
138 locked_by = extras['locked_by']
139 elif 'username' in rc_extras:
140 username = rc_extras['username']
141 repository = rc_extras['repository']
142 scm = rc_extras['scm']
143 locked_by = rc_extras['locked_by']
144 else:
145 raise Exception('Missing data in repo.ui and os.environ')
146
147 if locked_by[0]:
148 locked_by = User.get(locked_by[0]).username
149 # this exception is interpreted in git/hg middlewares and based
116 # this exception is interpreted in git/hg middlewares and based
150 # on that proper return code is server to client
117 # on that proper return code is server to client
151 _http_ret = HTTPLockedRC(repository, locked_by)
118 _http_ret = HTTPLockedRC(ex.repository, locked_by)
152 if str(_http_ret.code).startswith('2'):
119 if str(_http_ret.code).startswith('2'):
153 #2xx Codes don't raise exceptions
120 #2xx Codes don't raise exceptions
154 sys.stdout.write(_http_ret.title)
121 sys.stdout.write(_http_ret.title)
155 else:
122 else:
156 raise _http_ret
123 raise _http_ret
157
124
158
125
159 def log_pull_action(ui, repo, **kwargs):
126 def log_pull_action(ui, repo, **kwargs):
160 """
127 """
161 Logs user last pull action
128 Logs user last pull action
162
129
163 :param ui:
130 :param ui:
164 :param repo:
131 :param repo:
165 """
132 """
166 try:
133 ex = _extract_extras()
167 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
134
168 except:
135 user = User.get_by_username(ex.username)
169 rc_extras = {}
170 extras = dict(repo.ui.configitems('rhodecode_extras'))
171 if 'username' in extras:
172 username = extras['username']
173 repository = extras['repository']
174 scm = extras['scm']
175 make_lock = extras['make_lock']
176 locked_by = extras['locked_by']
177 ip = extras['ip']
178 elif 'username' in rc_extras:
179 username = rc_extras['username']
180 repository = rc_extras['repository']
181 scm = rc_extras['scm']
182 make_lock = rc_extras['make_lock']
183 locked_by = rc_extras['locked_by']
184 ip = rc_extras['ip']
185 else:
186 raise Exception('Missing data in repo.ui and os.environ')
187 user = User.get_by_username(username)
188 action = 'pull'
136 action = 'pull'
189 action_logger(user, action, repository, ip, commit=True)
137 action_logger(user, action, ex.repository, ex.ip, commit=True)
190 # extension hook call
138 # extension hook call
191 from rhodecode import EXTENSIONS
139 from rhodecode import EXTENSIONS
192 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
140 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
193
194 if isfunction(callback):
141 if isfunction(callback):
195 kw = {}
142 kw = {}
196 kw.update(extras)
143 kw.update(ex)
197 callback(**kw)
144 callback(**kw)
198
145
199 if make_lock is True:
146 if ex.make_lock is True:
200 Repository.lock(Repository.get_by_repo_name(repository), user.user_id)
147 Repository.lock(Repository.get_by_repo_name(ex.repository), user.user_id)
201 #msg = 'Made lock on repo `%s`' % repository
148 #msg = 'Made lock on repo `%s`' % repository
202 #sys.stdout.write(msg)
149 #sys.stdout.write(msg)
203
150
204 if locked_by[0]:
151 if ex.locked_by[0]:
205 locked_by = User.get(locked_by[0]).username
152 locked_by = User.get(ex.locked_by[0]).username
206 _http_ret = HTTPLockedRC(repository, locked_by)
153 _http_ret = HTTPLockedRC(ex.repository, locked_by)
207 if str(_http_ret.code).startswith('2'):
154 if str(_http_ret.code).startswith('2'):
208 #2xx Codes don't raise exceptions
155 #2xx Codes don't raise exceptions
209 sys.stdout.write(_http_ret.title)
156 sys.stdout.write(_http_ret.title)
210 return 0
157 return 0
211
158
212
159
213 def log_push_action(ui, repo, **kwargs):
160 def log_push_action(ui, repo, **kwargs):
214 """
161 """
215 Maps user last push action to new changeset id, from mercurial
162 Maps user last push action to new changeset id, from mercurial
216
163
217 :param ui:
164 :param ui:
218 :param repo: repo object containing the `ui` object
165 :param repo: repo object containing the `ui` object
219 """
166 """
220
167
221 try:
168 ex = _extract_extras()
222 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
223 except:
224 rc_extras = {}
225
169
226 extras = dict(repo.ui.configitems('rhodecode_extras'))
170 action = ex.action + ':%s'
227 if 'username' in extras:
228 username = extras['username']
229 repository = extras['repository']
230 scm = extras['scm']
231 make_lock = extras['make_lock']
232 locked_by = extras['locked_by']
233 action = extras['action']
234 elif 'username' in rc_extras:
235 username = rc_extras['username']
236 repository = rc_extras['repository']
237 scm = rc_extras['scm']
238 make_lock = rc_extras['make_lock']
239 locked_by = rc_extras['locked_by']
240 action = extras['action']
241 else:
242 raise Exception('Missing data in repo.ui and os.environ')
243
171
244 action = action + ':%s'
172 if ex.scm == 'hg':
245
246 if scm == 'hg':
247 node = kwargs['node']
173 node = kwargs['node']
248
174
249 def get_revs(repo, rev_opt):
175 def get_revs(repo, rev_opt):
250 if rev_opt:
176 if rev_opt:
251 revs = revrange(repo, rev_opt)
177 revs = revrange(repo, rev_opt)
252
178
253 if len(revs) == 0:
179 if len(revs) == 0:
254 return (nullrev, nullrev)
180 return (nullrev, nullrev)
255 return (max(revs), min(revs))
181 return (max(revs), min(revs))
256 else:
182 else:
257 return (len(repo) - 1, 0)
183 return (len(repo) - 1, 0)
258
184
259 stop, start = get_revs(repo, [node + ':'])
185 stop, start = get_revs(repo, [node + ':'])
260 h = binascii.hexlify
186 h = binascii.hexlify
261 revs = [h(repo[r].node()) for r in xrange(start, stop + 1)]
187 revs = [h(repo[r].node()) for r in xrange(start, stop + 1)]
262 elif scm == 'git':
188 elif ex.scm == 'git':
263 revs = kwargs.get('_git_revs', [])
189 revs = kwargs.get('_git_revs', [])
264 if '_git_revs' in kwargs:
190 if '_git_revs' in kwargs:
265 kwargs.pop('_git_revs')
191 kwargs.pop('_git_revs')
266
192
267 action = action % ','.join(revs)
193 action = action % ','.join(revs)
268
194
269 action_logger(username, action, repository, extras['ip'], commit=True)
195 action_logger(ex.username, action, ex.repository, ex.ip, commit=True)
270
196
271 # extension hook call
197 # extension hook call
272 from rhodecode import EXTENSIONS
198 from rhodecode import EXTENSIONS
273 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
199 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
274 if isfunction(callback):
200 if isfunction(callback):
275 kw = {'pushed_revs': revs}
201 kw = {'pushed_revs': revs}
276 kw.update(extras)
202 kw.update(ex)
277 callback(**kw)
203 callback(**kw)
278
204
279 if make_lock is False:
205 if ex.make_lock is False:
280 Repository.unlock(Repository.get_by_repo_name(repository))
206 Repository.unlock(Repository.get_by_repo_name(ex.repository))
281 msg = 'Released lock on repo `%s`\n' % repository
207 msg = 'Released lock on repo `%s`\n' % ex.repository
282 sys.stdout.write(msg)
208 sys.stdout.write(msg)
283
209
284 if locked_by[0]:
210 if ex.locked_by[0]:
285 locked_by = User.get(locked_by[0]).username
211 locked_by = User.get(ex.locked_by[0]).username
286 _http_ret = HTTPLockedRC(repository, locked_by)
212 _http_ret = HTTPLockedRC(ex.repository, locked_by)
287 if str(_http_ret.code).startswith('2'):
213 if str(_http_ret.code).startswith('2'):
288 #2xx Codes don't raise exceptions
214 #2xx Codes don't raise exceptions
289 sys.stdout.write(_http_ret.title)
215 sys.stdout.write(_http_ret.title)
290
216
291 return 0
217 return 0
292
218
293
219
294 def log_create_repository(repository_dict, created_by, **kwargs):
220 def log_create_repository(repository_dict, created_by, **kwargs):
295 """
221 """
296 Post create repository Hook. This is a dummy function for admins to re-use
222 Post create repository Hook. This is a dummy function for admins to re-use
297 if needed. It's taken from rhodecode-extensions module and executed
223 if needed. It's taken from rhodecode-extensions module and executed
298 if present
224 if present
299
225
300 :param repository: dict dump of repository object
226 :param repository: dict dump of repository object
301 :param created_by: username who created repository
227 :param created_by: username who created repository
302
228
303 available keys of repository_dict:
229 available keys of repository_dict:
304
230
305 'repo_type',
231 'repo_type',
306 'description',
232 'description',
307 'private',
233 'private',
308 'created_on',
234 'created_on',
309 'enable_downloads',
235 'enable_downloads',
310 'repo_id',
236 'repo_id',
311 'user_id',
237 'user_id',
312 'enable_statistics',
238 'enable_statistics',
313 'clone_uri',
239 'clone_uri',
314 'fork_id',
240 'fork_id',
315 'group_id',
241 'group_id',
316 'repo_name'
242 'repo_name'
317
243
318 """
244 """
319 from rhodecode import EXTENSIONS
245 from rhodecode import EXTENSIONS
320 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
246 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
321 if isfunction(callback):
247 if isfunction(callback):
322 kw = {}
248 kw = {}
323 kw.update(repository_dict)
249 kw.update(repository_dict)
324 kw.update({'created_by': created_by})
250 kw.update({'created_by': created_by})
325 kw.update(kwargs)
251 kw.update(kwargs)
326 return callback(**kw)
252 return callback(**kw)
327
253
328 return 0
254 return 0
329
255
330
256
331 def log_delete_repository(repository_dict, deleted_by, **kwargs):
257 def log_delete_repository(repository_dict, deleted_by, **kwargs):
332 """
258 """
333 Post delete repository Hook. This is a dummy function for admins to re-use
259 Post delete repository Hook. This is a dummy function for admins to re-use
334 if needed. It's taken from rhodecode-extensions module and executed
260 if needed. It's taken from rhodecode-extensions module and executed
335 if present
261 if present
336
262
337 :param repository: dict dump of repository object
263 :param repository: dict dump of repository object
338 :param deleted_by: username who deleted the repository
264 :param deleted_by: username who deleted the repository
339
265
340 available keys of repository_dict:
266 available keys of repository_dict:
341
267
342 'repo_type',
268 'repo_type',
343 'description',
269 'description',
344 'private',
270 'private',
345 'created_on',
271 'created_on',
346 'enable_downloads',
272 'enable_downloads',
347 'repo_id',
273 'repo_id',
348 'user_id',
274 'user_id',
349 'enable_statistics',
275 'enable_statistics',
350 'clone_uri',
276 'clone_uri',
351 'fork_id',
277 'fork_id',
352 'group_id',
278 'group_id',
353 'repo_name'
279 'repo_name'
354
280
355 """
281 """
356 from rhodecode import EXTENSIONS
282 from rhodecode import EXTENSIONS
357 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
283 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
358 if isfunction(callback):
284 if isfunction(callback):
359 kw = {}
285 kw = {}
360 kw.update(repository_dict)
286 kw.update(repository_dict)
361 kw.update({'deleted_by': deleted_by,
287 kw.update({'deleted_by': deleted_by,
362 'deleted_on': time.time()})
288 'deleted_on': time.time()})
363 kw.update(kwargs)
289 kw.update(kwargs)
364 return callback(**kw)
290 return callback(**kw)
365
291
366 return 0
292 return 0
367
293
368
294
369 handle_git_pre_receive = (lambda repo_path, revs, env:
295 handle_git_pre_receive = (lambda repo_path, revs, env:
370 handle_git_receive(repo_path, revs, env, hook_type='pre'))
296 handle_git_receive(repo_path, revs, env, hook_type='pre'))
371 handle_git_post_receive = (lambda repo_path, revs, env:
297 handle_git_post_receive = (lambda repo_path, revs, env:
372 handle_git_receive(repo_path, revs, env, hook_type='post'))
298 handle_git_receive(repo_path, revs, env, hook_type='post'))
373
299
374
300
375 def handle_git_receive(repo_path, revs, env, hook_type='post'):
301 def handle_git_receive(repo_path, revs, env, hook_type='post'):
376 """
302 """
377 A really hacky method that is runned by git post-receive hook and logs
303 A really hacky method that is runned by git post-receive hook and logs
378 an push action together with pushed revisions. It's executed by subprocess
304 an push action together with pushed revisions. It's executed by subprocess
379 thus needs all info to be able to create a on the fly pylons enviroment,
305 thus needs all info to be able to create a on the fly pylons enviroment,
380 connect to database and run the logging code. Hacky as sh*t but works.
306 connect to database and run the logging code. Hacky as sh*t but works.
381
307
382 :param repo_path:
308 :param repo_path:
383 :type repo_path:
309 :type repo_path:
384 :param revs:
310 :param revs:
385 :type revs:
311 :type revs:
386 :param env:
312 :param env:
387 :type env:
313 :type env:
388 """
314 """
389 from paste.deploy import appconfig
315 from paste.deploy import appconfig
390 from sqlalchemy import engine_from_config
316 from sqlalchemy import engine_from_config
391 from rhodecode.config.environment import load_environment
317 from rhodecode.config.environment import load_environment
392 from rhodecode.model import init_model
318 from rhodecode.model import init_model
393 from rhodecode.model.db import RhodeCodeUi
319 from rhodecode.model.db import RhodeCodeUi
394 from rhodecode.lib.utils import make_ui
320 from rhodecode.lib.utils import make_ui
395 extras = json.loads(env['RHODECODE_EXTRAS'])
321 extras = json.loads(env['RHODECODE_EXTRAS'])
396
322
397 path, ini_name = os.path.split(extras['config'])
323 path, ini_name = os.path.split(extras['config'])
398 conf = appconfig('config:%s' % ini_name, relative_to=path)
324 conf = appconfig('config:%s' % ini_name, relative_to=path)
399 load_environment(conf.global_conf, conf.local_conf)
325 load_environment(conf.global_conf, conf.local_conf)
400
326
401 engine = engine_from_config(conf, 'sqlalchemy.db1.')
327 engine = engine_from_config(conf, 'sqlalchemy.db1.')
402 init_model(engine)
328 init_model(engine)
403
329
404 baseui = make_ui('db')
330 baseui = make_ui('db')
405 # fix if it's not a bare repo
331 # fix if it's not a bare repo
406 if repo_path.endswith(os.sep + '.git'):
332 if repo_path.endswith(os.sep + '.git'):
407 repo_path = repo_path[:-5]
333 repo_path = repo_path[:-5]
408
334
409 repo = Repository.get_by_full_path(repo_path)
335 repo = Repository.get_by_full_path(repo_path)
410 if not repo:
336 if not repo:
411 raise OSError('Repository %s not found in database'
337 raise OSError('Repository %s not found in database'
412 % (safe_str(repo_path)))
338 % (safe_str(repo_path)))
413
339
414 _hooks = dict(baseui.configitems('hooks')) or {}
340 _hooks = dict(baseui.configitems('hooks')) or {}
415
341
416 for k, v in extras.items():
342 for k, v in extras.items():
417 baseui.setconfig('rhodecode_extras', k, v)
343 baseui.setconfig('rhodecode_extras', k, v)
418 if hook_type == 'pre':
344 if hook_type == 'pre':
419 repo = repo.scm_instance
345 repo = repo.scm_instance
420 else:
346 else:
421 #post push shouldn't use the cached instance never
347 #post push shouldn't use the cached instance never
422 repo = repo.scm_instance_no_cache()
348 repo = repo.scm_instance_no_cache()
423
349
424 repo.ui = baseui
350 repo.ui = baseui
425
351
426 if hook_type == 'pre':
352 if hook_type == 'pre':
427 pre_push(baseui, repo)
353 pre_push(baseui, repo)
428
354
429 # if push hook is enabled via web interface
355 # if push hook is enabled via web interface
430 elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
356 elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
431
357
432 rev_data = []
358 rev_data = []
433 for l in revs:
359 for l in revs:
434 old_rev, new_rev, ref = l.split(' ')
360 old_rev, new_rev, ref = l.split(' ')
435 _ref_data = ref.split('/')
361 _ref_data = ref.split('/')
436 if _ref_data[1] in ['tags', 'heads']:
362 if _ref_data[1] in ['tags', 'heads']:
437 rev_data.append({'old_rev': old_rev,
363 rev_data.append({'old_rev': old_rev,
438 'new_rev': new_rev,
364 'new_rev': new_rev,
439 'ref': ref,
365 'ref': ref,
440 'type': _ref_data[1],
366 'type': _ref_data[1],
441 'name': _ref_data[2].strip()})
367 'name': _ref_data[2].strip()})
442
368
443 git_revs = []
369 git_revs = []
444 for push_ref in rev_data:
370 for push_ref in rev_data:
445 _type = push_ref['type']
371 _type = push_ref['type']
446 if _type == 'heads':
372 if _type == 'heads':
447 if push_ref['old_rev'] == EmptyChangeset().raw_id:
373 if push_ref['old_rev'] == EmptyChangeset().raw_id:
448 cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
374 cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
449 heads = repo.run_git_command(cmd)[0]
375 heads = repo.run_git_command(cmd)[0]
450 heads = heads.replace(push_ref['ref'], '')
376 heads = heads.replace(push_ref['ref'], '')
451 heads = ' '.join(map(lambda c: c.strip('\n').strip(),
377 heads = ' '.join(map(lambda c: c.strip('\n').strip(),
452 heads.splitlines()))
378 heads.splitlines()))
453 cmd = (('log %(new_rev)s' % push_ref) +
379 cmd = (('log %(new_rev)s' % push_ref) +
454 ' --reverse --pretty=format:"%H" --not ' + heads)
380 ' --reverse --pretty=format:"%H" --not ' + heads)
455 git_revs += repo.run_git_command(cmd)[0].splitlines()
381 git_revs += repo.run_git_command(cmd)[0].splitlines()
456
382
457 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
383 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
458 #delete branch case
384 #delete branch case
459 git_revs += ['delete_branch=>%s' % push_ref['name']]
385 git_revs += ['delete_branch=>%s' % push_ref['name']]
460 else:
386 else:
461 cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
387 cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
462 ' --reverse --pretty=format:"%H"')
388 ' --reverse --pretty=format:"%H"')
463 git_revs += repo.run_git_command(cmd)[0].splitlines()
389 git_revs += repo.run_git_command(cmd)[0].splitlines()
464
390
465 elif _type == 'tags':
391 elif _type == 'tags':
466 git_revs += ['tag=>%s' % push_ref['name']]
392 git_revs += ['tag=>%s' % push_ref['name']]
467
393
468 log_push_action(baseui, repo, _git_revs=git_revs)
394 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,203 +1,201 b''
1 import os
1 import os
2 import socket
2 import socket
3 import logging
3 import logging
4 import subprocess
4 import subprocess
5 import traceback
5 import traceback
6
6
7 from webob import Request, Response, exc
7 from webob import Request, Response, exc
8
8
9 import rhodecode
9 import rhodecode
10 from rhodecode.lib import subprocessio
10 from rhodecode.lib import subprocessio
11
11
12 log = logging.getLogger(__name__)
12 log = logging.getLogger(__name__)
13
13
14
14
15 class FileWrapper(object):
15 class FileWrapper(object):
16
16
17 def __init__(self, fd, content_length):
17 def __init__(self, fd, content_length):
18 self.fd = fd
18 self.fd = fd
19 self.content_length = content_length
19 self.content_length = content_length
20 self.remain = content_length
20 self.remain = content_length
21
21
22 def read(self, size):
22 def read(self, size):
23 if size <= self.remain:
23 if size <= self.remain:
24 try:
24 try:
25 data = self.fd.read(size)
25 data = self.fd.read(size)
26 except socket.error:
26 except socket.error:
27 raise IOError(self)
27 raise IOError(self)
28 self.remain -= size
28 self.remain -= size
29 elif self.remain:
29 elif self.remain:
30 data = self.fd.read(self.remain)
30 data = self.fd.read(self.remain)
31 self.remain = 0
31 self.remain = 0
32 else:
32 else:
33 data = None
33 data = None
34 return data
34 return data
35
35
36 def __repr__(self):
36 def __repr__(self):
37 return '<FileWrapper %s len: %s, read: %s>' % (
37 return '<FileWrapper %s len: %s, read: %s>' % (
38 self.fd, self.content_length, self.content_length - self.remain
38 self.fd, self.content_length, self.content_length - self.remain
39 )
39 )
40
40
41
41
42 class GitRepository(object):
42 class GitRepository(object):
43 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
43 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
44 commands = ['git-upload-pack', 'git-receive-pack']
44 commands = ['git-upload-pack', 'git-receive-pack']
45
45
46 def __init__(self, repo_name, content_path, extras):
46 def __init__(self, repo_name, content_path, extras):
47 files = set([f.lower() for f in os.listdir(content_path)])
47 files = set([f.lower() for f in os.listdir(content_path)])
48 if not (self.git_folder_signature.intersection(files)
48 if not (self.git_folder_signature.intersection(files)
49 == self.git_folder_signature):
49 == self.git_folder_signature):
50 raise OSError('%s missing git signature' % content_path)
50 raise OSError('%s missing git signature' % content_path)
51 self.content_path = content_path
51 self.content_path = content_path
52 self.valid_accepts = ['application/x-%s-result' %
52 self.valid_accepts = ['application/x-%s-result' %
53 c for c in self.commands]
53 c for c in self.commands]
54 self.repo_name = repo_name
54 self.repo_name = repo_name
55 self.extras = extras
55 self.extras = extras
56
56
57 def _get_fixedpath(self, path):
57 def _get_fixedpath(self, path):
58 """
58 """
59 Small fix for repo_path
59 Small fix for repo_path
60
60
61 :param path:
61 :param path:
62 :type path:
62 :type path:
63 """
63 """
64 return path.split(self.repo_name, 1)[-1].strip('/')
64 return path.split(self.repo_name, 1)[-1].strip('/')
65
65
66 def inforefs(self, request, environ):
66 def inforefs(self, request, environ):
67 """
67 """
68 WSGI Response producer for HTTP GET Git Smart
68 WSGI Response producer for HTTP GET Git Smart
69 HTTP /info/refs request.
69 HTTP /info/refs request.
70 """
70 """
71
71
72 git_command = request.GET.get('service')
72 git_command = request.GET.get('service')
73 if git_command not in self.commands:
73 if git_command not in self.commands:
74 log.debug('command %s not allowed' % git_command)
74 log.debug('command %s not allowed' % git_command)
75 return exc.HTTPMethodNotAllowed()
75 return exc.HTTPMethodNotAllowed()
76
76
77 # note to self:
77 # note to self:
78 # please, resist the urge to add '\n' to git capture and increment
78 # please, resist the urge to add '\n' to git capture and increment
79 # line count by 1.
79 # line count by 1.
80 # The code in Git client not only does NOT need '\n', but actually
80 # The code in Git client not only does NOT need '\n', but actually
81 # blows up if you sprinkle "flush" (0000) as "0001\n".
81 # blows up if you sprinkle "flush" (0000) as "0001\n".
82 # It reads binary, per number of bytes specified.
82 # It reads binary, per number of bytes specified.
83 # if you do add '\n' as part of data, count it.
83 # if you do add '\n' as part of data, count it.
84 server_advert = '# service=%s' % git_command
84 server_advert = '# service=%s' % git_command
85 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
85 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
86 _git_path = rhodecode.CONFIG.get('git_path', 'git')
86 _git_path = rhodecode.CONFIG.get('git_path', 'git')
87 try:
87 try:
88 out = subprocessio.SubprocessIOChunker(
88 out = subprocessio.SubprocessIOChunker(
89 r'%s %s --stateless-rpc --advertise-refs "%s"' % (
89 r'%s %s --stateless-rpc --advertise-refs "%s"' % (
90 _git_path, git_command[4:], self.content_path),
90 _git_path, git_command[4:], self.content_path),
91 starting_values=[
91 starting_values=[
92 packet_len + server_advert + '0000'
92 packet_len + server_advert + '0000'
93 ]
93 ]
94 )
94 )
95 except EnvironmentError, e:
95 except EnvironmentError, e:
96 log.error(traceback.format_exc())
96 log.error(traceback.format_exc())
97 raise exc.HTTPExpectationFailed()
97 raise exc.HTTPExpectationFailed()
98 resp = Response()
98 resp = Response()
99 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
99 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
100 resp.charset = None
100 resp.charset = None
101 resp.app_iter = out
101 resp.app_iter = out
102 return resp
102 return resp
103
103
104 def backend(self, request, environ):
104 def backend(self, request, environ):
105 """
105 """
106 WSGI Response producer for HTTP POST Git Smart HTTP requests.
106 WSGI Response producer for HTTP POST Git Smart HTTP requests.
107 Reads commands and data from HTTP POST's body.
107 Reads commands and data from HTTP POST's body.
108 returns an iterator obj with contents of git command's
108 returns an iterator obj with contents of git command's
109 response to stdout
109 response to stdout
110 """
110 """
111 git_command = self._get_fixedpath(request.path_info)
111 git_command = self._get_fixedpath(request.path_info)
112 if git_command not in self.commands:
112 if git_command not in self.commands:
113 log.debug('command %s not allowed' % git_command)
113 log.debug('command %s not allowed' % git_command)
114 return exc.HTTPMethodNotAllowed()
114 return exc.HTTPMethodNotAllowed()
115
115
116 if 'CONTENT_LENGTH' in environ:
116 if 'CONTENT_LENGTH' in environ:
117 inputstream = FileWrapper(environ['wsgi.input'],
117 inputstream = FileWrapper(environ['wsgi.input'],
118 request.content_length)
118 request.content_length)
119 else:
119 else:
120 inputstream = environ['wsgi.input']
120 inputstream = environ['wsgi.input']
121
121
122 try:
122 try:
123 gitenv = os.environ
123 gitenv = os.environ
124 from rhodecode.lib.compat import json
125 gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras)
126 # forget all configs
124 # forget all configs
127 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
125 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
128 opts = dict(
126 opts = dict(
129 env=gitenv,
127 env=gitenv,
130 cwd=os.getcwd()
128 cwd=os.getcwd()
131 )
129 )
132 cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
130 cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
133 self.content_path),
131 self.content_path),
134 log.debug('handling cmd %s' % cmd)
132 log.debug('handling cmd %s' % cmd)
135 out = subprocessio.SubprocessIOChunker(
133 out = subprocessio.SubprocessIOChunker(
136 cmd,
134 cmd,
137 inputstream=inputstream,
135 inputstream=inputstream,
138 **opts
136 **opts
139 )
137 )
140 except EnvironmentError, e:
138 except EnvironmentError, e:
141 log.error(traceback.format_exc())
139 log.error(traceback.format_exc())
142 raise exc.HTTPExpectationFailed()
140 raise exc.HTTPExpectationFailed()
143
141
144 if git_command in [u'git-receive-pack']:
142 if git_command in [u'git-receive-pack']:
145 # updating refs manually after each push.
143 # updating refs manually after each push.
146 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
144 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
147 _git_path = rhodecode.CONFIG.get('git_path', 'git')
145 _git_path = rhodecode.CONFIG.get('git_path', 'git')
148 cmd = (u'%s --git-dir "%s" '
146 cmd = (u'%s --git-dir "%s" '
149 'update-server-info' % (_git_path, self.content_path))
147 'update-server-info' % (_git_path, self.content_path))
150 log.debug('handling cmd %s' % cmd)
148 log.debug('handling cmd %s' % cmd)
151 subprocess.call(cmd, shell=True)
149 subprocess.call(cmd, shell=True)
152
150
153 resp = Response()
151 resp = Response()
154 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
152 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
155 resp.charset = None
153 resp.charset = None
156 resp.app_iter = out
154 resp.app_iter = out
157 return resp
155 return resp
158
156
159 def __call__(self, environ, start_response):
157 def __call__(self, environ, start_response):
160 request = Request(environ)
158 request = Request(environ)
161 _path = self._get_fixedpath(request.path_info)
159 _path = self._get_fixedpath(request.path_info)
162 if _path.startswith('info/refs'):
160 if _path.startswith('info/refs'):
163 app = self.inforefs
161 app = self.inforefs
164 elif [a for a in self.valid_accepts if a in request.accept]:
162 elif [a for a in self.valid_accepts if a in request.accept]:
165 app = self.backend
163 app = self.backend
166 try:
164 try:
167 resp = app(request, environ)
165 resp = app(request, environ)
168 except exc.HTTPException, e:
166 except exc.HTTPException, e:
169 resp = e
167 resp = e
170 log.error(traceback.format_exc())
168 log.error(traceback.format_exc())
171 except Exception, e:
169 except Exception, e:
172 log.error(traceback.format_exc())
170 log.error(traceback.format_exc())
173 resp = exc.HTTPInternalServerError()
171 resp = exc.HTTPInternalServerError()
174 return resp(environ, start_response)
172 return resp(environ, start_response)
175
173
176
174
177 class GitDirectory(object):
175 class GitDirectory(object):
178
176
179 def __init__(self, repo_root, repo_name, extras):
177 def __init__(self, repo_root, repo_name, extras):
180 repo_location = os.path.join(repo_root, repo_name)
178 repo_location = os.path.join(repo_root, repo_name)
181 if not os.path.isdir(repo_location):
179 if not os.path.isdir(repo_location):
182 raise OSError(repo_location)
180 raise OSError(repo_location)
183
181
184 self.content_path = repo_location
182 self.content_path = repo_location
185 self.repo_name = repo_name
183 self.repo_name = repo_name
186 self.repo_location = repo_location
184 self.repo_location = repo_location
187 self.extras = extras
185 self.extras = extras
188
186
189 def __call__(self, environ, start_response):
187 def __call__(self, environ, start_response):
190 content_path = self.content_path
188 content_path = self.content_path
191 try:
189 try:
192 app = GitRepository(self.repo_name, content_path, self.extras)
190 app = GitRepository(self.repo_name, content_path, self.extras)
193 except (AssertionError, OSError):
191 except (AssertionError, OSError):
194 content_path = os.path.join(content_path, '.git')
192 content_path = os.path.join(content_path, '.git')
195 if os.path.isdir(content_path):
193 if os.path.isdir(content_path):
196 app = GitRepository(self.repo_name, content_path, self.extras)
194 app = GitRepository(self.repo_name, content_path, self.extras)
197 else:
195 else:
198 return exc.HTTPNotFound()(environ, start_response)
196 return exc.HTTPNotFound()(environ, start_response)
199 return app(environ, start_response)
197 return app(environ, start_response)
200
198
201
199
202 def make_wsgi_app(repo_name, repo_root, extras):
200 def make_wsgi_app(repo_name, repo_root, extras):
203 return GitDirectory(repo_root, repo_name, extras)
201 return GitDirectory(repo_root, repo_name, extras)
@@ -1,342 +1,337 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplegit
3 rhodecode.lib.middleware.simplegit
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 It's implemented with basic auth function
7 It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import re
28 import re
29 import logging
29 import logging
30 import traceback
30 import traceback
31
31
32 from dulwich import server as dulserver
32 from dulwich import server as dulserver
33 from dulwich.web import LimitedInputFilter, GunzipFilter
33 from dulwich.web import LimitedInputFilter, GunzipFilter
34 from rhodecode.lib.exceptions import HTTPLockedRC
34 from rhodecode.lib.exceptions import HTTPLockedRC
35 from rhodecode.lib.hooks import pre_pull
35 from rhodecode.lib.hooks import pre_pull
36
36
37
37
38 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
38 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
39
39
40 def handle(self):
40 def handle(self):
41 write = lambda x: self.proto.write_sideband(1, x)
41 write = lambda x: self.proto.write_sideband(1, x)
42
42
43 graph_walker = dulserver.ProtocolGraphWalker(self,
43 graph_walker = dulserver.ProtocolGraphWalker(self,
44 self.repo.object_store,
44 self.repo.object_store,
45 self.repo.get_peeled)
45 self.repo.get_peeled)
46 objects_iter = self.repo.fetch_objects(
46 objects_iter = self.repo.fetch_objects(
47 graph_walker.determine_wants, graph_walker, self.progress,
47 graph_walker.determine_wants, graph_walker, self.progress,
48 get_tagged=self.get_tagged)
48 get_tagged=self.get_tagged)
49
49
50 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
50 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
51 # that the client still expects a 0-object pack in most cases.
51 # that the client still expects a 0-object pack in most cases.
52 if objects_iter is None:
52 if objects_iter is None:
53 return
53 return
54
54
55 self.progress("counting objects: %d, done.\n" % len(objects_iter))
55 self.progress("counting objects: %d, done.\n" % len(objects_iter))
56 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
56 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
57 objects_iter)
57 objects_iter)
58 messages = []
58 messages = []
59 messages.append('thank you for using rhodecode')
59 messages.append('thank you for using rhodecode')
60
60
61 for msg in messages:
61 for msg in messages:
62 self.progress(msg + "\n")
62 self.progress(msg + "\n")
63 # we are done
63 # we are done
64 self.proto.write("0000")
64 self.proto.write("0000")
65
65
66
66
67 dulserver.DEFAULT_HANDLERS = {
67 dulserver.DEFAULT_HANDLERS = {
68 #git-ls-remote, git-clone, git-fetch and git-pull
68 #git-ls-remote, git-clone, git-fetch and git-pull
69 'git-upload-pack': SimpleGitUploadPackHandler,
69 'git-upload-pack': SimpleGitUploadPackHandler,
70 #git-push
70 #git-push
71 'git-receive-pack': dulserver.ReceivePackHandler,
71 'git-receive-pack': dulserver.ReceivePackHandler,
72 }
72 }
73
73
74 # not used for now until dulwich get's fixed
74 # not used for now until dulwich get's fixed
75 #from dulwich.repo import Repo
75 #from dulwich.repo import Repo
76 #from dulwich.web import make_wsgi_chain
76 #from dulwich.web import make_wsgi_chain
77
77
78 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
78 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
80 HTTPBadRequest, HTTPNotAcceptable
80 HTTPBadRequest, HTTPNotAcceptable
81
81
82 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url
82 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url,\
83 _set_extras
83 from rhodecode.lib.base import BaseVCSController
84 from rhodecode.lib.base import BaseVCSController
84 from rhodecode.lib.auth import get_container_username
85 from rhodecode.lib.auth import get_container_username
85 from rhodecode.lib.utils import is_valid_repo, make_ui
86 from rhodecode.lib.utils import is_valid_repo, make_ui
86 from rhodecode.lib.compat import json
87 from rhodecode.lib.compat import json
87 from rhodecode.model.db import User, RhodeCodeUi
88 from rhodecode.model.db import User, RhodeCodeUi
88
89
89 log = logging.getLogger(__name__)
90 log = logging.getLogger(__name__)
90
91
91
92
92 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
93 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
93
94
94
95
95 def is_git(environ):
96 def is_git(environ):
96 path_info = environ['PATH_INFO']
97 path_info = environ['PATH_INFO']
97 isgit_path = GIT_PROTO_PAT.match(path_info)
98 isgit_path = GIT_PROTO_PAT.match(path_info)
98 log.debug('pathinfo: %s detected as GIT %s' % (
99 log.debug('pathinfo: %s detected as GIT %s' % (
99 path_info, isgit_path != None)
100 path_info, isgit_path != None)
100 )
101 )
101 return isgit_path
102 return isgit_path
102
103
103
104
104 class SimpleGit(BaseVCSController):
105 class SimpleGit(BaseVCSController):
105
106
106 def _handle_request(self, environ, start_response):
107 def _handle_request(self, environ, start_response):
107 if not is_git(environ):
108 if not is_git(environ):
108 return self.application(environ, start_response)
109 return self.application(environ, start_response)
109 if not self._check_ssl(environ, start_response):
110 if not self._check_ssl(environ, start_response):
110 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
111 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
111
112
112 ip_addr = self._get_ip_addr(environ)
113 ip_addr = self._get_ip_addr(environ)
113 username = None
114 username = None
114 self._git_first_op = False
115 self._git_first_op = False
115 # skip passing error to error controller
116 # skip passing error to error controller
116 environ['pylons.status_code_redirect'] = True
117 environ['pylons.status_code_redirect'] = True
117
118
118 #======================================================================
119 #======================================================================
119 # EXTRACT REPOSITORY NAME FROM ENV
120 # EXTRACT REPOSITORY NAME FROM ENV
120 #======================================================================
121 #======================================================================
121 try:
122 try:
122 repo_name = self.__get_repository(environ)
123 repo_name = self.__get_repository(environ)
123 log.debug('Extracted repo name is %s' % repo_name)
124 log.debug('Extracted repo name is %s' % repo_name)
124 except:
125 except:
125 return HTTPInternalServerError()(environ, start_response)
126 return HTTPInternalServerError()(environ, start_response)
126
127
127 # quick check if that dir exists...
128 # quick check if that dir exists...
128 if is_valid_repo(repo_name, self.basepath, 'git') is False:
129 if is_valid_repo(repo_name, self.basepath, 'git') is False:
129 return HTTPNotFound()(environ, start_response)
130 return HTTPNotFound()(environ, start_response)
130
131
131 #======================================================================
132 #======================================================================
132 # GET ACTION PULL or PUSH
133 # GET ACTION PULL or PUSH
133 #======================================================================
134 #======================================================================
134 action = self.__get_action(environ)
135 action = self.__get_action(environ)
135
136
136 #======================================================================
137 #======================================================================
137 # CHECK ANONYMOUS PERMISSION
138 # CHECK ANONYMOUS PERMISSION
138 #======================================================================
139 #======================================================================
139 if action in ['pull', 'push']:
140 if action in ['pull', 'push']:
140 anonymous_user = self.__get_user('default')
141 anonymous_user = self.__get_user('default')
141 username = anonymous_user.username
142 username = anonymous_user.username
142 anonymous_perm = self._check_permission(action, anonymous_user,
143 anonymous_perm = self._check_permission(action, anonymous_user,
143 repo_name, ip_addr)
144 repo_name, ip_addr)
144
145
145 if anonymous_perm is not True or anonymous_user.active is False:
146 if anonymous_perm is not True or anonymous_user.active is False:
146 if anonymous_perm is not True:
147 if anonymous_perm is not True:
147 log.debug('Not enough credentials to access this '
148 log.debug('Not enough credentials to access this '
148 'repository as anonymous user')
149 'repository as anonymous user')
149 if anonymous_user.active is False:
150 if anonymous_user.active is False:
150 log.debug('Anonymous access is disabled, running '
151 log.debug('Anonymous access is disabled, running '
151 'authentication')
152 'authentication')
152 #==============================================================
153 #==============================================================
153 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
154 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
154 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
155 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
155 #==============================================================
156 #==============================================================
156
157
157 # Attempting to retrieve username from the container
158 # Attempting to retrieve username from the container
158 username = get_container_username(environ, self.config)
159 username = get_container_username(environ, self.config)
159
160
160 # If not authenticated by the container, running basic auth
161 # If not authenticated by the container, running basic auth
161 if not username:
162 if not username:
162 self.authenticate.realm = \
163 self.authenticate.realm = \
163 safe_str(self.config['rhodecode_realm'])
164 safe_str(self.config['rhodecode_realm'])
164 result = self.authenticate(environ)
165 result = self.authenticate(environ)
165 if isinstance(result, str):
166 if isinstance(result, str):
166 AUTH_TYPE.update(environ, 'basic')
167 AUTH_TYPE.update(environ, 'basic')
167 REMOTE_USER.update(environ, result)
168 REMOTE_USER.update(environ, result)
168 username = result
169 username = result
169 else:
170 else:
170 return result.wsgi_application(environ, start_response)
171 return result.wsgi_application(environ, start_response)
171
172
172 #==============================================================
173 #==============================================================
173 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
174 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
174 #==============================================================
175 #==============================================================
175 try:
176 try:
176 user = self.__get_user(username)
177 user = self.__get_user(username)
177 if user is None or not user.active:
178 if user is None or not user.active:
178 return HTTPForbidden()(environ, start_response)
179 return HTTPForbidden()(environ, start_response)
179 username = user.username
180 username = user.username
180 except:
181 except:
181 log.error(traceback.format_exc())
182 log.error(traceback.format_exc())
182 return HTTPInternalServerError()(environ, start_response)
183 return HTTPInternalServerError()(environ, start_response)
183
184
184 #check permissions for this repository
185 #check permissions for this repository
185 perm = self._check_permission(action, user, repo_name, ip_addr)
186 perm = self._check_permission(action, user, repo_name, ip_addr)
186 if perm is not True:
187 if perm is not True:
187 return HTTPForbidden()(environ, start_response)
188 return HTTPForbidden()(environ, start_response)
188
189
189 # extras are injected into UI object and later available
190 # extras are injected into UI object and later available
190 # in hooks executed by rhodecode
191 # in hooks executed by rhodecode
191 from rhodecode import CONFIG
192 from rhodecode import CONFIG
192 server_url = get_server_url(environ)
193 server_url = get_server_url(environ)
193 extras = {
194 extras = {
194 'ip': ip_addr,
195 'ip': ip_addr,
195 'username': username,
196 'username': username,
196 'action': action,
197 'action': action,
197 'repository': repo_name,
198 'repository': repo_name,
198 'scm': 'git',
199 'scm': 'git',
199 'config': CONFIG['__file__'],
200 'config': CONFIG['__file__'],
200 'server_url': server_url,
201 'server_url': server_url,
201 'make_lock': None,
202 'make_lock': None,
202 'locked_by': [None, None]
203 'locked_by': [None, None]
203 }
204 }
204
205
205 #===================================================================
206 #===================================================================
206 # GIT REQUEST HANDLING
207 # GIT REQUEST HANDLING
207 #===================================================================
208 #===================================================================
208 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
209 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
209 log.debug('Repository path is %s' % repo_path)
210 log.debug('Repository path is %s' % repo_path)
210
211
211 # CHECK LOCKING only if it's not ANONYMOUS USER
212 # CHECK LOCKING only if it's not ANONYMOUS USER
212 if username != User.DEFAULT_USER:
213 if username != User.DEFAULT_USER:
213 log.debug('Checking locking on repository')
214 log.debug('Checking locking on repository')
214 (make_lock,
215 (make_lock,
215 locked,
216 locked,
216 locked_by) = self._check_locking_state(
217 locked_by) = self._check_locking_state(
217 environ=environ, action=action,
218 environ=environ, action=action,
218 repo=repo_name, user_id=user.user_id
219 repo=repo_name, user_id=user.user_id
219 )
220 )
220 # store the make_lock for later evaluation in hooks
221 # store the make_lock for later evaluation in hooks
221 extras.update({'make_lock': make_lock,
222 extras.update({'make_lock': make_lock,
222 'locked_by': locked_by})
223 'locked_by': locked_by})
223 # set the environ variables for this request
224 # set the environ variables for this request
224 os.environ['RC_SCM_DATA'] = json.dumps(extras)
225 os.environ['RC_SCM_DATA'] = json.dumps(extras)
225 fix_PATH()
226 fix_PATH()
226 log.debug('HOOKS extras is %s' % extras)
227 log.debug('HOOKS extras is %s' % extras)
227 baseui = make_ui('db')
228 baseui = make_ui('db')
228 self.__inject_extras(repo_path, baseui, extras)
229 self.__inject_extras(repo_path, baseui, extras)
229
230
230 try:
231 try:
231 self._handle_githooks(repo_name, action, baseui, environ)
232 self._handle_githooks(repo_name, action, baseui, environ)
232 log.info('%s action on GIT repo "%s" by "%s" from %s' %
233 log.info('%s action on GIT repo "%s" by "%s" from %s' %
233 (action, repo_name, username, ip_addr))
234 (action, repo_name, username, ip_addr))
234 app = self.__make_app(repo_name, repo_path, extras)
235 app = self.__make_app(repo_name, repo_path, extras)
235 return app(environ, start_response)
236 return app(environ, start_response)
236 except HTTPLockedRC, e:
237 except HTTPLockedRC, e:
237 _code = CONFIG.get('lock_ret_code')
238 _code = CONFIG.get('lock_ret_code')
238 log.debug('Repository LOCKED ret code %s!' % (_code))
239 log.debug('Repository LOCKED ret code %s!' % (_code))
239 return e(environ, start_response)
240 return e(environ, start_response)
240 except Exception:
241 except Exception:
241 log.error(traceback.format_exc())
242 log.error(traceback.format_exc())
242 return HTTPInternalServerError()(environ, start_response)
243 return HTTPInternalServerError()(environ, start_response)
243 finally:
244 finally:
244 # invalidate cache on push
245 # invalidate cache on push
245 if action == 'push':
246 if action == 'push':
246 self._invalidate_cache(repo_name)
247 self._invalidate_cache(repo_name)
247
248
248 def __make_app(self, repo_name, repo_path, extras):
249 def __make_app(self, repo_name, repo_path, extras):
249 """
250 """
250 Make an wsgi application using dulserver
251 Make an wsgi application using dulserver
251
252
252 :param repo_name: name of the repository
253 :param repo_name: name of the repository
253 :param repo_path: full path to the repository
254 :param repo_path: full path to the repository
254 """
255 """
255
256
256 from rhodecode.lib.middleware.pygrack import make_wsgi_app
257 from rhodecode.lib.middleware.pygrack import make_wsgi_app
257 app = make_wsgi_app(
258 app = make_wsgi_app(
258 repo_root=safe_str(self.basepath),
259 repo_root=safe_str(self.basepath),
259 repo_name=repo_name,
260 repo_name=repo_name,
260 extras=extras,
261 extras=extras,
261 )
262 )
262 app = GunzipFilter(LimitedInputFilter(app))
263 app = GunzipFilter(LimitedInputFilter(app))
263 return app
264 return app
264
265
265 def __get_repository(self, environ):
266 def __get_repository(self, environ):
266 """
267 """
267 Get's repository name out of PATH_INFO header
268 Get's repository name out of PATH_INFO header
268
269
269 :param environ: environ where PATH_INFO is stored
270 :param environ: environ where PATH_INFO is stored
270 """
271 """
271 try:
272 try:
272 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
273 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
273 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
274 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
274 except:
275 except:
275 log.error(traceback.format_exc())
276 log.error(traceback.format_exc())
276 raise
277 raise
277
278
278 return repo_name
279 return repo_name
279
280
280 def __get_user(self, username):
281 def __get_user(self, username):
281 return User.get_by_username(username)
282 return User.get_by_username(username)
282
283
283 def __get_action(self, environ):
284 def __get_action(self, environ):
284 """
285 """
285 Maps git request commands into a pull or push command.
286 Maps git request commands into a pull or push command.
286
287
287 :param environ:
288 :param environ:
288 """
289 """
289 service = environ['QUERY_STRING'].split('=')
290 service = environ['QUERY_STRING'].split('=')
290
291
291 if len(service) > 1:
292 if len(service) > 1:
292 service_cmd = service[1]
293 service_cmd = service[1]
293 mapping = {
294 mapping = {
294 'git-receive-pack': 'push',
295 'git-receive-pack': 'push',
295 'git-upload-pack': 'pull',
296 'git-upload-pack': 'pull',
296 }
297 }
297 op = mapping[service_cmd]
298 op = mapping[service_cmd]
298 self._git_stored_op = op
299 self._git_stored_op = op
299 return op
300 return op
300 else:
301 else:
301 # try to fallback to stored variable as we don't know if the last
302 # try to fallback to stored variable as we don't know if the last
302 # operation is pull/push
303 # operation is pull/push
303 op = getattr(self, '_git_stored_op', 'pull')
304 op = getattr(self, '_git_stored_op', 'pull')
304 return op
305 return op
305
306
306 def _handle_githooks(self, repo_name, action, baseui, environ):
307 def _handle_githooks(self, repo_name, action, baseui, environ):
307 """
308 """
308 Handles pull action, push is handled by post-receive hook
309 Handles pull action, push is handled by post-receive hook
309 """
310 """
310 from rhodecode.lib.hooks import log_pull_action
311 from rhodecode.lib.hooks import log_pull_action
311 service = environ['QUERY_STRING'].split('=')
312 service = environ['QUERY_STRING'].split('=')
312
313
313 if len(service) < 2:
314 if len(service) < 2:
314 return
315 return
315
316
316 from rhodecode.model.db import Repository
317 from rhodecode.model.db import Repository
317 _repo = Repository.get_by_repo_name(repo_name)
318 _repo = Repository.get_by_repo_name(repo_name)
318 _repo = _repo.scm_instance
319 _repo = _repo.scm_instance
319 _repo._repo.ui = baseui
320 _repo._repo.ui = baseui
320
321
321 _hooks = dict(baseui.configitems('hooks')) or {}
322 _hooks = dict(baseui.configitems('hooks')) or {}
322 if action == 'pull':
323 if action == 'pull':
323 # stupid git, emulate pre-pull hook !
324 # stupid git, emulate pre-pull hook !
324 pre_pull(ui=baseui, repo=_repo._repo)
325 pre_pull(ui=baseui, repo=_repo._repo)
325 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
326 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
326 log_pull_action(ui=baseui, repo=_repo._repo)
327 log_pull_action(ui=baseui, repo=_repo._repo)
327
328
328 def __inject_extras(self, repo_path, baseui, extras={}):
329 def __inject_extras(self, repo_path, baseui, extras={}):
329 """
330 """
330 Injects some extra params into baseui instance
331 Injects some extra params into baseui instance
331
332
332 :param baseui: baseui instance
333 :param baseui: baseui instance
333 :param extras: dict with extra params to put into baseui
334 :param extras: dict with extra params to put into baseui
334 """
335 """
335
336
336 # make our hgweb quiet so it doesn't print output
337 _set_extras(extras)
337 baseui.setconfig('ui', 'quiet', 'true')
338
339 #inject some additional parameters that will be available in ui
340 #for hooks
341 for k, v in extras.items():
342 baseui.setconfig('rhodecode_extras', k, v)
@@ -1,290 +1,287 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplehg
3 rhodecode.lib.middleware.simplehg
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleHG middleware for handling mercurial protocol request
6 SimpleHG middleware for handling mercurial protocol request
7 (push/clone etc.). It's implemented with basic auth function
7 (push/clone etc.). It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 from mercurial.error import RepoError
31 from mercurial.error import RepoError
32 from mercurial.hgweb import hgweb_mod
32 from mercurial.hgweb import hgweb_mod
33
33
34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
35 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
36 HTTPBadRequest, HTTPNotAcceptable
36 HTTPBadRequest, HTTPNotAcceptable
37
37
38 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url
38 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url,\
39 _set_extras
39 from rhodecode.lib.base import BaseVCSController
40 from rhodecode.lib.base import BaseVCSController
40 from rhodecode.lib.auth import get_container_username
41 from rhodecode.lib.auth import get_container_username
41 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
42 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
42 from rhodecode.lib.compat import json
43 from rhodecode.lib.compat import json
43 from rhodecode.model.db import User
44 from rhodecode.model.db import User
44 from rhodecode.lib.exceptions import HTTPLockedRC
45 from rhodecode.lib.exceptions import HTTPLockedRC
45
46
46
47
47 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
48
49
49
50
50 def is_mercurial(environ):
51 def is_mercurial(environ):
51 """
52 """
52 Returns True if request's target is mercurial server - header
53 Returns True if request's target is mercurial server - header
53 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 """
55 """
55 http_accept = environ.get('HTTP_ACCEPT')
56 http_accept = environ.get('HTTP_ACCEPT')
56 path_info = environ['PATH_INFO']
57 path_info = environ['PATH_INFO']
57 if http_accept and http_accept.startswith('application/mercurial'):
58 if http_accept and http_accept.startswith('application/mercurial'):
58 ishg_path = True
59 ishg_path = True
59 else:
60 else:
60 ishg_path = False
61 ishg_path = False
61
62
62 log.debug('pathinfo: %s detected as HG %s' % (
63 log.debug('pathinfo: %s detected as HG %s' % (
63 path_info, ishg_path)
64 path_info, ishg_path)
64 )
65 )
65 return ishg_path
66 return ishg_path
66
67
67
68
68 class SimpleHg(BaseVCSController):
69 class SimpleHg(BaseVCSController):
69
70
70 def _handle_request(self, environ, start_response):
71 def _handle_request(self, environ, start_response):
71 if not is_mercurial(environ):
72 if not is_mercurial(environ):
72 return self.application(environ, start_response)
73 return self.application(environ, start_response)
73 if not self._check_ssl(environ, start_response):
74 if not self._check_ssl(environ, start_response):
74 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
75 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
75
76
76 ip_addr = self._get_ip_addr(environ)
77 ip_addr = self._get_ip_addr(environ)
77 username = None
78 username = None
78 # skip passing error to error controller
79 # skip passing error to error controller
79 environ['pylons.status_code_redirect'] = True
80 environ['pylons.status_code_redirect'] = True
80
81
81 #======================================================================
82 #======================================================================
82 # EXTRACT REPOSITORY NAME FROM ENV
83 # EXTRACT REPOSITORY NAME FROM ENV
83 #======================================================================
84 #======================================================================
84 try:
85 try:
85 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
86 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
86 log.debug('Extracted repo name is %s' % repo_name)
87 log.debug('Extracted repo name is %s' % repo_name)
87 except:
88 except:
88 return HTTPInternalServerError()(environ, start_response)
89 return HTTPInternalServerError()(environ, start_response)
89
90
90 # quick check if that dir exists...
91 # quick check if that dir exists...
91 if is_valid_repo(repo_name, self.basepath, 'hg') is False:
92 if is_valid_repo(repo_name, self.basepath, 'hg') is False:
92 return HTTPNotFound()(environ, start_response)
93 return HTTPNotFound()(environ, start_response)
93
94
94 #======================================================================
95 #======================================================================
95 # GET ACTION PULL or PUSH
96 # GET ACTION PULL or PUSH
96 #======================================================================
97 #======================================================================
97 action = self.__get_action(environ)
98 action = self.__get_action(environ)
98
99
99 #======================================================================
100 #======================================================================
100 # CHECK ANONYMOUS PERMISSION
101 # CHECK ANONYMOUS PERMISSION
101 #======================================================================
102 #======================================================================
102 if action in ['pull', 'push']:
103 if action in ['pull', 'push']:
103 anonymous_user = self.__get_user('default')
104 anonymous_user = self.__get_user('default')
104 username = anonymous_user.username
105 username = anonymous_user.username
105 anonymous_perm = self._check_permission(action, anonymous_user,
106 anonymous_perm = self._check_permission(action, anonymous_user,
106 repo_name, ip_addr)
107 repo_name, ip_addr)
107
108
108 if anonymous_perm is not True or anonymous_user.active is False:
109 if anonymous_perm is not True or anonymous_user.active is False:
109 if anonymous_perm is not True:
110 if anonymous_perm is not True:
110 log.debug('Not enough credentials to access this '
111 log.debug('Not enough credentials to access this '
111 'repository as anonymous user')
112 'repository as anonymous user')
112 if anonymous_user.active is False:
113 if anonymous_user.active is False:
113 log.debug('Anonymous access is disabled, running '
114 log.debug('Anonymous access is disabled, running '
114 'authentication')
115 'authentication')
115 #==============================================================
116 #==============================================================
116 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
117 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
117 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
118 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
118 #==============================================================
119 #==============================================================
119
120
120 # Attempting to retrieve username from the container
121 # Attempting to retrieve username from the container
121 username = get_container_username(environ, self.config)
122 username = get_container_username(environ, self.config)
122
123
123 # If not authenticated by the container, running basic auth
124 # If not authenticated by the container, running basic auth
124 if not username:
125 if not username:
125 self.authenticate.realm = \
126 self.authenticate.realm = \
126 safe_str(self.config['rhodecode_realm'])
127 safe_str(self.config['rhodecode_realm'])
127 result = self.authenticate(environ)
128 result = self.authenticate(environ)
128 if isinstance(result, str):
129 if isinstance(result, str):
129 AUTH_TYPE.update(environ, 'basic')
130 AUTH_TYPE.update(environ, 'basic')
130 REMOTE_USER.update(environ, result)
131 REMOTE_USER.update(environ, result)
131 username = result
132 username = result
132 else:
133 else:
133 return result.wsgi_application(environ, start_response)
134 return result.wsgi_application(environ, start_response)
134
135
135 #==============================================================
136 #==============================================================
136 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
137 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
137 #==============================================================
138 #==============================================================
138 try:
139 try:
139 user = self.__get_user(username)
140 user = self.__get_user(username)
140 if user is None or not user.active:
141 if user is None or not user.active:
141 return HTTPForbidden()(environ, start_response)
142 return HTTPForbidden()(environ, start_response)
142 username = user.username
143 username = user.username
143 except:
144 except:
144 log.error(traceback.format_exc())
145 log.error(traceback.format_exc())
145 return HTTPInternalServerError()(environ, start_response)
146 return HTTPInternalServerError()(environ, start_response)
146
147
147 #check permissions for this repository
148 #check permissions for this repository
148 perm = self._check_permission(action, user, repo_name, ip_addr)
149 perm = self._check_permission(action, user, repo_name, ip_addr)
149 if perm is not True:
150 if perm is not True:
150 return HTTPForbidden()(environ, start_response)
151 return HTTPForbidden()(environ, start_response)
151
152
152 # extras are injected into mercurial UI object and later available
153 # extras are injected into mercurial UI object and later available
153 # in hg hooks executed by rhodecode
154 # in hg hooks executed by rhodecode
154 from rhodecode import CONFIG
155 from rhodecode import CONFIG
155 server_url = get_server_url(environ)
156 server_url = get_server_url(environ)
156 extras = {
157 extras = {
157 'ip': ip_addr,
158 'ip': ip_addr,
158 'username': username,
159 'username': username,
159 'action': action,
160 'action': action,
160 'repository': repo_name,
161 'repository': repo_name,
161 'scm': 'hg',
162 'scm': 'hg',
162 'config': CONFIG['__file__'],
163 'config': CONFIG['__file__'],
163 'server_url': server_url,
164 'server_url': server_url,
164 'make_lock': None,
165 'make_lock': None,
165 'locked_by': [None, None]
166 'locked_by': [None, None]
166 }
167 }
167 #======================================================================
168 #======================================================================
168 # MERCURIAL REQUEST HANDLING
169 # MERCURIAL REQUEST HANDLING
169 #======================================================================
170 #======================================================================
170 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
171 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
171 log.debug('Repository path is %s' % repo_path)
172 log.debug('Repository path is %s' % repo_path)
172
173
173 # CHECK LOCKING only if it's not ANONYMOUS USER
174 # CHECK LOCKING only if it's not ANONYMOUS USER
174 if username != User.DEFAULT_USER:
175 if username != User.DEFAULT_USER:
175 log.debug('Checking locking on repository')
176 log.debug('Checking locking on repository')
176 (make_lock,
177 (make_lock,
177 locked,
178 locked,
178 locked_by) = self._check_locking_state(
179 locked_by) = self._check_locking_state(
179 environ=environ, action=action,
180 environ=environ, action=action,
180 repo=repo_name, user_id=user.user_id
181 repo=repo_name, user_id=user.user_id
181 )
182 )
182 # store the make_lock for later evaluation in hooks
183 # store the make_lock for later evaluation in hooks
183 extras.update({'make_lock': make_lock,
184 extras.update({'make_lock': make_lock,
184 'locked_by': locked_by})
185 'locked_by': locked_by})
185
186
186 # set the environ variables for this request
187 # set the environ variables for this request
187 os.environ['RC_SCM_DATA'] = json.dumps(extras)
188 os.environ['RC_SCM_DATA'] = json.dumps(extras)
188 fix_PATH()
189 fix_PATH()
189 log.debug('HOOKS extras is %s' % extras)
190 log.debug('HOOKS extras is %s' % extras)
190 baseui = make_ui('db')
191 baseui = make_ui('db')
191 self.__inject_extras(repo_path, baseui, extras)
192 self.__inject_extras(repo_path, baseui, extras)
192
193
193 try:
194 try:
194 log.info('%s action on HG repo "%s" by "%s" from %s' %
195 log.info('%s action on HG repo "%s" by "%s" from %s' %
195 (action, repo_name, username, ip_addr))
196 (action, repo_name, username, ip_addr))
196 app = self.__make_app(repo_path, baseui, extras)
197 app = self.__make_app(repo_path, baseui, extras)
197 return app(environ, start_response)
198 return app(environ, start_response)
198 except RepoError, e:
199 except RepoError, e:
199 if str(e).find('not found') != -1:
200 if str(e).find('not found') != -1:
200 return HTTPNotFound()(environ, start_response)
201 return HTTPNotFound()(environ, start_response)
201 except HTTPLockedRC, e:
202 except HTTPLockedRC, e:
202 _code = CONFIG.get('lock_ret_code')
203 _code = CONFIG.get('lock_ret_code')
203 log.debug('Repository LOCKED ret code %s!' % (_code))
204 log.debug('Repository LOCKED ret code %s!' % (_code))
204 return e(environ, start_response)
205 return e(environ, start_response)
205 except Exception:
206 except Exception:
206 log.error(traceback.format_exc())
207 log.error(traceback.format_exc())
207 return HTTPInternalServerError()(environ, start_response)
208 return HTTPInternalServerError()(environ, start_response)
208 finally:
209 finally:
209 # invalidate cache on push
210 # invalidate cache on push
210 if action == 'push':
211 if action == 'push':
211 self._invalidate_cache(repo_name)
212 self._invalidate_cache(repo_name)
212
213
213 def __make_app(self, repo_name, baseui, extras):
214 def __make_app(self, repo_name, baseui, extras):
214 """
215 """
215 Make an wsgi application using hgweb, and inject generated baseui
216 Make an wsgi application using hgweb, and inject generated baseui
216 instance, additionally inject some extras into ui object
217 instance, additionally inject some extras into ui object
217 """
218 """
218 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
219 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
219
220
220 def __get_repository(self, environ):
221 def __get_repository(self, environ):
221 """
222 """
222 Get's repository name out of PATH_INFO header
223 Get's repository name out of PATH_INFO header
223
224
224 :param environ: environ where PATH_INFO is stored
225 :param environ: environ where PATH_INFO is stored
225 """
226 """
226 try:
227 try:
227 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
228 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
228 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
229 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
229 if repo_name.endswith('/'):
230 if repo_name.endswith('/'):
230 repo_name = repo_name.rstrip('/')
231 repo_name = repo_name.rstrip('/')
231 except:
232 except:
232 log.error(traceback.format_exc())
233 log.error(traceback.format_exc())
233 raise
234 raise
234
235
235 return repo_name
236 return repo_name
236
237
237 def __get_user(self, username):
238 def __get_user(self, username):
238 return User.get_by_username(username)
239 return User.get_by_username(username)
239
240
240 def __get_action(self, environ):
241 def __get_action(self, environ):
241 """
242 """
242 Maps mercurial request commands into a clone,pull or push command.
243 Maps mercurial request commands into a clone,pull or push command.
243 This should always return a valid command string
244 This should always return a valid command string
244
245
245 :param environ:
246 :param environ:
246 """
247 """
247 mapping = {'changegroup': 'pull',
248 mapping = {'changegroup': 'pull',
248 'changegroupsubset': 'pull',
249 'changegroupsubset': 'pull',
249 'stream_out': 'pull',
250 'stream_out': 'pull',
250 'listkeys': 'pull',
251 'listkeys': 'pull',
251 'unbundle': 'push',
252 'unbundle': 'push',
252 'pushkey': 'push', }
253 'pushkey': 'push', }
253 for qry in environ['QUERY_STRING'].split('&'):
254 for qry in environ['QUERY_STRING'].split('&'):
254 if qry.startswith('cmd'):
255 if qry.startswith('cmd'):
255 cmd = qry.split('=')[-1]
256 cmd = qry.split('=')[-1]
256 if cmd in mapping:
257 if cmd in mapping:
257 return mapping[cmd]
258 return mapping[cmd]
258
259
259 return 'pull'
260 return 'pull'
260
261
261 raise Exception('Unable to detect pull/push action !!'
262 raise Exception('Unable to detect pull/push action !!'
262 'Are you using non standard command or client ?')
263 'Are you using non standard command or client ?')
263
264
264 def __inject_extras(self, repo_path, baseui, extras={}):
265 def __inject_extras(self, repo_path, baseui, extras={}):
265 """
266 """
266 Injects some extra params into baseui instance
267 Injects some extra params into baseui instance
267
268
268 also overwrites global settings with those takes from local hgrc file
269 also overwrites global settings with those takes from local hgrc file
269
270
270 :param baseui: baseui instance
271 :param baseui: baseui instance
271 :param extras: dict with extra params to put into baseui
272 :param extras: dict with extra params to put into baseui
272 """
273 """
273
274
274 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
275 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
275
276
276 # make our hgweb quiet so it doesn't print output
277 # make our hgweb quiet so it doesn't print output
277 baseui.setconfig('ui', 'quiet', 'true')
278 baseui.setconfig('ui', 'quiet', 'true')
278
279
279 #inject some additional parameters that will be available in ui
280 #for hooks
281 for k, v in extras.items():
282 baseui.setconfig('rhodecode_extras', k, v)
283
284 repoui = make_ui('file', hgrc, False)
280 repoui = make_ui('file', hgrc, False)
285
281
286 if repoui:
282 if repoui:
287 #overwrite our ui instance with the section from hgrc file
283 #overwrite our ui instance with the section from hgrc file
288 for section in ui_sections:
284 for section in ui_sections:
289 for k, v in repoui.configitems(section):
285 for k, v in repoui.configitems(section):
290 baseui.setconfig(section, k, v)
286 baseui.setconfig(section, k, v)
287 _set_extras(extras)
@@ -1,580 +1,609 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.utils
3 rhodecode.lib.utils
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Some simple helper functions
6 Some simple helper functions
7
7
8 :created_on: Jan 5, 2011
8 :created_on: Jan 5, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import re
27 import re
28 import sys
27 import time
29 import time
28 import datetime
30 import datetime
31 import traceback
29 import webob
32 import webob
30
33
31 from pylons.i18n.translation import _, ungettext
34 from pylons.i18n.translation import _, ungettext
32 from rhodecode.lib.vcs.utils.lazy import LazyProperty
35 from rhodecode.lib.vcs.utils.lazy import LazyProperty
36 from rhodecode.lib.compat import json
33
37
34
38
35 def __get_lem():
39 def __get_lem():
36 """
40 """
37 Get language extension map based on what's inside pygments lexers
41 Get language extension map based on what's inside pygments lexers
38 """
42 """
39 from pygments import lexers
43 from pygments import lexers
40 from string import lower
44 from string import lower
41 from collections import defaultdict
45 from collections import defaultdict
42
46
43 d = defaultdict(lambda: [])
47 d = defaultdict(lambda: [])
44
48
45 def __clean(s):
49 def __clean(s):
46 s = s.lstrip('*')
50 s = s.lstrip('*')
47 s = s.lstrip('.')
51 s = s.lstrip('.')
48
52
49 if s.find('[') != -1:
53 if s.find('[') != -1:
50 exts = []
54 exts = []
51 start, stop = s.find('['), s.find(']')
55 start, stop = s.find('['), s.find(']')
52
56
53 for suffix in s[start + 1:stop]:
57 for suffix in s[start + 1:stop]:
54 exts.append(s[:s.find('[')] + suffix)
58 exts.append(s[:s.find('[')] + suffix)
55 return map(lower, exts)
59 return map(lower, exts)
56 else:
60 else:
57 return map(lower, [s])
61 return map(lower, [s])
58
62
59 for lx, t in sorted(lexers.LEXERS.items()):
63 for lx, t in sorted(lexers.LEXERS.items()):
60 m = map(__clean, t[-2])
64 m = map(__clean, t[-2])
61 if m:
65 if m:
62 m = reduce(lambda x, y: x + y, m)
66 m = reduce(lambda x, y: x + y, m)
63 for ext in m:
67 for ext in m:
64 desc = lx.replace('Lexer', '')
68 desc = lx.replace('Lexer', '')
65 d[ext].append(desc)
69 d[ext].append(desc)
66
70
67 return dict(d)
71 return dict(d)
68
72
69
73
70 def str2bool(_str):
74 def str2bool(_str):
71 """
75 """
72 returs True/False value from given string, it tries to translate the
76 returs True/False value from given string, it tries to translate the
73 string into boolean
77 string into boolean
74
78
75 :param _str: string value to translate into boolean
79 :param _str: string value to translate into boolean
76 :rtype: boolean
80 :rtype: boolean
77 :returns: boolean from given string
81 :returns: boolean from given string
78 """
82 """
79 if _str is None:
83 if _str is None:
80 return False
84 return False
81 if _str in (True, False):
85 if _str in (True, False):
82 return _str
86 return _str
83 _str = str(_str).strip().lower()
87 _str = str(_str).strip().lower()
84 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
88 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
85
89
86
90
87 def aslist(obj, sep=None, strip=True):
91 def aslist(obj, sep=None, strip=True):
88 """
92 """
89 Returns given string separated by sep as list
93 Returns given string separated by sep as list
90
94
91 :param obj:
95 :param obj:
92 :param sep:
96 :param sep:
93 :param strip:
97 :param strip:
94 """
98 """
95 if isinstance(obj, (basestring)):
99 if isinstance(obj, (basestring)):
96 lst = obj.split(sep)
100 lst = obj.split(sep)
97 if strip:
101 if strip:
98 lst = [v.strip() for v in lst]
102 lst = [v.strip() for v in lst]
99 return lst
103 return lst
100 elif isinstance(obj, (list, tuple)):
104 elif isinstance(obj, (list, tuple)):
101 return obj
105 return obj
102 elif obj is None:
106 elif obj is None:
103 return []
107 return []
104 else:
108 else:
105 return [obj]
109 return [obj]
106
110
107
111
108 def convert_line_endings(line, mode):
112 def convert_line_endings(line, mode):
109 """
113 """
110 Converts a given line "line end" accordingly to given mode
114 Converts a given line "line end" accordingly to given mode
111
115
112 Available modes are::
116 Available modes are::
113 0 - Unix
117 0 - Unix
114 1 - Mac
118 1 - Mac
115 2 - DOS
119 2 - DOS
116
120
117 :param line: given line to convert
121 :param line: given line to convert
118 :param mode: mode to convert to
122 :param mode: mode to convert to
119 :rtype: str
123 :rtype: str
120 :return: converted line according to mode
124 :return: converted line according to mode
121 """
125 """
122 from string import replace
126 from string import replace
123
127
124 if mode == 0:
128 if mode == 0:
125 line = replace(line, '\r\n', '\n')
129 line = replace(line, '\r\n', '\n')
126 line = replace(line, '\r', '\n')
130 line = replace(line, '\r', '\n')
127 elif mode == 1:
131 elif mode == 1:
128 line = replace(line, '\r\n', '\r')
132 line = replace(line, '\r\n', '\r')
129 line = replace(line, '\n', '\r')
133 line = replace(line, '\n', '\r')
130 elif mode == 2:
134 elif mode == 2:
131 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
135 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
132 return line
136 return line
133
137
134
138
135 def detect_mode(line, default):
139 def detect_mode(line, default):
136 """
140 """
137 Detects line break for given line, if line break couldn't be found
141 Detects line break for given line, if line break couldn't be found
138 given default value is returned
142 given default value is returned
139
143
140 :param line: str line
144 :param line: str line
141 :param default: default
145 :param default: default
142 :rtype: int
146 :rtype: int
143 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
147 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
144 """
148 """
145 if line.endswith('\r\n'):
149 if line.endswith('\r\n'):
146 return 2
150 return 2
147 elif line.endswith('\n'):
151 elif line.endswith('\n'):
148 return 0
152 return 0
149 elif line.endswith('\r'):
153 elif line.endswith('\r'):
150 return 1
154 return 1
151 else:
155 else:
152 return default
156 return default
153
157
154
158
155 def generate_api_key(username, salt=None):
159 def generate_api_key(username, salt=None):
156 """
160 """
157 Generates unique API key for given username, if salt is not given
161 Generates unique API key for given username, if salt is not given
158 it'll be generated from some random string
162 it'll be generated from some random string
159
163
160 :param username: username as string
164 :param username: username as string
161 :param salt: salt to hash generate KEY
165 :param salt: salt to hash generate KEY
162 :rtype: str
166 :rtype: str
163 :returns: sha1 hash from username+salt
167 :returns: sha1 hash from username+salt
164 """
168 """
165 from tempfile import _RandomNameSequence
169 from tempfile import _RandomNameSequence
166 import hashlib
170 import hashlib
167
171
168 if salt is None:
172 if salt is None:
169 salt = _RandomNameSequence().next()
173 salt = _RandomNameSequence().next()
170
174
171 return hashlib.sha1(username + salt).hexdigest()
175 return hashlib.sha1(username + salt).hexdigest()
172
176
173
177
174 def safe_int(val, default=None):
178 def safe_int(val, default=None):
175 """
179 """
176 Returns int() of val if val is not convertable to int use default
180 Returns int() of val if val is not convertable to int use default
177 instead
181 instead
178
182
179 :param val:
183 :param val:
180 :param default:
184 :param default:
181 """
185 """
182
186
183 try:
187 try:
184 val = int(val)
188 val = int(val)
185 except (ValueError, TypeError):
189 except (ValueError, TypeError):
186 val = default
190 val = default
187
191
188 return val
192 return val
189
193
190
194
191 def safe_unicode(str_, from_encoding=None):
195 def safe_unicode(str_, from_encoding=None):
192 """
196 """
193 safe unicode function. Does few trick to turn str_ into unicode
197 safe unicode function. Does few trick to turn str_ into unicode
194
198
195 In case of UnicodeDecode error we try to return it with encoding detected
199 In case of UnicodeDecode error we try to return it with encoding detected
196 by chardet library if it fails fallback to unicode with errors replaced
200 by chardet library if it fails fallback to unicode with errors replaced
197
201
198 :param str_: string to decode
202 :param str_: string to decode
199 :rtype: unicode
203 :rtype: unicode
200 :returns: unicode object
204 :returns: unicode object
201 """
205 """
202 if isinstance(str_, unicode):
206 if isinstance(str_, unicode):
203 return str_
207 return str_
204
208
205 if not from_encoding:
209 if not from_encoding:
206 import rhodecode
210 import rhodecode
207 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
211 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
208 'utf8'), sep=',')
212 'utf8'), sep=',')
209 from_encoding = DEFAULT_ENCODINGS
213 from_encoding = DEFAULT_ENCODINGS
210
214
211 if not isinstance(from_encoding, (list, tuple)):
215 if not isinstance(from_encoding, (list, tuple)):
212 from_encoding = [from_encoding]
216 from_encoding = [from_encoding]
213
217
214 try:
218 try:
215 return unicode(str_)
219 return unicode(str_)
216 except UnicodeDecodeError:
220 except UnicodeDecodeError:
217 pass
221 pass
218
222
219 for enc in from_encoding:
223 for enc in from_encoding:
220 try:
224 try:
221 return unicode(str_, enc)
225 return unicode(str_, enc)
222 except UnicodeDecodeError:
226 except UnicodeDecodeError:
223 pass
227 pass
224
228
225 try:
229 try:
226 import chardet
230 import chardet
227 encoding = chardet.detect(str_)['encoding']
231 encoding = chardet.detect(str_)['encoding']
228 if encoding is None:
232 if encoding is None:
229 raise Exception()
233 raise Exception()
230 return str_.decode(encoding)
234 return str_.decode(encoding)
231 except (ImportError, UnicodeDecodeError, Exception):
235 except (ImportError, UnicodeDecodeError, Exception):
232 return unicode(str_, from_encoding[0], 'replace')
236 return unicode(str_, from_encoding[0], 'replace')
233
237
234
238
235 def safe_str(unicode_, to_encoding=None):
239 def safe_str(unicode_, to_encoding=None):
236 """
240 """
237 safe str function. Does few trick to turn unicode_ into string
241 safe str function. Does few trick to turn unicode_ into string
238
242
239 In case of UnicodeEncodeError we try to return it with encoding detected
243 In case of UnicodeEncodeError we try to return it with encoding detected
240 by chardet library if it fails fallback to string with errors replaced
244 by chardet library if it fails fallback to string with errors replaced
241
245
242 :param unicode_: unicode to encode
246 :param unicode_: unicode to encode
243 :rtype: str
247 :rtype: str
244 :returns: str object
248 :returns: str object
245 """
249 """
246
250
247 # if it's not basestr cast to str
251 # if it's not basestr cast to str
248 if not isinstance(unicode_, basestring):
252 if not isinstance(unicode_, basestring):
249 return str(unicode_)
253 return str(unicode_)
250
254
251 if isinstance(unicode_, str):
255 if isinstance(unicode_, str):
252 return unicode_
256 return unicode_
253
257
254 if not to_encoding:
258 if not to_encoding:
255 import rhodecode
259 import rhodecode
256 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
257 'utf8'), sep=',')
261 'utf8'), sep=',')
258 to_encoding = DEFAULT_ENCODINGS
262 to_encoding = DEFAULT_ENCODINGS
259
263
260 if not isinstance(to_encoding, (list, tuple)):
264 if not isinstance(to_encoding, (list, tuple)):
261 to_encoding = [to_encoding]
265 to_encoding = [to_encoding]
262
266
263 for enc in to_encoding:
267 for enc in to_encoding:
264 try:
268 try:
265 return unicode_.encode(enc)
269 return unicode_.encode(enc)
266 except UnicodeEncodeError:
270 except UnicodeEncodeError:
267 pass
271 pass
268
272
269 try:
273 try:
270 import chardet
274 import chardet
271 encoding = chardet.detect(unicode_)['encoding']
275 encoding = chardet.detect(unicode_)['encoding']
272 if encoding is None:
276 if encoding is None:
273 raise UnicodeEncodeError()
277 raise UnicodeEncodeError()
274
278
275 return unicode_.encode(encoding)
279 return unicode_.encode(encoding)
276 except (ImportError, UnicodeEncodeError):
280 except (ImportError, UnicodeEncodeError):
277 return unicode_.encode(to_encoding[0], 'replace')
281 return unicode_.encode(to_encoding[0], 'replace')
278
282
279 return safe_str
283 return safe_str
280
284
281
285
282 def remove_suffix(s, suffix):
286 def remove_suffix(s, suffix):
283 if s.endswith(suffix):
287 if s.endswith(suffix):
284 s = s[:-1 * len(suffix)]
288 s = s[:-1 * len(suffix)]
285 return s
289 return s
286
290
287
291
288 def remove_prefix(s, prefix):
292 def remove_prefix(s, prefix):
289 if s.startswith(prefix):
293 if s.startswith(prefix):
290 s = s[len(prefix):]
294 s = s[len(prefix):]
291 return s
295 return s
292
296
293
297
294 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
298 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
295 """
299 """
296 Custom engine_from_config functions that makes sure we use NullPool for
300 Custom engine_from_config functions that makes sure we use NullPool for
297 file based sqlite databases. This prevents errors on sqlite. This only
301 file based sqlite databases. This prevents errors on sqlite. This only
298 applies to sqlalchemy versions < 0.7.0
302 applies to sqlalchemy versions < 0.7.0
299
303
300 """
304 """
301 import sqlalchemy
305 import sqlalchemy
302 from sqlalchemy import engine_from_config as efc
306 from sqlalchemy import engine_from_config as efc
303 import logging
307 import logging
304
308
305 if int(sqlalchemy.__version__.split('.')[1]) < 7:
309 if int(sqlalchemy.__version__.split('.')[1]) < 7:
306
310
307 # This solution should work for sqlalchemy < 0.7.0, and should use
311 # This solution should work for sqlalchemy < 0.7.0, and should use
308 # proxy=TimerProxy() for execution time profiling
312 # proxy=TimerProxy() for execution time profiling
309
313
310 from sqlalchemy.pool import NullPool
314 from sqlalchemy.pool import NullPool
311 url = configuration[prefix + 'url']
315 url = configuration[prefix + 'url']
312
316
313 if url.startswith('sqlite'):
317 if url.startswith('sqlite'):
314 kwargs.update({'poolclass': NullPool})
318 kwargs.update({'poolclass': NullPool})
315 return efc(configuration, prefix, **kwargs)
319 return efc(configuration, prefix, **kwargs)
316 else:
320 else:
317 import time
321 import time
318 from sqlalchemy import event
322 from sqlalchemy import event
319 from sqlalchemy.engine import Engine
323 from sqlalchemy.engine import Engine
320
324
321 log = logging.getLogger('sqlalchemy.engine')
325 log = logging.getLogger('sqlalchemy.engine')
322 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
326 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
323 engine = efc(configuration, prefix, **kwargs)
327 engine = efc(configuration, prefix, **kwargs)
324
328
325 def color_sql(sql):
329 def color_sql(sql):
326 COLOR_SEQ = "\033[1;%dm"
330 COLOR_SEQ = "\033[1;%dm"
327 COLOR_SQL = YELLOW
331 COLOR_SQL = YELLOW
328 normal = '\x1b[0m'
332 normal = '\x1b[0m'
329 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
333 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
330
334
331 if configuration['debug']:
335 if configuration['debug']:
332 #attach events only for debug configuration
336 #attach events only for debug configuration
333
337
334 def before_cursor_execute(conn, cursor, statement,
338 def before_cursor_execute(conn, cursor, statement,
335 parameters, context, executemany):
339 parameters, context, executemany):
336 context._query_start_time = time.time()
340 context._query_start_time = time.time()
337 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
341 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
338
342
339 def after_cursor_execute(conn, cursor, statement,
343 def after_cursor_execute(conn, cursor, statement,
340 parameters, context, executemany):
344 parameters, context, executemany):
341 total = time.time() - context._query_start_time
345 total = time.time() - context._query_start_time
342 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
346 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
343
347
344 event.listen(engine, "before_cursor_execute",
348 event.listen(engine, "before_cursor_execute",
345 before_cursor_execute)
349 before_cursor_execute)
346 event.listen(engine, "after_cursor_execute",
350 event.listen(engine, "after_cursor_execute",
347 after_cursor_execute)
351 after_cursor_execute)
348
352
349 return engine
353 return engine
350
354
351
355
352 def age(prevdate, show_short_version=False):
356 def age(prevdate, show_short_version=False):
353 """
357 """
354 turns a datetime into an age string.
358 turns a datetime into an age string.
355 If show_short_version is True, then it will generate a not so accurate but shorter string,
359 If show_short_version is True, then it will generate a not so accurate but shorter string,
356 example: 2days ago, instead of 2 days and 23 hours ago.
360 example: 2days ago, instead of 2 days and 23 hours ago.
357
361
358 :param prevdate: datetime object
362 :param prevdate: datetime object
359 :param show_short_version: if it should aproximate the date and return a shorter string
363 :param show_short_version: if it should aproximate the date and return a shorter string
360 :rtype: unicode
364 :rtype: unicode
361 :returns: unicode words describing age
365 :returns: unicode words describing age
362 """
366 """
363 now = datetime.datetime.now()
367 now = datetime.datetime.now()
364 now = now.replace(microsecond=0)
368 now = now.replace(microsecond=0)
365 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
369 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
366 deltas = {}
370 deltas = {}
367 future = False
371 future = False
368
372
369 if prevdate > now:
373 if prevdate > now:
370 now, prevdate = prevdate, now
374 now, prevdate = prevdate, now
371 future = True
375 future = True
372
376
373 # Get date parts deltas
377 # Get date parts deltas
374 for part in order:
378 for part in order:
375 if future:
379 if future:
376 from dateutil import relativedelta
380 from dateutil import relativedelta
377 d = relativedelta.relativedelta(now, prevdate)
381 d = relativedelta.relativedelta(now, prevdate)
378 deltas[part] = getattr(d, part + 's')
382 deltas[part] = getattr(d, part + 's')
379 else:
383 else:
380 deltas[part] = getattr(now, part) - getattr(prevdate, part)
384 deltas[part] = getattr(now, part) - getattr(prevdate, part)
381
385
382 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
386 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
383 # not 1 hour, -59 minutes and -59 seconds)
387 # not 1 hour, -59 minutes and -59 seconds)
384 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
388 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
385 part = order[num]
389 part = order[num]
386 carry_part = order[num - 1]
390 carry_part = order[num - 1]
387
391
388 if deltas[part] < 0:
392 if deltas[part] < 0:
389 deltas[part] += length
393 deltas[part] += length
390 deltas[carry_part] -= 1
394 deltas[carry_part] -= 1
391
395
392 # Same thing for days except that the increment depends on the (variable)
396 # Same thing for days except that the increment depends on the (variable)
393 # number of days in the month
397 # number of days in the month
394 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
398 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
395 if deltas['day'] < 0:
399 if deltas['day'] < 0:
396 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
400 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
397 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
401 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
398 deltas['day'] += 29
402 deltas['day'] += 29
399 else:
403 else:
400 deltas['day'] += month_lengths[prevdate.month - 1]
404 deltas['day'] += month_lengths[prevdate.month - 1]
401
405
402 deltas['month'] -= 1
406 deltas['month'] -= 1
403
407
404 if deltas['month'] < 0:
408 if deltas['month'] < 0:
405 deltas['month'] += 12
409 deltas['month'] += 12
406 deltas['year'] -= 1
410 deltas['year'] -= 1
407
411
408 # Format the result
412 # Format the result
409 fmt_funcs = {
413 fmt_funcs = {
410 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
414 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
411 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
415 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
412 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
416 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
413 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
417 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
414 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
418 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
415 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
419 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
416 }
420 }
417
421
418 for i, part in enumerate(order):
422 for i, part in enumerate(order):
419 value = deltas[part]
423 value = deltas[part]
420 if value == 0:
424 if value == 0:
421 continue
425 continue
422
426
423 if i < 5:
427 if i < 5:
424 sub_part = order[i + 1]
428 sub_part = order[i + 1]
425 sub_value = deltas[sub_part]
429 sub_value = deltas[sub_part]
426 else:
430 else:
427 sub_value = 0
431 sub_value = 0
428
432
429 if sub_value == 0 or show_short_version:
433 if sub_value == 0 or show_short_version:
430 if future:
434 if future:
431 return _(u'in %s') % fmt_funcs[part](value)
435 return _(u'in %s') % fmt_funcs[part](value)
432 else:
436 else:
433 return _(u'%s ago') % fmt_funcs[part](value)
437 return _(u'%s ago') % fmt_funcs[part](value)
434 if future:
438 if future:
435 return _(u'in %s and %s') % (fmt_funcs[part](value),
439 return _(u'in %s and %s') % (fmt_funcs[part](value),
436 fmt_funcs[sub_part](sub_value))
440 fmt_funcs[sub_part](sub_value))
437 else:
441 else:
438 return _(u'%s and %s ago') % (fmt_funcs[part](value),
442 return _(u'%s and %s ago') % (fmt_funcs[part](value),
439 fmt_funcs[sub_part](sub_value))
443 fmt_funcs[sub_part](sub_value))
440
444
441 return _(u'just now')
445 return _(u'just now')
442
446
443
447
444 def uri_filter(uri):
448 def uri_filter(uri):
445 """
449 """
446 Removes user:password from given url string
450 Removes user:password from given url string
447
451
448 :param uri:
452 :param uri:
449 :rtype: unicode
453 :rtype: unicode
450 :returns: filtered list of strings
454 :returns: filtered list of strings
451 """
455 """
452 if not uri:
456 if not uri:
453 return ''
457 return ''
454
458
455 proto = ''
459 proto = ''
456
460
457 for pat in ('https://', 'http://'):
461 for pat in ('https://', 'http://'):
458 if uri.startswith(pat):
462 if uri.startswith(pat):
459 uri = uri[len(pat):]
463 uri = uri[len(pat):]
460 proto = pat
464 proto = pat
461 break
465 break
462
466
463 # remove passwords and username
467 # remove passwords and username
464 uri = uri[uri.find('@') + 1:]
468 uri = uri[uri.find('@') + 1:]
465
469
466 # get the port
470 # get the port
467 cred_pos = uri.find(':')
471 cred_pos = uri.find(':')
468 if cred_pos == -1:
472 if cred_pos == -1:
469 host, port = uri, None
473 host, port = uri, None
470 else:
474 else:
471 host, port = uri[:cred_pos], uri[cred_pos + 1:]
475 host, port = uri[:cred_pos], uri[cred_pos + 1:]
472
476
473 return filter(None, [proto, host, port])
477 return filter(None, [proto, host, port])
474
478
475
479
476 def credentials_filter(uri):
480 def credentials_filter(uri):
477 """
481 """
478 Returns a url with removed credentials
482 Returns a url with removed credentials
479
483
480 :param uri:
484 :param uri:
481 """
485 """
482
486
483 uri = uri_filter(uri)
487 uri = uri_filter(uri)
484 #check if we have port
488 #check if we have port
485 if len(uri) > 2 and uri[2]:
489 if len(uri) > 2 and uri[2]:
486 uri[2] = ':' + uri[2]
490 uri[2] = ':' + uri[2]
487
491
488 return ''.join(uri)
492 return ''.join(uri)
489
493
490
494
491 def get_changeset_safe(repo, rev):
495 def get_changeset_safe(repo, rev):
492 """
496 """
493 Safe version of get_changeset if this changeset doesn't exists for a
497 Safe version of get_changeset if this changeset doesn't exists for a
494 repo it returns a Dummy one instead
498 repo it returns a Dummy one instead
495
499
496 :param repo:
500 :param repo:
497 :param rev:
501 :param rev:
498 """
502 """
499 from rhodecode.lib.vcs.backends.base import BaseRepository
503 from rhodecode.lib.vcs.backends.base import BaseRepository
500 from rhodecode.lib.vcs.exceptions import RepositoryError
504 from rhodecode.lib.vcs.exceptions import RepositoryError
501 from rhodecode.lib.vcs.backends.base import EmptyChangeset
505 from rhodecode.lib.vcs.backends.base import EmptyChangeset
502 if not isinstance(repo, BaseRepository):
506 if not isinstance(repo, BaseRepository):
503 raise Exception('You must pass an Repository '
507 raise Exception('You must pass an Repository '
504 'object as first argument got %s', type(repo))
508 'object as first argument got %s', type(repo))
505
509
506 try:
510 try:
507 cs = repo.get_changeset(rev)
511 cs = repo.get_changeset(rev)
508 except RepositoryError:
512 except RepositoryError:
509 cs = EmptyChangeset(requested_revision=rev)
513 cs = EmptyChangeset(requested_revision=rev)
510 return cs
514 return cs
511
515
512
516
513 def datetime_to_time(dt):
517 def datetime_to_time(dt):
514 if dt:
518 if dt:
515 return time.mktime(dt.timetuple())
519 return time.mktime(dt.timetuple())
516
520
517
521
518 def time_to_datetime(tm):
522 def time_to_datetime(tm):
519 if tm:
523 if tm:
520 if isinstance(tm, basestring):
524 if isinstance(tm, basestring):
521 try:
525 try:
522 tm = float(tm)
526 tm = float(tm)
523 except ValueError:
527 except ValueError:
524 return
528 return
525 return datetime.datetime.fromtimestamp(tm)
529 return datetime.datetime.fromtimestamp(tm)
526
530
527 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
531 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
528
532
529
533
530 def extract_mentioned_users(s):
534 def extract_mentioned_users(s):
531 """
535 """
532 Returns unique usernames from given string s that have @mention
536 Returns unique usernames from given string s that have @mention
533
537
534 :param s: string to get mentions
538 :param s: string to get mentions
535 """
539 """
536 usrs = set()
540 usrs = set()
537 for username in re.findall(MENTIONS_REGEX, s):
541 for username in re.findall(MENTIONS_REGEX, s):
538 usrs.add(username)
542 usrs.add(username)
539
543
540 return sorted(list(usrs), key=lambda k: k.lower())
544 return sorted(list(usrs), key=lambda k: k.lower())
541
545
542
546
543 class AttributeDict(dict):
547 class AttributeDict(dict):
544 def __getattr__(self, attr):
548 def __getattr__(self, attr):
545 return self.get(attr, None)
549 return self.get(attr, None)
546 __setattr__ = dict.__setitem__
550 __setattr__ = dict.__setitem__
547 __delattr__ = dict.__delitem__
551 __delattr__ = dict.__delitem__
548
552
549
553
550 def fix_PATH(os_=None):
554 def fix_PATH(os_=None):
551 """
555 """
552 Get current active python path, and append it to PATH variable to fix issues
556 Get current active python path, and append it to PATH variable to fix issues
553 of subprocess calls and different python versions
557 of subprocess calls and different python versions
554 """
558 """
555 import sys
556 if os_ is None:
559 if os_ is None:
557 import os
560 import os
558 else:
561 else:
559 os = os_
562 os = os_
560
563
561 cur_path = os.path.split(sys.executable)[0]
564 cur_path = os.path.split(sys.executable)[0]
562 if not os.environ['PATH'].startswith(cur_path):
565 if not os.environ['PATH'].startswith(cur_path):
563 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
566 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
564
567
565
568
566 def obfuscate_url_pw(engine):
569 def obfuscate_url_pw(engine):
567 _url = engine or ''
570 _url = engine or ''
568 from sqlalchemy.engine import url as sa_url
571 from sqlalchemy.engine import url as sa_url
569 try:
572 try:
570 _url = sa_url.make_url(engine)
573 _url = sa_url.make_url(engine)
571 if _url.password:
574 if _url.password:
572 _url.password = 'XXXXX'
575 _url.password = 'XXXXX'
573 except:
576 except:
574 pass
577 pass
575 return str(_url)
578 return str(_url)
576
579
577
580
578 def get_server_url(environ):
581 def get_server_url(environ):
579 req = webob.Request(environ)
582 req = webob.Request(environ)
580 return req.host_url + req.script_name
583 return req.host_url + req.script_name
584
585
586 def _extract_extras():
587 """
588 Extracts the rc extras data from os.environ, and wraps it into named
589 AttributeDict object
590 """
591 try:
592 rc_extras = json.loads(os.environ['RC_SCM_DATA'])
593 except:
594 print os.environ
595 print >> sys.stderr, traceback.format_exc()
596 rc_extras = {}
597
598 try:
599 for k in ['username', 'repository', 'locked_by', 'scm', 'make_lock',
600 'action', 'ip']:
601 rc_extras[k]
602 except KeyError, e:
603 raise Exception('Missing key %s in os.environ %s' % (e, rc_extras))
604
605 return AttributeDict(rc_extras)
606
607
608 def _set_extras(extras):
609 os.environ['RC_SCM_DATA'] = json.dumps(extras)
@@ -1,1018 +1,997 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 from itertools import chain
13 from itertools import chain
14 from rhodecode.lib.vcs.utils import author_name, author_email
14 from rhodecode.lib.vcs.utils import author_name, author_email
15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 from rhodecode.lib.vcs.conf import settings
17 from rhodecode.lib.vcs.conf import settings
18
18
19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 RepositoryError
22 RepositoryError
23
23
24
24
25 class BaseRepository(object):
25 class BaseRepository(object):
26 """
26 """
27 Base Repository for final backends
27 Base Repository for final backends
28
28
29 **Attributes**
29 **Attributes**
30
30
31 ``DEFAULT_BRANCH_NAME``
31 ``DEFAULT_BRANCH_NAME``
32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33
33
34 ``scm``
34 ``scm``
35 alias of scm, i.e. *git* or *hg*
35 alias of scm, i.e. *git* or *hg*
36
36
37 ``repo``
37 ``repo``
38 object from external api
38 object from external api
39
39
40 ``revisions``
40 ``revisions``
41 list of all available revisions' ids, in ascending order
41 list of all available revisions' ids, in ascending order
42
42
43 ``changesets``
43 ``changesets``
44 storage dict caching returned changesets
44 storage dict caching returned changesets
45
45
46 ``path``
46 ``path``
47 absolute path to the repository
47 absolute path to the repository
48
48
49 ``branches``
49 ``branches``
50 branches as list of changesets
50 branches as list of changesets
51
51
52 ``tags``
52 ``tags``
53 tags as list of changesets
53 tags as list of changesets
54 """
54 """
55 scm = None
55 scm = None
56 DEFAULT_BRANCH_NAME = None
56 DEFAULT_BRANCH_NAME = None
57 EMPTY_CHANGESET = '0' * 40
57 EMPTY_CHANGESET = '0' * 40
58
58
59 def __init__(self, repo_path, create=False, **kwargs):
59 def __init__(self, repo_path, create=False, **kwargs):
60 """
60 """
61 Initializes repository. Raises RepositoryError if repository could
61 Initializes repository. Raises RepositoryError if repository could
62 not be find at the given ``repo_path`` or directory at ``repo_path``
62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 exists and ``create`` is set to True.
63 exists and ``create`` is set to True.
64
64
65 :param repo_path: local path of the repository
65 :param repo_path: local path of the repository
66 :param create=False: if set to True, would try to craete repository.
66 :param create=False: if set to True, would try to craete repository.
67 :param src_url=None: if set, should be proper url from which repository
67 :param src_url=None: if set, should be proper url from which repository
68 would be cloned; requires ``create`` parameter to be set to True -
68 would be cloned; requires ``create`` parameter to be set to True -
69 raises RepositoryError if src_url is set and create evaluates to
69 raises RepositoryError if src_url is set and create evaluates to
70 False
70 False
71 """
71 """
72 raise NotImplementedError
72 raise NotImplementedError
73
73
74 def __str__(self):
74 def __str__(self):
75 return '<%s at %s>' % (self.__class__.__name__, self.path)
75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return self.__str__()
78 return self.__str__()
79
79
80 def __len__(self):
80 def __len__(self):
81 return self.count()
81 return self.count()
82
82
83 @LazyProperty
83 @LazyProperty
84 def alias(self):
84 def alias(self):
85 for k, v in settings.BACKENDS.items():
85 for k, v in settings.BACKENDS.items():
86 if v.split('.')[-1] == str(self.__class__.__name__):
86 if v.split('.')[-1] == str(self.__class__.__name__):
87 return k
87 return k
88
88
89 @LazyProperty
89 @LazyProperty
90 def name(self):
90 def name(self):
91 raise NotImplementedError
91 raise NotImplementedError
92
92
93 @LazyProperty
93 @LazyProperty
94 def owner(self):
94 def owner(self):
95 raise NotImplementedError
95 raise NotImplementedError
96
96
97 @LazyProperty
97 @LazyProperty
98 def description(self):
98 def description(self):
99 raise NotImplementedError
99 raise NotImplementedError
100
100
101 @LazyProperty
101 @LazyProperty
102 def size(self):
102 def size(self):
103 """
103 """
104 Returns combined size in bytes for all repository files
104 Returns combined size in bytes for all repository files
105 """
105 """
106
106
107 size = 0
107 size = 0
108 try:
108 try:
109 tip = self.get_changeset()
109 tip = self.get_changeset()
110 for topnode, dirs, files in tip.walk('/'):
110 for topnode, dirs, files in tip.walk('/'):
111 for f in files:
111 for f in files:
112 size += tip.get_file_size(f.path)
112 size += tip.get_file_size(f.path)
113 for dir in dirs:
113 for dir in dirs:
114 for f in files:
114 for f in files:
115 size += tip.get_file_size(f.path)
115 size += tip.get_file_size(f.path)
116
116
117 except RepositoryError, e:
117 except RepositoryError, e:
118 pass
118 pass
119 return size
119 return size
120
120
121 def is_valid(self):
121 def is_valid(self):
122 """
122 """
123 Validates repository.
123 Validates repository.
124 """
124 """
125 raise NotImplementedError
125 raise NotImplementedError
126
126
127 def get_last_change(self):
127 def get_last_change(self):
128 self.get_changesets()
128 self.get_changesets()
129
129
130 #==========================================================================
130 #==========================================================================
131 # CHANGESETS
131 # CHANGESETS
132 #==========================================================================
132 #==========================================================================
133
133
134 def get_changeset(self, revision=None):
134 def get_changeset(self, revision=None):
135 """
135 """
136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 recent changeset is returned.
137 recent changeset is returned.
138
138
139 :raises ``EmptyRepositoryError``: if there are no revisions
139 :raises ``EmptyRepositoryError``: if there are no revisions
140 """
140 """
141 raise NotImplementedError
141 raise NotImplementedError
142
142
143 def __iter__(self):
143 def __iter__(self):
144 """
144 """
145 Allows Repository objects to be iterated.
145 Allows Repository objects to be iterated.
146
146
147 *Requires* implementation of ``__getitem__`` method.
147 *Requires* implementation of ``__getitem__`` method.
148 """
148 """
149 for revision in self.revisions:
149 for revision in self.revisions:
150 yield self.get_changeset(revision)
150 yield self.get_changeset(revision)
151
151
152 def get_changesets(self, start=None, end=None, start_date=None,
152 def get_changesets(self, start=None, end=None, start_date=None,
153 end_date=None, branch_name=None, reverse=False):
153 end_date=None, branch_name=None, reverse=False):
154 """
154 """
155 Returns iterator of ``MercurialChangeset`` objects from start to end
155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 not inclusive This should behave just like a list, ie. end is not
156 not inclusive This should behave just like a list, ie. end is not
157 inclusive
157 inclusive
158
158
159 :param start: None or str
159 :param start: None or str
160 :param end: None or str
160 :param end: None or str
161 :param start_date:
161 :param start_date:
162 :param end_date:
162 :param end_date:
163 :param branch_name:
163 :param branch_name:
164 :param reversed:
164 :param reversed:
165 """
165 """
166 raise NotImplementedError
166 raise NotImplementedError
167
167
168 def __getslice__(self, i, j):
168 def __getslice__(self, i, j):
169 """
169 """
170 Returns a iterator of sliced repository
170 Returns a iterator of sliced repository
171 """
171 """
172 for rev in self.revisions[i:j]:
172 for rev in self.revisions[i:j]:
173 yield self.get_changeset(rev)
173 yield self.get_changeset(rev)
174
174
175 def __getitem__(self, key):
175 def __getitem__(self, key):
176 return self.get_changeset(key)
176 return self.get_changeset(key)
177
177
178 def count(self):
178 def count(self):
179 return len(self.revisions)
179 return len(self.revisions)
180
180
181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 """
182 """
183 Creates and returns a tag for the given ``revision``.
183 Creates and returns a tag for the given ``revision``.
184
184
185 :param name: name for new tag
185 :param name: name for new tag
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param revision: changeset id for which new tag would be created
187 :param revision: changeset id for which new tag would be created
188 :param message: message of the tag's commit
188 :param message: message of the tag's commit
189 :param date: date of tag's commit
189 :param date: date of tag's commit
190
190
191 :raises TagAlreadyExistError: if tag with same name already exists
191 :raises TagAlreadyExistError: if tag with same name already exists
192 """
192 """
193 raise NotImplementedError
193 raise NotImplementedError
194
194
195 def remove_tag(self, name, user, message=None, date=None):
195 def remove_tag(self, name, user, message=None, date=None):
196 """
196 """
197 Removes tag with the given ``name``.
197 Removes tag with the given ``name``.
198
198
199 :param name: name of the tag to be removed
199 :param name: name of the tag to be removed
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 :param message: message of the tag's removal commit
201 :param message: message of the tag's removal commit
202 :param date: date of tag's removal commit
202 :param date: date of tag's removal commit
203
203
204 :raises TagDoesNotExistError: if tag with given name does not exists
204 :raises TagDoesNotExistError: if tag with given name does not exists
205 """
205 """
206 raise NotImplementedError
206 raise NotImplementedError
207
207
208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 context=3):
209 context=3):
210 """
210 """
211 Returns (git like) *diff*, as plain text. Shows changes introduced by
211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 ``rev2`` since ``rev1``.
212 ``rev2`` since ``rev1``.
213
213
214 :param rev1: Entry point from which diff is shown. Can be
214 :param rev1: Entry point from which diff is shown. Can be
215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 the changes since empty state of the repository until ``rev2``
216 the changes since empty state of the repository until ``rev2``
217 :param rev2: Until which revision changes should be shown.
217 :param rev2: Until which revision changes should be shown.
218 :param ignore_whitespace: If set to ``True``, would not show whitespace
218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 changes. Defaults to ``False``.
219 changes. Defaults to ``False``.
220 :param context: How many lines before/after changed lines should be
220 :param context: How many lines before/after changed lines should be
221 shown. Defaults to ``3``.
221 shown. Defaults to ``3``.
222 """
222 """
223 raise NotImplementedError
223 raise NotImplementedError
224
224
225 # ========== #
225 # ========== #
226 # COMMIT API #
226 # COMMIT API #
227 # ========== #
227 # ========== #
228
228
229 @LazyProperty
229 @LazyProperty
230 def in_memory_changeset(self):
230 def in_memory_changeset(self):
231 """
231 """
232 Returns ``InMemoryChangeset`` object for this repository.
232 Returns ``InMemoryChangeset`` object for this repository.
233 """
233 """
234 raise NotImplementedError
234 raise NotImplementedError
235
235
236 def add(self, filenode, **kwargs):
236 def add(self, filenode, **kwargs):
237 """
237 """
238 Commit api function that will add given ``FileNode`` into this
238 Commit api function that will add given ``FileNode`` into this
239 repository.
239 repository.
240
240
241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 already in repository
242 already in repository
243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 *added*
244 *added*
245 """
245 """
246 raise NotImplementedError
246 raise NotImplementedError
247
247
248 def remove(self, filenode, **kwargs):
248 def remove(self, filenode, **kwargs):
249 """
249 """
250 Commit api function that will remove given ``FileNode`` into this
250 Commit api function that will remove given ``FileNode`` into this
251 repository.
251 repository.
252
252
253 :raises ``EmptyRepositoryError``: if there are no changesets yet
253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 :raises ``NodeDoesNotExistError``: if there is no file with given path
254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 """
255 """
256 raise NotImplementedError
256 raise NotImplementedError
257
257
258 def commit(self, message, **kwargs):
258 def commit(self, message, **kwargs):
259 """
259 """
260 Persists current changes made on this repository and returns newly
260 Persists current changes made on this repository and returns newly
261 created changeset.
261 created changeset.
262
262
263 :raises ``NothingChangedError``: if no changes has been made
263 :raises ``NothingChangedError``: if no changes has been made
264 """
264 """
265 raise NotImplementedError
265 raise NotImplementedError
266
266
267 def get_state(self):
267 def get_state(self):
268 """
268 """
269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 containing ``FileNode`` objects.
270 containing ``FileNode`` objects.
271 """
271 """
272 raise NotImplementedError
272 raise NotImplementedError
273
273
274 def get_config_value(self, section, name, config_file=None):
274 def get_config_value(self, section, name, config_file=None):
275 """
275 """
276 Returns configuration value for a given [``section``] and ``name``.
276 Returns configuration value for a given [``section``] and ``name``.
277
277
278 :param section: Section we want to retrieve value from
278 :param section: Section we want to retrieve value from
279 :param name: Name of configuration we want to retrieve
279 :param name: Name of configuration we want to retrieve
280 :param config_file: A path to file which should be used to retrieve
280 :param config_file: A path to file which should be used to retrieve
281 configuration from (might also be a list of file paths)
281 configuration from (might also be a list of file paths)
282 """
282 """
283 raise NotImplementedError
283 raise NotImplementedError
284
284
285 def get_user_name(self, config_file=None):
285 def get_user_name(self, config_file=None):
286 """
286 """
287 Returns user's name from global configuration file.
287 Returns user's name from global configuration file.
288
288
289 :param config_file: A path to file which should be used to retrieve
289 :param config_file: A path to file which should be used to retrieve
290 configuration from (might also be a list of file paths)
290 configuration from (might also be a list of file paths)
291 """
291 """
292 raise NotImplementedError
292 raise NotImplementedError
293
293
294 def get_user_email(self, config_file=None):
294 def get_user_email(self, config_file=None):
295 """
295 """
296 Returns user's email from global configuration file.
296 Returns user's email from global configuration file.
297
297
298 :param config_file: A path to file which should be used to retrieve
298 :param config_file: A path to file which should be used to retrieve
299 configuration from (might also be a list of file paths)
299 configuration from (might also be a list of file paths)
300 """
300 """
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 # =========== #
303 # =========== #
304 # WORKDIR API #
304 # WORKDIR API #
305 # =========== #
305 # =========== #
306
306
307 @LazyProperty
307 @LazyProperty
308 def workdir(self):
308 def workdir(self):
309 """
309 """
310 Returns ``Workdir`` instance for this repository.
310 Returns ``Workdir`` instance for this repository.
311 """
311 """
312 raise NotImplementedError
312 raise NotImplementedError
313
313
314 def inject_ui(self, **extras):
315 """
316 Injects extra parameters into UI object of this repo
317 """
318 required_extras = [
319 'ip',
320 'username',
321 'action',
322 'repository',
323 'scm',
324 'config',
325 'server_url',
326 'make_lock',
327 'locked_by',
328 ]
329 for req in required_extras:
330 if req not in extras:
331 raise AttributeError('Missing attribute %s in extras' % (req))
332 for k, v in extras.items():
333 self._repo.ui.setconfig('rhodecode_extras', k, v)
334
335
314
336 class BaseChangeset(object):
315 class BaseChangeset(object):
337 """
316 """
338 Each backend should implement it's changeset representation.
317 Each backend should implement it's changeset representation.
339
318
340 **Attributes**
319 **Attributes**
341
320
342 ``repository``
321 ``repository``
343 repository object within which changeset exists
322 repository object within which changeset exists
344
323
345 ``id``
324 ``id``
346 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
325 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
347
326
348 ``raw_id``
327 ``raw_id``
349 raw changeset representation (i.e. full 40 length sha for git
328 raw changeset representation (i.e. full 40 length sha for git
350 backend)
329 backend)
351
330
352 ``short_id``
331 ``short_id``
353 shortened (if apply) version of ``raw_id``; it would be simple
332 shortened (if apply) version of ``raw_id``; it would be simple
354 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
333 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
355 as ``raw_id`` for subversion
334 as ``raw_id`` for subversion
356
335
357 ``revision``
336 ``revision``
358 revision number as integer
337 revision number as integer
359
338
360 ``files``
339 ``files``
361 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
340 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
362
341
363 ``dirs``
342 ``dirs``
364 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
343 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
365
344
366 ``nodes``
345 ``nodes``
367 combined list of ``Node`` objects
346 combined list of ``Node`` objects
368
347
369 ``author``
348 ``author``
370 author of the changeset, as unicode
349 author of the changeset, as unicode
371
350
372 ``message``
351 ``message``
373 message of the changeset, as unicode
352 message of the changeset, as unicode
374
353
375 ``parents``
354 ``parents``
376 list of parent changesets
355 list of parent changesets
377
356
378 ``last``
357 ``last``
379 ``True`` if this is last changeset in repository, ``False``
358 ``True`` if this is last changeset in repository, ``False``
380 otherwise; trying to access this attribute while there is no
359 otherwise; trying to access this attribute while there is no
381 changesets would raise ``EmptyRepositoryError``
360 changesets would raise ``EmptyRepositoryError``
382 """
361 """
383 def __str__(self):
362 def __str__(self):
384 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
363 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
385 self.short_id)
364 self.short_id)
386
365
387 def __repr__(self):
366 def __repr__(self):
388 return self.__str__()
367 return self.__str__()
389
368
390 def __unicode__(self):
369 def __unicode__(self):
391 return u'%s:%s' % (self.revision, self.short_id)
370 return u'%s:%s' % (self.revision, self.short_id)
392
371
393 def __eq__(self, other):
372 def __eq__(self, other):
394 return self.raw_id == other.raw_id
373 return self.raw_id == other.raw_id
395
374
396 def __json__(self):
375 def __json__(self):
397 return dict(
376 return dict(
398 short_id=self.short_id,
377 short_id=self.short_id,
399 raw_id=self.raw_id,
378 raw_id=self.raw_id,
400 revision=self.revision,
379 revision=self.revision,
401 message=self.message,
380 message=self.message,
402 date=self.date,
381 date=self.date,
403 author=self.author,
382 author=self.author,
404 )
383 )
405
384
406 @LazyProperty
385 @LazyProperty
407 def last(self):
386 def last(self):
408 if self.repository is None:
387 if self.repository is None:
409 raise ChangesetError("Cannot check if it's most recent revision")
388 raise ChangesetError("Cannot check if it's most recent revision")
410 return self.raw_id == self.repository.revisions[-1]
389 return self.raw_id == self.repository.revisions[-1]
411
390
412 @LazyProperty
391 @LazyProperty
413 def parents(self):
392 def parents(self):
414 """
393 """
415 Returns list of parents changesets.
394 Returns list of parents changesets.
416 """
395 """
417 raise NotImplementedError
396 raise NotImplementedError
418
397
419 @LazyProperty
398 @LazyProperty
420 def children(self):
399 def children(self):
421 """
400 """
422 Returns list of children changesets.
401 Returns list of children changesets.
423 """
402 """
424 raise NotImplementedError
403 raise NotImplementedError
425
404
426 @LazyProperty
405 @LazyProperty
427 def id(self):
406 def id(self):
428 """
407 """
429 Returns string identifying this changeset.
408 Returns string identifying this changeset.
430 """
409 """
431 raise NotImplementedError
410 raise NotImplementedError
432
411
433 @LazyProperty
412 @LazyProperty
434 def raw_id(self):
413 def raw_id(self):
435 """
414 """
436 Returns raw string identifying this changeset.
415 Returns raw string identifying this changeset.
437 """
416 """
438 raise NotImplementedError
417 raise NotImplementedError
439
418
440 @LazyProperty
419 @LazyProperty
441 def short_id(self):
420 def short_id(self):
442 """
421 """
443 Returns shortened version of ``raw_id`` attribute, as string,
422 Returns shortened version of ``raw_id`` attribute, as string,
444 identifying this changeset, useful for web representation.
423 identifying this changeset, useful for web representation.
445 """
424 """
446 raise NotImplementedError
425 raise NotImplementedError
447
426
448 @LazyProperty
427 @LazyProperty
449 def revision(self):
428 def revision(self):
450 """
429 """
451 Returns integer identifying this changeset.
430 Returns integer identifying this changeset.
452
431
453 """
432 """
454 raise NotImplementedError
433 raise NotImplementedError
455
434
456 @LazyProperty
435 @LazyProperty
457 def committer(self):
436 def committer(self):
458 """
437 """
459 Returns Committer for given commit
438 Returns Committer for given commit
460 """
439 """
461
440
462 raise NotImplementedError
441 raise NotImplementedError
463
442
464 @LazyProperty
443 @LazyProperty
465 def committer_name(self):
444 def committer_name(self):
466 """
445 """
467 Returns Author name for given commit
446 Returns Author name for given commit
468 """
447 """
469
448
470 return author_name(self.committer)
449 return author_name(self.committer)
471
450
472 @LazyProperty
451 @LazyProperty
473 def committer_email(self):
452 def committer_email(self):
474 """
453 """
475 Returns Author email address for given commit
454 Returns Author email address for given commit
476 """
455 """
477
456
478 return author_email(self.committer)
457 return author_email(self.committer)
479
458
480 @LazyProperty
459 @LazyProperty
481 def author(self):
460 def author(self):
482 """
461 """
483 Returns Author for given commit
462 Returns Author for given commit
484 """
463 """
485
464
486 raise NotImplementedError
465 raise NotImplementedError
487
466
488 @LazyProperty
467 @LazyProperty
489 def author_name(self):
468 def author_name(self):
490 """
469 """
491 Returns Author name for given commit
470 Returns Author name for given commit
492 """
471 """
493
472
494 return author_name(self.author)
473 return author_name(self.author)
495
474
496 @LazyProperty
475 @LazyProperty
497 def author_email(self):
476 def author_email(self):
498 """
477 """
499 Returns Author email address for given commit
478 Returns Author email address for given commit
500 """
479 """
501
480
502 return author_email(self.author)
481 return author_email(self.author)
503
482
504 def get_file_mode(self, path):
483 def get_file_mode(self, path):
505 """
484 """
506 Returns stat mode of the file at the given ``path``.
485 Returns stat mode of the file at the given ``path``.
507 """
486 """
508 raise NotImplementedError
487 raise NotImplementedError
509
488
510 def get_file_content(self, path):
489 def get_file_content(self, path):
511 """
490 """
512 Returns content of the file at the given ``path``.
491 Returns content of the file at the given ``path``.
513 """
492 """
514 raise NotImplementedError
493 raise NotImplementedError
515
494
516 def get_file_size(self, path):
495 def get_file_size(self, path):
517 """
496 """
518 Returns size of the file at the given ``path``.
497 Returns size of the file at the given ``path``.
519 """
498 """
520 raise NotImplementedError
499 raise NotImplementedError
521
500
522 def get_file_changeset(self, path):
501 def get_file_changeset(self, path):
523 """
502 """
524 Returns last commit of the file at the given ``path``.
503 Returns last commit of the file at the given ``path``.
525 """
504 """
526 raise NotImplementedError
505 raise NotImplementedError
527
506
528 def get_file_history(self, path):
507 def get_file_history(self, path):
529 """
508 """
530 Returns history of file as reversed list of ``Changeset`` objects for
509 Returns history of file as reversed list of ``Changeset`` objects for
531 which file at given ``path`` has been modified.
510 which file at given ``path`` has been modified.
532 """
511 """
533 raise NotImplementedError
512 raise NotImplementedError
534
513
535 def get_nodes(self, path):
514 def get_nodes(self, path):
536 """
515 """
537 Returns combined ``DirNode`` and ``FileNode`` objects list representing
516 Returns combined ``DirNode`` and ``FileNode`` objects list representing
538 state of changeset at the given ``path``.
517 state of changeset at the given ``path``.
539
518
540 :raises ``ChangesetError``: if node at the given ``path`` is not
519 :raises ``ChangesetError``: if node at the given ``path`` is not
541 instance of ``DirNode``
520 instance of ``DirNode``
542 """
521 """
543 raise NotImplementedError
522 raise NotImplementedError
544
523
545 def get_node(self, path):
524 def get_node(self, path):
546 """
525 """
547 Returns ``Node`` object from the given ``path``.
526 Returns ``Node`` object from the given ``path``.
548
527
549 :raises ``NodeDoesNotExistError``: if there is no node at the given
528 :raises ``NodeDoesNotExistError``: if there is no node at the given
550 ``path``
529 ``path``
551 """
530 """
552 raise NotImplementedError
531 raise NotImplementedError
553
532
554 def fill_archive(self, stream=None, kind='tgz', prefix=None):
533 def fill_archive(self, stream=None, kind='tgz', prefix=None):
555 """
534 """
556 Fills up given stream.
535 Fills up given stream.
557
536
558 :param stream: file like object.
537 :param stream: file like object.
559 :param kind: one of following: ``zip``, ``tar``, ``tgz``
538 :param kind: one of following: ``zip``, ``tar``, ``tgz``
560 or ``tbz2``. Default: ``tgz``.
539 or ``tbz2``. Default: ``tgz``.
561 :param prefix: name of root directory in archive.
540 :param prefix: name of root directory in archive.
562 Default is repository name and changeset's raw_id joined with dash.
541 Default is repository name and changeset's raw_id joined with dash.
563
542
564 repo-tip.<kind>
543 repo-tip.<kind>
565 """
544 """
566
545
567 raise NotImplementedError
546 raise NotImplementedError
568
547
569 def get_chunked_archive(self, **kwargs):
548 def get_chunked_archive(self, **kwargs):
570 """
549 """
571 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
550 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
572
551
573 :param chunk_size: extra parameter which controls size of returned
552 :param chunk_size: extra parameter which controls size of returned
574 chunks. Default:8k.
553 chunks. Default:8k.
575 """
554 """
576
555
577 chunk_size = kwargs.pop('chunk_size', 8192)
556 chunk_size = kwargs.pop('chunk_size', 8192)
578 stream = kwargs.get('stream')
557 stream = kwargs.get('stream')
579 self.fill_archive(**kwargs)
558 self.fill_archive(**kwargs)
580 while True:
559 while True:
581 data = stream.read(chunk_size)
560 data = stream.read(chunk_size)
582 if not data:
561 if not data:
583 break
562 break
584 yield data
563 yield data
585
564
586 @LazyProperty
565 @LazyProperty
587 def root(self):
566 def root(self):
588 """
567 """
589 Returns ``RootNode`` object for this changeset.
568 Returns ``RootNode`` object for this changeset.
590 """
569 """
591 return self.get_node('')
570 return self.get_node('')
592
571
593 def next(self, branch=None):
572 def next(self, branch=None):
594 """
573 """
595 Returns next changeset from current, if branch is gives it will return
574 Returns next changeset from current, if branch is gives it will return
596 next changeset belonging to this branch
575 next changeset belonging to this branch
597
576
598 :param branch: show changesets within the given named branch
577 :param branch: show changesets within the given named branch
599 """
578 """
600 raise NotImplementedError
579 raise NotImplementedError
601
580
602 def prev(self, branch=None):
581 def prev(self, branch=None):
603 """
582 """
604 Returns previous changeset from current, if branch is gives it will
583 Returns previous changeset from current, if branch is gives it will
605 return previous changeset belonging to this branch
584 return previous changeset belonging to this branch
606
585
607 :param branch: show changesets within the given named branch
586 :param branch: show changesets within the given named branch
608 """
587 """
609 raise NotImplementedError
588 raise NotImplementedError
610
589
611 @LazyProperty
590 @LazyProperty
612 def added(self):
591 def added(self):
613 """
592 """
614 Returns list of added ``FileNode`` objects.
593 Returns list of added ``FileNode`` objects.
615 """
594 """
616 raise NotImplementedError
595 raise NotImplementedError
617
596
618 @LazyProperty
597 @LazyProperty
619 def changed(self):
598 def changed(self):
620 """
599 """
621 Returns list of modified ``FileNode`` objects.
600 Returns list of modified ``FileNode`` objects.
622 """
601 """
623 raise NotImplementedError
602 raise NotImplementedError
624
603
625 @LazyProperty
604 @LazyProperty
626 def removed(self):
605 def removed(self):
627 """
606 """
628 Returns list of removed ``FileNode`` objects.
607 Returns list of removed ``FileNode`` objects.
629 """
608 """
630 raise NotImplementedError
609 raise NotImplementedError
631
610
632 @LazyProperty
611 @LazyProperty
633 def size(self):
612 def size(self):
634 """
613 """
635 Returns total number of bytes from contents of all filenodes.
614 Returns total number of bytes from contents of all filenodes.
636 """
615 """
637 return sum((node.size for node in self.get_filenodes_generator()))
616 return sum((node.size for node in self.get_filenodes_generator()))
638
617
639 def walk(self, topurl=''):
618 def walk(self, topurl=''):
640 """
619 """
641 Similar to os.walk method. Insted of filesystem it walks through
620 Similar to os.walk method. Insted of filesystem it walks through
642 changeset starting at given ``topurl``. Returns generator of tuples
621 changeset starting at given ``topurl``. Returns generator of tuples
643 (topnode, dirnodes, filenodes).
622 (topnode, dirnodes, filenodes).
644 """
623 """
645 topnode = self.get_node(topurl)
624 topnode = self.get_node(topurl)
646 yield (topnode, topnode.dirs, topnode.files)
625 yield (topnode, topnode.dirs, topnode.files)
647 for dirnode in topnode.dirs:
626 for dirnode in topnode.dirs:
648 for tup in self.walk(dirnode.path):
627 for tup in self.walk(dirnode.path):
649 yield tup
628 yield tup
650
629
651 def get_filenodes_generator(self):
630 def get_filenodes_generator(self):
652 """
631 """
653 Returns generator that yields *all* file nodes.
632 Returns generator that yields *all* file nodes.
654 """
633 """
655 for topnode, dirs, files in self.walk():
634 for topnode, dirs, files in self.walk():
656 for node in files:
635 for node in files:
657 yield node
636 yield node
658
637
659 def as_dict(self):
638 def as_dict(self):
660 """
639 """
661 Returns dictionary with changeset's attributes and their values.
640 Returns dictionary with changeset's attributes and their values.
662 """
641 """
663 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
642 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
664 'revision', 'date', 'message'])
643 'revision', 'date', 'message'])
665 data['author'] = {'name': self.author_name, 'email': self.author_email}
644 data['author'] = {'name': self.author_name, 'email': self.author_email}
666 data['added'] = [node.path for node in self.added]
645 data['added'] = [node.path for node in self.added]
667 data['changed'] = [node.path for node in self.changed]
646 data['changed'] = [node.path for node in self.changed]
668 data['removed'] = [node.path for node in self.removed]
647 data['removed'] = [node.path for node in self.removed]
669 return data
648 return data
670
649
671
650
672 class BaseWorkdir(object):
651 class BaseWorkdir(object):
673 """
652 """
674 Working directory representation of single repository.
653 Working directory representation of single repository.
675
654
676 :attribute: repository: repository object of working directory
655 :attribute: repository: repository object of working directory
677 """
656 """
678
657
679 def __init__(self, repository):
658 def __init__(self, repository):
680 self.repository = repository
659 self.repository = repository
681
660
682 def get_branch(self):
661 def get_branch(self):
683 """
662 """
684 Returns name of current branch.
663 Returns name of current branch.
685 """
664 """
686 raise NotImplementedError
665 raise NotImplementedError
687
666
688 def get_changeset(self):
667 def get_changeset(self):
689 """
668 """
690 Returns current changeset.
669 Returns current changeset.
691 """
670 """
692 raise NotImplementedError
671 raise NotImplementedError
693
672
694 def get_added(self):
673 def get_added(self):
695 """
674 """
696 Returns list of ``FileNode`` objects marked as *new* in working
675 Returns list of ``FileNode`` objects marked as *new* in working
697 directory.
676 directory.
698 """
677 """
699 raise NotImplementedError
678 raise NotImplementedError
700
679
701 def get_changed(self):
680 def get_changed(self):
702 """
681 """
703 Returns list of ``FileNode`` objects *changed* in working directory.
682 Returns list of ``FileNode`` objects *changed* in working directory.
704 """
683 """
705 raise NotImplementedError
684 raise NotImplementedError
706
685
707 def get_removed(self):
686 def get_removed(self):
708 """
687 """
709 Returns list of ``RemovedFileNode`` objects marked as *removed* in
688 Returns list of ``RemovedFileNode`` objects marked as *removed* in
710 working directory.
689 working directory.
711 """
690 """
712 raise NotImplementedError
691 raise NotImplementedError
713
692
714 def get_untracked(self):
693 def get_untracked(self):
715 """
694 """
716 Returns list of ``FileNode`` objects which are present within working
695 Returns list of ``FileNode`` objects which are present within working
717 directory however are not tracked by repository.
696 directory however are not tracked by repository.
718 """
697 """
719 raise NotImplementedError
698 raise NotImplementedError
720
699
721 def get_status(self):
700 def get_status(self):
722 """
701 """
723 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
702 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
724 lists.
703 lists.
725 """
704 """
726 raise NotImplementedError
705 raise NotImplementedError
727
706
728 def commit(self, message, **kwargs):
707 def commit(self, message, **kwargs):
729 """
708 """
730 Commits local (from working directory) changes and returns newly
709 Commits local (from working directory) changes and returns newly
731 created
710 created
732 ``Changeset``. Updates repository's ``revisions`` list.
711 ``Changeset``. Updates repository's ``revisions`` list.
733
712
734 :raises ``CommitError``: if any error occurs while committing
713 :raises ``CommitError``: if any error occurs while committing
735 """
714 """
736 raise NotImplementedError
715 raise NotImplementedError
737
716
738 def update(self, revision=None):
717 def update(self, revision=None):
739 """
718 """
740 Fetches content of the given revision and populates it within working
719 Fetches content of the given revision and populates it within working
741 directory.
720 directory.
742 """
721 """
743 raise NotImplementedError
722 raise NotImplementedError
744
723
745 def checkout_branch(self, branch=None):
724 def checkout_branch(self, branch=None):
746 """
725 """
747 Checks out ``branch`` or the backend's default branch.
726 Checks out ``branch`` or the backend's default branch.
748
727
749 Raises ``BranchDoesNotExistError`` if the branch does not exist.
728 Raises ``BranchDoesNotExistError`` if the branch does not exist.
750 """
729 """
751 raise NotImplementedError
730 raise NotImplementedError
752
731
753
732
754 class BaseInMemoryChangeset(object):
733 class BaseInMemoryChangeset(object):
755 """
734 """
756 Represents differences between repository's state (most recent head) and
735 Represents differences between repository's state (most recent head) and
757 changes made *in place*.
736 changes made *in place*.
758
737
759 **Attributes**
738 **Attributes**
760
739
761 ``repository``
740 ``repository``
762 repository object for this in-memory-changeset
741 repository object for this in-memory-changeset
763
742
764 ``added``
743 ``added``
765 list of ``FileNode`` objects marked as *added*
744 list of ``FileNode`` objects marked as *added*
766
745
767 ``changed``
746 ``changed``
768 list of ``FileNode`` objects marked as *changed*
747 list of ``FileNode`` objects marked as *changed*
769
748
770 ``removed``
749 ``removed``
771 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
750 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
772 *removed*
751 *removed*
773
752
774 ``parents``
753 ``parents``
775 list of ``Changeset`` representing parents of in-memory changeset.
754 list of ``Changeset`` representing parents of in-memory changeset.
776 Should always be 2-element sequence.
755 Should always be 2-element sequence.
777
756
778 """
757 """
779
758
780 def __init__(self, repository):
759 def __init__(self, repository):
781 self.repository = repository
760 self.repository = repository
782 self.added = []
761 self.added = []
783 self.changed = []
762 self.changed = []
784 self.removed = []
763 self.removed = []
785 self.parents = []
764 self.parents = []
786
765
787 def add(self, *filenodes):
766 def add(self, *filenodes):
788 """
767 """
789 Marks given ``FileNode`` objects as *to be committed*.
768 Marks given ``FileNode`` objects as *to be committed*.
790
769
791 :raises ``NodeAlreadyExistsError``: if node with same path exists at
770 :raises ``NodeAlreadyExistsError``: if node with same path exists at
792 latest changeset
771 latest changeset
793 :raises ``NodeAlreadyAddedError``: if node with same path is already
772 :raises ``NodeAlreadyAddedError``: if node with same path is already
794 marked as *added*
773 marked as *added*
795 """
774 """
796 # Check if not already marked as *added* first
775 # Check if not already marked as *added* first
797 for node in filenodes:
776 for node in filenodes:
798 if node.path in (n.path for n in self.added):
777 if node.path in (n.path for n in self.added):
799 raise NodeAlreadyAddedError("Such FileNode %s is already "
778 raise NodeAlreadyAddedError("Such FileNode %s is already "
800 "marked for addition" % node.path)
779 "marked for addition" % node.path)
801 for node in filenodes:
780 for node in filenodes:
802 self.added.append(node)
781 self.added.append(node)
803
782
804 def change(self, *filenodes):
783 def change(self, *filenodes):
805 """
784 """
806 Marks given ``FileNode`` objects to be *changed* in next commit.
785 Marks given ``FileNode`` objects to be *changed* in next commit.
807
786
808 :raises ``EmptyRepositoryError``: if there are no changesets yet
787 :raises ``EmptyRepositoryError``: if there are no changesets yet
809 :raises ``NodeAlreadyExistsError``: if node with same path is already
788 :raises ``NodeAlreadyExistsError``: if node with same path is already
810 marked to be *changed*
789 marked to be *changed*
811 :raises ``NodeAlreadyRemovedError``: if node with same path is already
790 :raises ``NodeAlreadyRemovedError``: if node with same path is already
812 marked to be *removed*
791 marked to be *removed*
813 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
792 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
814 changeset
793 changeset
815 :raises ``NodeNotChangedError``: if node hasn't really be changed
794 :raises ``NodeNotChangedError``: if node hasn't really be changed
816 """
795 """
817 for node in filenodes:
796 for node in filenodes:
818 if node.path in (n.path for n in self.removed):
797 if node.path in (n.path for n in self.removed):
819 raise NodeAlreadyRemovedError("Node at %s is already marked "
798 raise NodeAlreadyRemovedError("Node at %s is already marked "
820 "as removed" % node.path)
799 "as removed" % node.path)
821 try:
800 try:
822 self.repository.get_changeset()
801 self.repository.get_changeset()
823 except EmptyRepositoryError:
802 except EmptyRepositoryError:
824 raise EmptyRepositoryError("Nothing to change - try to *add* new "
803 raise EmptyRepositoryError("Nothing to change - try to *add* new "
825 "nodes rather than changing them")
804 "nodes rather than changing them")
826 for node in filenodes:
805 for node in filenodes:
827 if node.path in (n.path for n in self.changed):
806 if node.path in (n.path for n in self.changed):
828 raise NodeAlreadyChangedError("Node at '%s' is already "
807 raise NodeAlreadyChangedError("Node at '%s' is already "
829 "marked as changed" % node.path)
808 "marked as changed" % node.path)
830 self.changed.append(node)
809 self.changed.append(node)
831
810
832 def remove(self, *filenodes):
811 def remove(self, *filenodes):
833 """
812 """
834 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
813 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
835 *removed* in next commit.
814 *removed* in next commit.
836
815
837 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
816 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
838 be *removed*
817 be *removed*
839 :raises ``NodeAlreadyChangedError``: if node has been already marked to
818 :raises ``NodeAlreadyChangedError``: if node has been already marked to
840 be *changed*
819 be *changed*
841 """
820 """
842 for node in filenodes:
821 for node in filenodes:
843 if node.path in (n.path for n in self.removed):
822 if node.path in (n.path for n in self.removed):
844 raise NodeAlreadyRemovedError("Node is already marked to "
823 raise NodeAlreadyRemovedError("Node is already marked to "
845 "for removal at %s" % node.path)
824 "for removal at %s" % node.path)
846 if node.path in (n.path for n in self.changed):
825 if node.path in (n.path for n in self.changed):
847 raise NodeAlreadyChangedError("Node is already marked to "
826 raise NodeAlreadyChangedError("Node is already marked to "
848 "be changed at %s" % node.path)
827 "be changed at %s" % node.path)
849 # We only mark node as *removed* - real removal is done by
828 # We only mark node as *removed* - real removal is done by
850 # commit method
829 # commit method
851 self.removed.append(node)
830 self.removed.append(node)
852
831
853 def reset(self):
832 def reset(self):
854 """
833 """
855 Resets this instance to initial state (cleans ``added``, ``changed``
834 Resets this instance to initial state (cleans ``added``, ``changed``
856 and ``removed`` lists).
835 and ``removed`` lists).
857 """
836 """
858 self.added = []
837 self.added = []
859 self.changed = []
838 self.changed = []
860 self.removed = []
839 self.removed = []
861 self.parents = []
840 self.parents = []
862
841
863 def get_ipaths(self):
842 def get_ipaths(self):
864 """
843 """
865 Returns generator of paths from nodes marked as added, changed or
844 Returns generator of paths from nodes marked as added, changed or
866 removed.
845 removed.
867 """
846 """
868 for node in chain(self.added, self.changed, self.removed):
847 for node in chain(self.added, self.changed, self.removed):
869 yield node.path
848 yield node.path
870
849
871 def get_paths(self):
850 def get_paths(self):
872 """
851 """
873 Returns list of paths from nodes marked as added, changed or removed.
852 Returns list of paths from nodes marked as added, changed or removed.
874 """
853 """
875 return list(self.get_ipaths())
854 return list(self.get_ipaths())
876
855
877 def check_integrity(self, parents=None):
856 def check_integrity(self, parents=None):
878 """
857 """
879 Checks in-memory changeset's integrity. Also, sets parents if not
858 Checks in-memory changeset's integrity. Also, sets parents if not
880 already set.
859 already set.
881
860
882 :raises CommitError: if any error occurs (i.e.
861 :raises CommitError: if any error occurs (i.e.
883 ``NodeDoesNotExistError``).
862 ``NodeDoesNotExistError``).
884 """
863 """
885 if not self.parents:
864 if not self.parents:
886 parents = parents or []
865 parents = parents or []
887 if len(parents) == 0:
866 if len(parents) == 0:
888 try:
867 try:
889 parents = [self.repository.get_changeset(), None]
868 parents = [self.repository.get_changeset(), None]
890 except EmptyRepositoryError:
869 except EmptyRepositoryError:
891 parents = [None, None]
870 parents = [None, None]
892 elif len(parents) == 1:
871 elif len(parents) == 1:
893 parents += [None]
872 parents += [None]
894 self.parents = parents
873 self.parents = parents
895
874
896 # Local parents, only if not None
875 # Local parents, only if not None
897 parents = [p for p in self.parents if p]
876 parents = [p for p in self.parents if p]
898
877
899 # Check nodes marked as added
878 # Check nodes marked as added
900 for p in parents:
879 for p in parents:
901 for node in self.added:
880 for node in self.added:
902 try:
881 try:
903 p.get_node(node.path)
882 p.get_node(node.path)
904 except NodeDoesNotExistError:
883 except NodeDoesNotExistError:
905 pass
884 pass
906 else:
885 else:
907 raise NodeAlreadyExistsError("Node at %s already exists "
886 raise NodeAlreadyExistsError("Node at %s already exists "
908 "at %s" % (node.path, p))
887 "at %s" % (node.path, p))
909
888
910 # Check nodes marked as changed
889 # Check nodes marked as changed
911 missing = set(self.changed)
890 missing = set(self.changed)
912 not_changed = set(self.changed)
891 not_changed = set(self.changed)
913 if self.changed and not parents:
892 if self.changed and not parents:
914 raise NodeDoesNotExistError(str(self.changed[0].path))
893 raise NodeDoesNotExistError(str(self.changed[0].path))
915 for p in parents:
894 for p in parents:
916 for node in self.changed:
895 for node in self.changed:
917 try:
896 try:
918 old = p.get_node(node.path)
897 old = p.get_node(node.path)
919 missing.remove(node)
898 missing.remove(node)
920 if old.content != node.content:
899 if old.content != node.content:
921 not_changed.remove(node)
900 not_changed.remove(node)
922 except NodeDoesNotExistError:
901 except NodeDoesNotExistError:
923 pass
902 pass
924 if self.changed and missing:
903 if self.changed and missing:
925 raise NodeDoesNotExistError("Node at %s is missing "
904 raise NodeDoesNotExistError("Node at %s is missing "
926 "(parents: %s)" % (node.path, parents))
905 "(parents: %s)" % (node.path, parents))
927
906
928 if self.changed and not_changed:
907 if self.changed and not_changed:
929 raise NodeNotChangedError("Node at %s wasn't actually changed "
908 raise NodeNotChangedError("Node at %s wasn't actually changed "
930 "since parents' changesets: %s" % (not_changed.pop().path,
909 "since parents' changesets: %s" % (not_changed.pop().path,
931 parents)
910 parents)
932 )
911 )
933
912
934 # Check nodes marked as removed
913 # Check nodes marked as removed
935 if self.removed and not parents:
914 if self.removed and not parents:
936 raise NodeDoesNotExistError("Cannot remove node at %s as there "
915 raise NodeDoesNotExistError("Cannot remove node at %s as there "
937 "were no parents specified" % self.removed[0].path)
916 "were no parents specified" % self.removed[0].path)
938 really_removed = set()
917 really_removed = set()
939 for p in parents:
918 for p in parents:
940 for node in self.removed:
919 for node in self.removed:
941 try:
920 try:
942 p.get_node(node.path)
921 p.get_node(node.path)
943 really_removed.add(node)
922 really_removed.add(node)
944 except ChangesetError:
923 except ChangesetError:
945 pass
924 pass
946 not_removed = set(self.removed) - really_removed
925 not_removed = set(self.removed) - really_removed
947 if not_removed:
926 if not_removed:
948 raise NodeDoesNotExistError("Cannot remove node at %s from "
927 raise NodeDoesNotExistError("Cannot remove node at %s from "
949 "following parents: %s" % (not_removed[0], parents))
928 "following parents: %s" % (not_removed[0], parents))
950
929
951 def commit(self, message, author, parents=None, branch=None, date=None,
930 def commit(self, message, author, parents=None, branch=None, date=None,
952 **kwargs):
931 **kwargs):
953 """
932 """
954 Performs in-memory commit (doesn't check workdir in any way) and
933 Performs in-memory commit (doesn't check workdir in any way) and
955 returns newly created ``Changeset``. Updates repository's
934 returns newly created ``Changeset``. Updates repository's
956 ``revisions``.
935 ``revisions``.
957
936
958 .. note::
937 .. note::
959 While overriding this method each backend's should call
938 While overriding this method each backend's should call
960 ``self.check_integrity(parents)`` in the first place.
939 ``self.check_integrity(parents)`` in the first place.
961
940
962 :param message: message of the commit
941 :param message: message of the commit
963 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
942 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
964 :param parents: single parent or sequence of parents from which commit
943 :param parents: single parent or sequence of parents from which commit
965 would be derieved
944 would be derieved
966 :param date: ``datetime.datetime`` instance. Defaults to
945 :param date: ``datetime.datetime`` instance. Defaults to
967 ``datetime.datetime.now()``.
946 ``datetime.datetime.now()``.
968 :param branch: branch name, as string. If none given, default backend's
947 :param branch: branch name, as string. If none given, default backend's
969 branch would be used.
948 branch would be used.
970
949
971 :raises ``CommitError``: if any error occurs while committing
950 :raises ``CommitError``: if any error occurs while committing
972 """
951 """
973 raise NotImplementedError
952 raise NotImplementedError
974
953
975
954
976 class EmptyChangeset(BaseChangeset):
955 class EmptyChangeset(BaseChangeset):
977 """
956 """
978 An dummy empty changeset. It's possible to pass hash when creating
957 An dummy empty changeset. It's possible to pass hash when creating
979 an EmptyChangeset
958 an EmptyChangeset
980 """
959 """
981
960
982 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
961 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
983 alias=None, revision=-1, message='', author='', date=None):
962 alias=None, revision=-1, message='', author='', date=None):
984 self._empty_cs = cs
963 self._empty_cs = cs
985 self.revision = revision
964 self.revision = revision
986 self.message = message
965 self.message = message
987 self.author = author
966 self.author = author
988 self.date = date or datetime.datetime.fromtimestamp(0)
967 self.date = date or datetime.datetime.fromtimestamp(0)
989 self.repository = repo
968 self.repository = repo
990 self.requested_revision = requested_revision
969 self.requested_revision = requested_revision
991 self.alias = alias
970 self.alias = alias
992
971
993 @LazyProperty
972 @LazyProperty
994 def raw_id(self):
973 def raw_id(self):
995 """
974 """
996 Returns raw string identifying this changeset, useful for web
975 Returns raw string identifying this changeset, useful for web
997 representation.
976 representation.
998 """
977 """
999
978
1000 return self._empty_cs
979 return self._empty_cs
1001
980
1002 @LazyProperty
981 @LazyProperty
1003 def branch(self):
982 def branch(self):
1004 from rhodecode.lib.vcs.backends import get_backend
983 from rhodecode.lib.vcs.backends import get_backend
1005 return get_backend(self.alias).DEFAULT_BRANCH_NAME
984 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1006
985
1007 @LazyProperty
986 @LazyProperty
1008 def short_id(self):
987 def short_id(self):
1009 return self.raw_id[:12]
988 return self.raw_id[:12]
1010
989
1011 def get_file_changeset(self, path):
990 def get_file_changeset(self, path):
1012 return self
991 return self
1013
992
1014 def get_file_content(self, path):
993 def get_file_content(self, path):
1015 return u''
994 return u''
1016
995
1017 def get_file_size(self, path):
996 def get_file_size(self, path):
1018 return 0
997 return 0
@@ -1,698 +1,691 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git
3 vcs.backends.git
4 ~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~
5
5
6 Git backend implementation.
6 Git backend implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import re
13 import re
14 import time
14 import time
15 import posixpath
15 import posixpath
16 import logging
16 import logging
17 import traceback
17 import traceback
18 import urllib
18 import urllib
19 import urllib2
19 import urllib2
20 from dulwich.repo import Repo, NotGitRepository
20 from dulwich.repo import Repo, NotGitRepository
21 from dulwich.objects import Tag
21 from dulwich.objects import Tag
22 from string import Template
22 from string import Template
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.lib.vcs.backends.base import BaseRepository
25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 from rhodecode.lib.vcs.utils.paths import abspath
35 from rhodecode.lib.vcs.utils.paths import abspath
36 from rhodecode.lib.vcs.utils.paths import get_user_home
36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 from .workdir import GitWorkdir
37 from .workdir import GitWorkdir
38 from .changeset import GitChangeset
38 from .changeset import GitChangeset
39 from .inmemory import GitInMemoryChangeset
39 from .inmemory import GitInMemoryChangeset
40 from .config import ConfigFile
40 from .config import ConfigFile
41 from rhodecode.lib import subprocessio
41 from rhodecode.lib import subprocessio
42
42
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
48 """
48 """
49 Git repository backend.
49 Git repository backend.
50 """
50 """
51 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
52 scm = 'git'
52 scm = 'git'
53
53
54 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
55 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
56
56
57 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 self.bare = repo.bare
59 self.bare = repo.bare
60
60
61 self._config_files = [
61 self._config_files = [
62 bare and abspath(self.path, 'config')
62 bare and abspath(self.path, 'config')
63 or abspath(self.path, '.git', 'config'),
63 or abspath(self.path, '.git', 'config'),
64 abspath(get_user_home(), '.gitconfig'),
64 abspath(get_user_home(), '.gitconfig'),
65 ]
65 ]
66
66
67 @ThreadLocalLazyProperty
67 @ThreadLocalLazyProperty
68 def _repo(self):
68 def _repo(self):
69 repo = Repo(self.path)
69 return Repo(self.path)
70 # patch the instance of GitRepo with an "FAKE" ui object to add
71 # compatibility layer with Mercurial
72 if not hasattr(repo, 'ui'):
73 from mercurial.ui import ui
74 baseui = ui()
75 setattr(repo, 'ui', baseui)
76 return repo
77
70
78 @property
71 @property
79 def head(self):
72 def head(self):
80 try:
73 try:
81 return self._repo.head()
74 return self._repo.head()
82 except KeyError:
75 except KeyError:
83 return None
76 return None
84
77
85 @LazyProperty
78 @LazyProperty
86 def revisions(self):
79 def revisions(self):
87 """
80 """
88 Returns list of revisions' ids, in ascending order. Being lazy
81 Returns list of revisions' ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
82 attribute allows external tools to inject shas from cache.
90 """
83 """
91 return self._get_all_revisions()
84 return self._get_all_revisions()
92
85
93 @classmethod
86 @classmethod
94 def _run_git_command(cls, cmd, **opts):
87 def _run_git_command(cls, cmd, **opts):
95 """
88 """
96 Runs given ``cmd`` as git command and returns tuple
89 Runs given ``cmd`` as git command and returns tuple
97 (stdout, stderr).
90 (stdout, stderr).
98
91
99 :param cmd: git command to be executed
92 :param cmd: git command to be executed
100 :param opts: env options to pass into Subprocess command
93 :param opts: env options to pass into Subprocess command
101 """
94 """
102
95
103 if '_bare' in opts:
96 if '_bare' in opts:
104 _copts = []
97 _copts = []
105 del opts['_bare']
98 del opts['_bare']
106 else:
99 else:
107 _copts = ['-c', 'core.quotepath=false', ]
100 _copts = ['-c', 'core.quotepath=false', ]
108 safe_call = False
101 safe_call = False
109 if '_safe' in opts:
102 if '_safe' in opts:
110 #no exc on failure
103 #no exc on failure
111 del opts['_safe']
104 del opts['_safe']
112 safe_call = True
105 safe_call = True
113
106
114 _str_cmd = False
107 _str_cmd = False
115 if isinstance(cmd, basestring):
108 if isinstance(cmd, basestring):
116 cmd = [cmd]
109 cmd = [cmd]
117 _str_cmd = True
110 _str_cmd = True
118
111
119 gitenv = os.environ
112 gitenv = os.environ
120 # need to clean fix GIT_DIR !
113 # need to clean fix GIT_DIR !
121 if 'GIT_DIR' in gitenv:
114 if 'GIT_DIR' in gitenv:
122 del gitenv['GIT_DIR']
115 del gitenv['GIT_DIR']
123 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
124
117
125 _git_path = rhodecode.CONFIG.get('git_path', 'git')
118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
126 cmd = [_git_path] + _copts + cmd
119 cmd = [_git_path] + _copts + cmd
127 if _str_cmd:
120 if _str_cmd:
128 cmd = ' '.join(cmd)
121 cmd = ' '.join(cmd)
129 try:
122 try:
130 _opts = dict(
123 _opts = dict(
131 env=gitenv,
124 env=gitenv,
132 shell=False,
125 shell=False,
133 )
126 )
134 _opts.update(opts)
127 _opts.update(opts)
135 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
136 except (EnvironmentError, OSError), err:
129 except (EnvironmentError, OSError), err:
137 tb_err = ("Couldn't run git command (%s).\n"
130 tb_err = ("Couldn't run git command (%s).\n"
138 "Original error was:%s\n" % (cmd, err))
131 "Original error was:%s\n" % (cmd, err))
139 log.error(tb_err)
132 log.error(tb_err)
140 if safe_call:
133 if safe_call:
141 return '', err
134 return '', err
142 else:
135 else:
143 raise RepositoryError(tb_err)
136 raise RepositoryError(tb_err)
144
137
145 return ''.join(p.output), ''.join(p.error)
138 return ''.join(p.output), ''.join(p.error)
146
139
147 def run_git_command(self, cmd):
140 def run_git_command(self, cmd):
148 opts = {}
141 opts = {}
149 if os.path.isdir(self.path):
142 if os.path.isdir(self.path):
150 opts['cwd'] = self.path
143 opts['cwd'] = self.path
151 return self._run_git_command(cmd, **opts)
144 return self._run_git_command(cmd, **opts)
152
145
153 @classmethod
146 @classmethod
154 def _check_url(cls, url):
147 def _check_url(cls, url):
155 """
148 """
156 Functon will check given url and try to verify if it's a valid
149 Functon will check given url and try to verify if it's a valid
157 link. Sometimes it may happened that mercurial will issue basic
150 link. Sometimes it may happened that mercurial will issue basic
158 auth request that can cause whole API to hang when used from python
151 auth request that can cause whole API to hang when used from python
159 or other external calls.
152 or other external calls.
160
153
161 On failures it'll raise urllib2.HTTPError
154 On failures it'll raise urllib2.HTTPError
162 """
155 """
163 from mercurial.util import url as Url
156 from mercurial.util import url as Url
164
157
165 # those authnadlers are patched for python 2.6.5 bug an
158 # those authnadlers are patched for python 2.6.5 bug an
166 # infinit looping when given invalid resources
159 # infinit looping when given invalid resources
167 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
168
161
169 # check first if it's not an local url
162 # check first if it's not an local url
170 if os.path.isdir(url) or url.startswith('file:'):
163 if os.path.isdir(url) or url.startswith('file:'):
171 return True
164 return True
172
165
173 if('+' in url[:url.find('://')]):
166 if('+' in url[:url.find('://')]):
174 url = url[url.find('+') + 1:]
167 url = url[url.find('+') + 1:]
175
168
176 handlers = []
169 handlers = []
177 test_uri, authinfo = Url(url).authinfo()
170 test_uri, authinfo = Url(url).authinfo()
178 if not test_uri.endswith('info/refs'):
171 if not test_uri.endswith('info/refs'):
179 test_uri = test_uri.rstrip('/') + '/info/refs'
172 test_uri = test_uri.rstrip('/') + '/info/refs'
180 if authinfo:
173 if authinfo:
181 #create a password manager
174 #create a password manager
182 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
183 passmgr.add_password(*authinfo)
176 passmgr.add_password(*authinfo)
184
177
185 handlers.extend((httpbasicauthhandler(passmgr),
178 handlers.extend((httpbasicauthhandler(passmgr),
186 httpdigestauthhandler(passmgr)))
179 httpdigestauthhandler(passmgr)))
187
180
188 o = urllib2.build_opener(*handlers)
181 o = urllib2.build_opener(*handlers)
189 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
190
183
191 q = {"service": 'git-upload-pack'}
184 q = {"service": 'git-upload-pack'}
192 qs = '?%s' % urllib.urlencode(q)
185 qs = '?%s' % urllib.urlencode(q)
193 cu = "%s%s" % (test_uri, qs)
186 cu = "%s%s" % (test_uri, qs)
194 req = urllib2.Request(cu, None, {})
187 req = urllib2.Request(cu, None, {})
195
188
196 try:
189 try:
197 resp = o.open(req)
190 resp = o.open(req)
198 return resp.code == 200
191 return resp.code == 200
199 except Exception, e:
192 except Exception, e:
200 # means it cannot be cloned
193 # means it cannot be cloned
201 raise urllib2.URLError("[%s] %s" % (url, e))
194 raise urllib2.URLError("[%s] %s" % (url, e))
202
195
203 def _get_repo(self, create, src_url=None, update_after_clone=False,
196 def _get_repo(self, create, src_url=None, update_after_clone=False,
204 bare=False):
197 bare=False):
205 if create and os.path.exists(self.path):
198 if create and os.path.exists(self.path):
206 raise RepositoryError("Location already exist")
199 raise RepositoryError("Location already exist")
207 if src_url and not create:
200 if src_url and not create:
208 raise RepositoryError("Create should be set to True if src_url is "
201 raise RepositoryError("Create should be set to True if src_url is "
209 "given (clone operation creates repository)")
202 "given (clone operation creates repository)")
210 try:
203 try:
211 if create and src_url:
204 if create and src_url:
212 GitRepository._check_url(src_url)
205 GitRepository._check_url(src_url)
213 self.clone(src_url, update_after_clone, bare)
206 self.clone(src_url, update_after_clone, bare)
214 return Repo(self.path)
207 return Repo(self.path)
215 elif create:
208 elif create:
216 os.mkdir(self.path)
209 os.mkdir(self.path)
217 if bare:
210 if bare:
218 return Repo.init_bare(self.path)
211 return Repo.init_bare(self.path)
219 else:
212 else:
220 return Repo.init(self.path)
213 return Repo.init(self.path)
221 else:
214 else:
222 return self._repo
215 return self._repo
223 except (NotGitRepository, OSError), err:
216 except (NotGitRepository, OSError), err:
224 raise RepositoryError(err)
217 raise RepositoryError(err)
225
218
226 def _get_all_revisions(self):
219 def _get_all_revisions(self):
227 # we must check if this repo is not empty, since later command
220 # we must check if this repo is not empty, since later command
228 # fails if it is. And it's cheaper to ask than throw the subprocess
221 # fails if it is. And it's cheaper to ask than throw the subprocess
229 # errors
222 # errors
230 try:
223 try:
231 self._repo.head()
224 self._repo.head()
232 except KeyError:
225 except KeyError:
233 return []
226 return []
234 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
235 '--all').strip()
228 '--all').strip()
236 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
237 try:
230 try:
238 so, se = self.run_git_command(cmd)
231 so, se = self.run_git_command(cmd)
239 except RepositoryError:
232 except RepositoryError:
240 # Can be raised for empty repositories
233 # Can be raised for empty repositories
241 return []
234 return []
242 return so.splitlines()
235 return so.splitlines()
243
236
244 def _get_all_revisions2(self):
237 def _get_all_revisions2(self):
245 #alternate implementation using dulwich
238 #alternate implementation using dulwich
246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
247 if x[1][1] != 'T']
240 if x[1][1] != 'T']
248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
249
242
250 def _get_revision(self, revision):
243 def _get_revision(self, revision):
251 """
244 """
252 For git backend we always return integer here. This way we ensure
245 For git backend we always return integer here. This way we ensure
253 that changset's revision attribute would become integer.
246 that changset's revision attribute would become integer.
254 """
247 """
255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
256 is_bstr = lambda o: isinstance(o, (str, unicode))
249 is_bstr = lambda o: isinstance(o, (str, unicode))
257 is_null = lambda o: len(o) == revision.count('0')
250 is_null = lambda o: len(o) == revision.count('0')
258
251
259 if len(self.revisions) == 0:
252 if len(self.revisions) == 0:
260 raise EmptyRepositoryError("There are no changesets yet")
253 raise EmptyRepositoryError("There are no changesets yet")
261
254
262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
263 revision = self.revisions[-1]
256 revision = self.revisions[-1]
264
257
265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
266 or isinstance(revision, int) or is_null(revision)):
259 or isinstance(revision, int) or is_null(revision)):
267 try:
260 try:
268 revision = self.revisions[int(revision)]
261 revision = self.revisions[int(revision)]
269 except:
262 except:
270 raise ChangesetDoesNotExistError("Revision %s does not exist "
263 raise ChangesetDoesNotExistError("Revision %s does not exist "
271 "for this repository" % (revision))
264 "for this repository" % (revision))
272
265
273 elif is_bstr(revision):
266 elif is_bstr(revision):
274 # get by branch/tag name
267 # get by branch/tag name
275 _ref_revision = self._parsed_refs.get(revision)
268 _ref_revision = self._parsed_refs.get(revision)
276 _tags_shas = self.tags.values()
269 _tags_shas = self.tags.values()
277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
278 return _ref_revision[0]
271 return _ref_revision[0]
279
272
280 # maybe it's a tag ? we don't have them in self.revisions
273 # maybe it's a tag ? we don't have them in self.revisions
281 elif revision in _tags_shas:
274 elif revision in _tags_shas:
282 return _tags_shas[_tags_shas.index(revision)]
275 return _tags_shas[_tags_shas.index(revision)]
283
276
284 elif not pattern.match(revision) or revision not in self.revisions:
277 elif not pattern.match(revision) or revision not in self.revisions:
285 raise ChangesetDoesNotExistError("Revision %s does not exist "
278 raise ChangesetDoesNotExistError("Revision %s does not exist "
286 "for this repository" % (revision))
279 "for this repository" % (revision))
287
280
288 # Ensure we return full id
281 # Ensure we return full id
289 if not pattern.match(str(revision)):
282 if not pattern.match(str(revision)):
290 raise ChangesetDoesNotExistError("Given revision %s not recognized"
283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
291 % revision)
284 % revision)
292 return revision
285 return revision
293
286
294 def _get_archives(self, archive_name='tip'):
287 def _get_archives(self, archive_name='tip'):
295
288
296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
297 yield {"type": i[0], "extension": i[1], "node": archive_name}
290 yield {"type": i[0], "extension": i[1], "node": archive_name}
298
291
299 def _get_url(self, url):
292 def _get_url(self, url):
300 """
293 """
301 Returns normalized url. If schema is not given, would fall to
294 Returns normalized url. If schema is not given, would fall to
302 filesystem (``file:///``) schema.
295 filesystem (``file:///``) schema.
303 """
296 """
304 url = str(url)
297 url = str(url)
305 if url != 'default' and not '://' in url:
298 if url != 'default' and not '://' in url:
306 url = ':///'.join(('file', url))
299 url = ':///'.join(('file', url))
307 return url
300 return url
308
301
309 def get_hook_location(self):
302 def get_hook_location(self):
310 """
303 """
311 returns absolute path to location where hooks are stored
304 returns absolute path to location where hooks are stored
312 """
305 """
313 loc = os.path.join(self.path, 'hooks')
306 loc = os.path.join(self.path, 'hooks')
314 if not self.bare:
307 if not self.bare:
315 loc = os.path.join(self.path, '.git', 'hooks')
308 loc = os.path.join(self.path, '.git', 'hooks')
316 return loc
309 return loc
317
310
318 @LazyProperty
311 @LazyProperty
319 def name(self):
312 def name(self):
320 return os.path.basename(self.path)
313 return os.path.basename(self.path)
321
314
322 @LazyProperty
315 @LazyProperty
323 def last_change(self):
316 def last_change(self):
324 """
317 """
325 Returns last change made on this repository as datetime object
318 Returns last change made on this repository as datetime object
326 """
319 """
327 return date_fromtimestamp(self._get_mtime(), makedate()[1])
320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
328
321
329 def _get_mtime(self):
322 def _get_mtime(self):
330 try:
323 try:
331 return time.mktime(self.get_changeset().date.timetuple())
324 return time.mktime(self.get_changeset().date.timetuple())
332 except RepositoryError:
325 except RepositoryError:
333 idx_loc = '' if self.bare else '.git'
326 idx_loc = '' if self.bare else '.git'
334 # fallback to filesystem
327 # fallback to filesystem
335 in_path = os.path.join(self.path, idx_loc, "index")
328 in_path = os.path.join(self.path, idx_loc, "index")
336 he_path = os.path.join(self.path, idx_loc, "HEAD")
329 he_path = os.path.join(self.path, idx_loc, "HEAD")
337 if os.path.exists(in_path):
330 if os.path.exists(in_path):
338 return os.stat(in_path).st_mtime
331 return os.stat(in_path).st_mtime
339 else:
332 else:
340 return os.stat(he_path).st_mtime
333 return os.stat(he_path).st_mtime
341
334
342 @LazyProperty
335 @LazyProperty
343 def description(self):
336 def description(self):
344 idx_loc = '' if self.bare else '.git'
337 idx_loc = '' if self.bare else '.git'
345 undefined_description = u'unknown'
338 undefined_description = u'unknown'
346 description_path = os.path.join(self.path, idx_loc, 'description')
339 description_path = os.path.join(self.path, idx_loc, 'description')
347 if os.path.isfile(description_path):
340 if os.path.isfile(description_path):
348 return safe_unicode(open(description_path).read())
341 return safe_unicode(open(description_path).read())
349 else:
342 else:
350 return undefined_description
343 return undefined_description
351
344
352 @LazyProperty
345 @LazyProperty
353 def contact(self):
346 def contact(self):
354 undefined_contact = u'Unknown'
347 undefined_contact = u'Unknown'
355 return undefined_contact
348 return undefined_contact
356
349
357 @property
350 @property
358 def branches(self):
351 def branches(self):
359 if not self.revisions:
352 if not self.revisions:
360 return {}
353 return {}
361 sortkey = lambda ctx: ctx[0]
354 sortkey = lambda ctx: ctx[0]
362 _branches = [(x[0], x[1][0])
355 _branches = [(x[0], x[1][0])
363 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
364 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
365
358
366 @LazyProperty
359 @LazyProperty
367 def tags(self):
360 def tags(self):
368 return self._get_tags()
361 return self._get_tags()
369
362
370 def _get_tags(self):
363 def _get_tags(self):
371 if not self.revisions:
364 if not self.revisions:
372 return {}
365 return {}
373
366
374 sortkey = lambda ctx: ctx[0]
367 sortkey = lambda ctx: ctx[0]
375 _tags = [(x[0], x[1][0])
368 _tags = [(x[0], x[1][0])
376 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
377 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
378
371
379 def tag(self, name, user, revision=None, message=None, date=None,
372 def tag(self, name, user, revision=None, message=None, date=None,
380 **kwargs):
373 **kwargs):
381 """
374 """
382 Creates and returns a tag for the given ``revision``.
375 Creates and returns a tag for the given ``revision``.
383
376
384 :param name: name for new tag
377 :param name: name for new tag
385 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
386 :param revision: changeset id for which new tag would be created
379 :param revision: changeset id for which new tag would be created
387 :param message: message of the tag's commit
380 :param message: message of the tag's commit
388 :param date: date of tag's commit
381 :param date: date of tag's commit
389
382
390 :raises TagAlreadyExistError: if tag with same name already exists
383 :raises TagAlreadyExistError: if tag with same name already exists
391 """
384 """
392 if name in self.tags:
385 if name in self.tags:
393 raise TagAlreadyExistError("Tag %s already exists" % name)
386 raise TagAlreadyExistError("Tag %s already exists" % name)
394 changeset = self.get_changeset(revision)
387 changeset = self.get_changeset(revision)
395 message = message or "Added tag %s for commit %s" % (name,
388 message = message or "Added tag %s for commit %s" % (name,
396 changeset.raw_id)
389 changeset.raw_id)
397 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
398
391
399 self._parsed_refs = self._get_parsed_refs()
392 self._parsed_refs = self._get_parsed_refs()
400 self.tags = self._get_tags()
393 self.tags = self._get_tags()
401 return changeset
394 return changeset
402
395
403 def remove_tag(self, name, user, message=None, date=None):
396 def remove_tag(self, name, user, message=None, date=None):
404 """
397 """
405 Removes tag with the given ``name``.
398 Removes tag with the given ``name``.
406
399
407 :param name: name of the tag to be removed
400 :param name: name of the tag to be removed
408 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
409 :param message: message of the tag's removal commit
402 :param message: message of the tag's removal commit
410 :param date: date of tag's removal commit
403 :param date: date of tag's removal commit
411
404
412 :raises TagDoesNotExistError: if tag with given name does not exists
405 :raises TagDoesNotExistError: if tag with given name does not exists
413 """
406 """
414 if name not in self.tags:
407 if name not in self.tags:
415 raise TagDoesNotExistError("Tag %s does not exist" % name)
408 raise TagDoesNotExistError("Tag %s does not exist" % name)
416 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
417 try:
410 try:
418 os.remove(tagpath)
411 os.remove(tagpath)
419 self._parsed_refs = self._get_parsed_refs()
412 self._parsed_refs = self._get_parsed_refs()
420 self.tags = self._get_tags()
413 self.tags = self._get_tags()
421 except OSError, e:
414 except OSError, e:
422 raise RepositoryError(e.strerror)
415 raise RepositoryError(e.strerror)
423
416
424 @LazyProperty
417 @LazyProperty
425 def _parsed_refs(self):
418 def _parsed_refs(self):
426 return self._get_parsed_refs()
419 return self._get_parsed_refs()
427
420
428 def _get_parsed_refs(self):
421 def _get_parsed_refs(self):
429 refs = self._repo.get_refs()
422 refs = self._repo.get_refs()
430 keys = [('refs/heads/', 'H'),
423 keys = [('refs/heads/', 'H'),
431 ('refs/remotes/origin/', 'RH'),
424 ('refs/remotes/origin/', 'RH'),
432 ('refs/tags/', 'T')]
425 ('refs/tags/', 'T')]
433 _refs = {}
426 _refs = {}
434 for ref, sha in refs.iteritems():
427 for ref, sha in refs.iteritems():
435 for k, type_ in keys:
428 for k, type_ in keys:
436 if ref.startswith(k):
429 if ref.startswith(k):
437 _key = ref[len(k):]
430 _key = ref[len(k):]
438 if type_ == 'T':
431 if type_ == 'T':
439 obj = self._repo.get_object(sha)
432 obj = self._repo.get_object(sha)
440 if isinstance(obj, Tag):
433 if isinstance(obj, Tag):
441 sha = self._repo.get_object(sha).object[1]
434 sha = self._repo.get_object(sha).object[1]
442 _refs[_key] = [sha, type_]
435 _refs[_key] = [sha, type_]
443 break
436 break
444 return _refs
437 return _refs
445
438
446 def _heads(self, reverse=False):
439 def _heads(self, reverse=False):
447 refs = self._repo.get_refs()
440 refs = self._repo.get_refs()
448 heads = {}
441 heads = {}
449
442
450 for key, val in refs.items():
443 for key, val in refs.items():
451 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
444 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
452 if key.startswith(ref_key):
445 if key.startswith(ref_key):
453 n = key[len(ref_key):]
446 n = key[len(ref_key):]
454 if n not in ['HEAD']:
447 if n not in ['HEAD']:
455 heads[n] = val
448 heads[n] = val
456
449
457 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
450 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
458
451
459 def get_changeset(self, revision=None):
452 def get_changeset(self, revision=None):
460 """
453 """
461 Returns ``GitChangeset`` object representing commit from git repository
454 Returns ``GitChangeset`` object representing commit from git repository
462 at the given revision or head (most recent commit) if None given.
455 at the given revision or head (most recent commit) if None given.
463 """
456 """
464 if isinstance(revision, GitChangeset):
457 if isinstance(revision, GitChangeset):
465 return revision
458 return revision
466 revision = self._get_revision(revision)
459 revision = self._get_revision(revision)
467 changeset = GitChangeset(repository=self, revision=revision)
460 changeset = GitChangeset(repository=self, revision=revision)
468 return changeset
461 return changeset
469
462
470 def get_changesets(self, start=None, end=None, start_date=None,
463 def get_changesets(self, start=None, end=None, start_date=None,
471 end_date=None, branch_name=None, reverse=False):
464 end_date=None, branch_name=None, reverse=False):
472 """
465 """
473 Returns iterator of ``GitChangeset`` objects from start to end (both
466 Returns iterator of ``GitChangeset`` objects from start to end (both
474 are inclusive), in ascending date order (unless ``reverse`` is set).
467 are inclusive), in ascending date order (unless ``reverse`` is set).
475
468
476 :param start: changeset ID, as str; first returned changeset
469 :param start: changeset ID, as str; first returned changeset
477 :param end: changeset ID, as str; last returned changeset
470 :param end: changeset ID, as str; last returned changeset
478 :param start_date: if specified, changesets with commit date less than
471 :param start_date: if specified, changesets with commit date less than
479 ``start_date`` would be filtered out from returned set
472 ``start_date`` would be filtered out from returned set
480 :param end_date: if specified, changesets with commit date greater than
473 :param end_date: if specified, changesets with commit date greater than
481 ``end_date`` would be filtered out from returned set
474 ``end_date`` would be filtered out from returned set
482 :param branch_name: if specified, changesets not reachable from given
475 :param branch_name: if specified, changesets not reachable from given
483 branch would be filtered out from returned set
476 branch would be filtered out from returned set
484 :param reverse: if ``True``, returned generator would be reversed
477 :param reverse: if ``True``, returned generator would be reversed
485 (meaning that returned changesets would have descending date order)
478 (meaning that returned changesets would have descending date order)
486
479
487 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 :raise BranchDoesNotExistError: If given ``branch_name`` does not
488 exist.
481 exist.
489 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
482 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
490 ``end`` could not be found.
483 ``end`` could not be found.
491
484
492 """
485 """
493 if branch_name and branch_name not in self.branches:
486 if branch_name and branch_name not in self.branches:
494 raise BranchDoesNotExistError("Branch '%s' not found" \
487 raise BranchDoesNotExistError("Branch '%s' not found" \
495 % branch_name)
488 % branch_name)
496 # %H at format means (full) commit hash, initial hashes are retrieved
489 # %H at format means (full) commit hash, initial hashes are retrieved
497 # in ascending date order
490 # in ascending date order
498 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
491 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
499 cmd_params = {}
492 cmd_params = {}
500 if start_date:
493 if start_date:
501 cmd_template += ' --since "$since"'
494 cmd_template += ' --since "$since"'
502 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
495 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
503 if end_date:
496 if end_date:
504 cmd_template += ' --until "$until"'
497 cmd_template += ' --until "$until"'
505 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
498 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
506 if branch_name:
499 if branch_name:
507 cmd_template += ' $branch_name'
500 cmd_template += ' $branch_name'
508 cmd_params['branch_name'] = branch_name
501 cmd_params['branch_name'] = branch_name
509 else:
502 else:
510 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
503 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
511 '--all').strip()
504 '--all').strip()
512 cmd_template += ' %s' % (rev_filter)
505 cmd_template += ' %s' % (rev_filter)
513
506
514 cmd = Template(cmd_template).safe_substitute(**cmd_params)
507 cmd = Template(cmd_template).safe_substitute(**cmd_params)
515 revs = self.run_git_command(cmd)[0].splitlines()
508 revs = self.run_git_command(cmd)[0].splitlines()
516 start_pos = 0
509 start_pos = 0
517 end_pos = len(revs)
510 end_pos = len(revs)
518 if start:
511 if start:
519 _start = self._get_revision(start)
512 _start = self._get_revision(start)
520 try:
513 try:
521 start_pos = revs.index(_start)
514 start_pos = revs.index(_start)
522 except ValueError:
515 except ValueError:
523 pass
516 pass
524
517
525 if end is not None:
518 if end is not None:
526 _end = self._get_revision(end)
519 _end = self._get_revision(end)
527 try:
520 try:
528 end_pos = revs.index(_end)
521 end_pos = revs.index(_end)
529 except ValueError:
522 except ValueError:
530 pass
523 pass
531
524
532 if None not in [start, end] and start_pos > end_pos:
525 if None not in [start, end] and start_pos > end_pos:
533 raise RepositoryError('start cannot be after end')
526 raise RepositoryError('start cannot be after end')
534
527
535 if end_pos is not None:
528 if end_pos is not None:
536 end_pos += 1
529 end_pos += 1
537
530
538 revs = revs[start_pos:end_pos]
531 revs = revs[start_pos:end_pos]
539 if reverse:
532 if reverse:
540 revs = reversed(revs)
533 revs = reversed(revs)
541 for rev in revs:
534 for rev in revs:
542 yield self.get_changeset(rev)
535 yield self.get_changeset(rev)
543
536
544 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
537 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
545 context=3):
538 context=3):
546 """
539 """
547 Returns (git like) *diff*, as plain text. Shows changes introduced by
540 Returns (git like) *diff*, as plain text. Shows changes introduced by
548 ``rev2`` since ``rev1``.
541 ``rev2`` since ``rev1``.
549
542
550 :param rev1: Entry point from which diff is shown. Can be
543 :param rev1: Entry point from which diff is shown. Can be
551 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
544 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
552 the changes since empty state of the repository until ``rev2``
545 the changes since empty state of the repository until ``rev2``
553 :param rev2: Until which revision changes should be shown.
546 :param rev2: Until which revision changes should be shown.
554 :param ignore_whitespace: If set to ``True``, would not show whitespace
547 :param ignore_whitespace: If set to ``True``, would not show whitespace
555 changes. Defaults to ``False``.
548 changes. Defaults to ``False``.
556 :param context: How many lines before/after changed lines should be
549 :param context: How many lines before/after changed lines should be
557 shown. Defaults to ``3``.
550 shown. Defaults to ``3``.
558 """
551 """
559 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
552 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
560 if ignore_whitespace:
553 if ignore_whitespace:
561 flags.append('-w')
554 flags.append('-w')
562
555
563 if hasattr(rev1, 'raw_id'):
556 if hasattr(rev1, 'raw_id'):
564 rev1 = getattr(rev1, 'raw_id')
557 rev1 = getattr(rev1, 'raw_id')
565
558
566 if hasattr(rev2, 'raw_id'):
559 if hasattr(rev2, 'raw_id'):
567 rev2 = getattr(rev2, 'raw_id')
560 rev2 = getattr(rev2, 'raw_id')
568
561
569 if rev1 == self.EMPTY_CHANGESET:
562 if rev1 == self.EMPTY_CHANGESET:
570 rev2 = self.get_changeset(rev2).raw_id
563 rev2 = self.get_changeset(rev2).raw_id
571 cmd = ' '.join(['show'] + flags + [rev2])
564 cmd = ' '.join(['show'] + flags + [rev2])
572 else:
565 else:
573 rev1 = self.get_changeset(rev1).raw_id
566 rev1 = self.get_changeset(rev1).raw_id
574 rev2 = self.get_changeset(rev2).raw_id
567 rev2 = self.get_changeset(rev2).raw_id
575 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
568 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
576
569
577 if path:
570 if path:
578 cmd += ' -- "%s"' % path
571 cmd += ' -- "%s"' % path
579
572
580 stdout, stderr = self.run_git_command(cmd)
573 stdout, stderr = self.run_git_command(cmd)
581 # If we used 'show' command, strip first few lines (until actual diff
574 # If we used 'show' command, strip first few lines (until actual diff
582 # starts)
575 # starts)
583 if rev1 == self.EMPTY_CHANGESET:
576 if rev1 == self.EMPTY_CHANGESET:
584 lines = stdout.splitlines()
577 lines = stdout.splitlines()
585 x = 0
578 x = 0
586 for line in lines:
579 for line in lines:
587 if line.startswith('diff'):
580 if line.startswith('diff'):
588 break
581 break
589 x += 1
582 x += 1
590 # Append new line just like 'diff' command do
583 # Append new line just like 'diff' command do
591 stdout = '\n'.join(lines[x:]) + '\n'
584 stdout = '\n'.join(lines[x:]) + '\n'
592 return stdout
585 return stdout
593
586
594 @LazyProperty
587 @LazyProperty
595 def in_memory_changeset(self):
588 def in_memory_changeset(self):
596 """
589 """
597 Returns ``GitInMemoryChangeset`` object for this repository.
590 Returns ``GitInMemoryChangeset`` object for this repository.
598 """
591 """
599 return GitInMemoryChangeset(self)
592 return GitInMemoryChangeset(self)
600
593
601 def clone(self, url, update_after_clone=True, bare=False):
594 def clone(self, url, update_after_clone=True, bare=False):
602 """
595 """
603 Tries to clone changes from external location.
596 Tries to clone changes from external location.
604
597
605 :param update_after_clone: If set to ``False``, git won't checkout
598 :param update_after_clone: If set to ``False``, git won't checkout
606 working directory
599 working directory
607 :param bare: If set to ``True``, repository would be cloned into
600 :param bare: If set to ``True``, repository would be cloned into
608 *bare* git repository (no working directory at all).
601 *bare* git repository (no working directory at all).
609 """
602 """
610 url = self._get_url(url)
603 url = self._get_url(url)
611 cmd = ['clone']
604 cmd = ['clone']
612 if bare:
605 if bare:
613 cmd.append('--bare')
606 cmd.append('--bare')
614 elif not update_after_clone:
607 elif not update_after_clone:
615 cmd.append('--no-checkout')
608 cmd.append('--no-checkout')
616 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
609 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
617 cmd = ' '.join(cmd)
610 cmd = ' '.join(cmd)
618 # If error occurs run_git_command raises RepositoryError already
611 # If error occurs run_git_command raises RepositoryError already
619 self.run_git_command(cmd)
612 self.run_git_command(cmd)
620
613
621 def pull(self, url):
614 def pull(self, url):
622 """
615 """
623 Tries to pull changes from external location.
616 Tries to pull changes from external location.
624 """
617 """
625 url = self._get_url(url)
618 url = self._get_url(url)
626 cmd = ['pull']
619 cmd = ['pull']
627 cmd.append("--ff-only")
620 cmd.append("--ff-only")
628 cmd.append(url)
621 cmd.append(url)
629 cmd = ' '.join(cmd)
622 cmd = ' '.join(cmd)
630 # If error occurs run_git_command raises RepositoryError already
623 # If error occurs run_git_command raises RepositoryError already
631 self.run_git_command(cmd)
624 self.run_git_command(cmd)
632
625
633 def fetch(self, url):
626 def fetch(self, url):
634 """
627 """
635 Tries to pull changes from external location.
628 Tries to pull changes from external location.
636 """
629 """
637 url = self._get_url(url)
630 url = self._get_url(url)
638 so, se = self.run_git_command('ls-remote -h %s' % url)
631 so, se = self.run_git_command('ls-remote -h %s' % url)
639 refs = []
632 refs = []
640 for line in (x for x in so.splitlines()):
633 for line in (x for x in so.splitlines()):
641 sha, ref = line.split('\t')
634 sha, ref = line.split('\t')
642 refs.append(ref)
635 refs.append(ref)
643 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
636 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
644 cmd = '''fetch %s -- %s''' % (url, refs)
637 cmd = '''fetch %s -- %s''' % (url, refs)
645 self.run_git_command(cmd)
638 self.run_git_command(cmd)
646
639
647 @LazyProperty
640 @LazyProperty
648 def workdir(self):
641 def workdir(self):
649 """
642 """
650 Returns ``Workdir`` instance for this repository.
643 Returns ``Workdir`` instance for this repository.
651 """
644 """
652 return GitWorkdir(self)
645 return GitWorkdir(self)
653
646
654 def get_config_value(self, section, name, config_file=None):
647 def get_config_value(self, section, name, config_file=None):
655 """
648 """
656 Returns configuration value for a given [``section``] and ``name``.
649 Returns configuration value for a given [``section``] and ``name``.
657
650
658 :param section: Section we want to retrieve value from
651 :param section: Section we want to retrieve value from
659 :param name: Name of configuration we want to retrieve
652 :param name: Name of configuration we want to retrieve
660 :param config_file: A path to file which should be used to retrieve
653 :param config_file: A path to file which should be used to retrieve
661 configuration from (might also be a list of file paths)
654 configuration from (might also be a list of file paths)
662 """
655 """
663 if config_file is None:
656 if config_file is None:
664 config_file = []
657 config_file = []
665 elif isinstance(config_file, basestring):
658 elif isinstance(config_file, basestring):
666 config_file = [config_file]
659 config_file = [config_file]
667
660
668 def gen_configs():
661 def gen_configs():
669 for path in config_file + self._config_files:
662 for path in config_file + self._config_files:
670 try:
663 try:
671 yield ConfigFile.from_path(path)
664 yield ConfigFile.from_path(path)
672 except (IOError, OSError, ValueError):
665 except (IOError, OSError, ValueError):
673 continue
666 continue
674
667
675 for config in gen_configs():
668 for config in gen_configs():
676 try:
669 try:
677 return config.get(section, name)
670 return config.get(section, name)
678 except KeyError:
671 except KeyError:
679 continue
672 continue
680 return None
673 return None
681
674
682 def get_user_name(self, config_file=None):
675 def get_user_name(self, config_file=None):
683 """
676 """
684 Returns user's name from global configuration file.
677 Returns user's name from global configuration file.
685
678
686 :param config_file: A path to file which should be used to retrieve
679 :param config_file: A path to file which should be used to retrieve
687 configuration from (might also be a list of file paths)
680 configuration from (might also be a list of file paths)
688 """
681 """
689 return self.get_config_value('user', 'name', config_file)
682 return self.get_config_value('user', 'name', config_file)
690
683
691 def get_user_email(self, config_file=None):
684 def get_user_email(self, config_file=None):
692 """
685 """
693 Returns user's email from global configuration file.
686 Returns user's email from global configuration file.
694
687
695 :param config_file: A path to file which should be used to retrieve
688 :param config_file: A path to file which should be used to retrieve
696 configuration from (might also be a list of file paths)
689 configuration from (might also be a list of file paths)
697 """
690 """
698 return self.get_config_value('user', 'email', config_file)
691 return self.get_config_value('user', 'email', config_file)
@@ -1,2056 +1,2052 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.db
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~
5
5
6 Database Models for RhodeCode
6 Database Models for RhodeCode
7
7
8 :created_on: Apr 08, 2010
8 :created_on: Apr 08, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import traceback
29 import traceback
30 import hashlib
30 import hashlib
31 import time
31 import time
32 from collections import defaultdict
32 from collections import defaultdict
33
33
34 from sqlalchemy import *
34 from sqlalchemy import *
35 from sqlalchemy.ext.hybrid import hybrid_property
35 from sqlalchemy.ext.hybrid import hybrid_property
36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 from sqlalchemy.exc import DatabaseError
37 from sqlalchemy.exc import DatabaseError
38 from beaker.cache import cache_region, region_invalidate
38 from beaker.cache import cache_region, region_invalidate
39 from webob.exc import HTTPNotFound
39 from webob.exc import HTTPNotFound
40
40
41 from pylons.i18n.translation import lazy_ugettext as _
41 from pylons.i18n.translation import lazy_ugettext as _
42
42
43 from rhodecode.lib.vcs import get_backend
43 from rhodecode.lib.vcs import get_backend
44 from rhodecode.lib.vcs.utils.helpers import get_scm
44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 from rhodecode.lib.vcs.exceptions import VCSError
45 from rhodecode.lib.vcs.exceptions import VCSError
46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48
48
49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime
50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime, _set_extras
51 from rhodecode.lib.compat import json
51 from rhodecode.lib.compat import json
52 from rhodecode.lib.caching_query import FromCache
52 from rhodecode.lib.caching_query import FromCache
53
53
54 from rhodecode.model.meta import Base, Session
54 from rhodecode.model.meta import Base, Session
55
55
56 URL_SEP = '/'
56 URL_SEP = '/'
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59 #==============================================================================
59 #==============================================================================
60 # BASE CLASSES
60 # BASE CLASSES
61 #==============================================================================
61 #==============================================================================
62
62
63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64
64
65
65
66 class BaseModel(object):
66 class BaseModel(object):
67 """
67 """
68 Base Model for all classess
68 Base Model for all classess
69 """
69 """
70
70
71 @classmethod
71 @classmethod
72 def _get_keys(cls):
72 def _get_keys(cls):
73 """return column names for this model """
73 """return column names for this model """
74 return class_mapper(cls).c.keys()
74 return class_mapper(cls).c.keys()
75
75
76 def get_dict(self):
76 def get_dict(self):
77 """
77 """
78 return dict with keys and values corresponding
78 return dict with keys and values corresponding
79 to this model data """
79 to this model data """
80
80
81 d = {}
81 d = {}
82 for k in self._get_keys():
82 for k in self._get_keys():
83 d[k] = getattr(self, k)
83 d[k] = getattr(self, k)
84
84
85 # also use __json__() if present to get additional fields
85 # also use __json__() if present to get additional fields
86 _json_attr = getattr(self, '__json__', None)
86 _json_attr = getattr(self, '__json__', None)
87 if _json_attr:
87 if _json_attr:
88 # update with attributes from __json__
88 # update with attributes from __json__
89 if callable(_json_attr):
89 if callable(_json_attr):
90 _json_attr = _json_attr()
90 _json_attr = _json_attr()
91 for k, val in _json_attr.iteritems():
91 for k, val in _json_attr.iteritems():
92 d[k] = val
92 d[k] = val
93 return d
93 return d
94
94
95 def get_appstruct(self):
95 def get_appstruct(self):
96 """return list with keys and values tupples corresponding
96 """return list with keys and values tupples corresponding
97 to this model data """
97 to this model data """
98
98
99 l = []
99 l = []
100 for k in self._get_keys():
100 for k in self._get_keys():
101 l.append((k, getattr(self, k),))
101 l.append((k, getattr(self, k),))
102 return l
102 return l
103
103
104 def populate_obj(self, populate_dict):
104 def populate_obj(self, populate_dict):
105 """populate model with data from given populate_dict"""
105 """populate model with data from given populate_dict"""
106
106
107 for k in self._get_keys():
107 for k in self._get_keys():
108 if k in populate_dict:
108 if k in populate_dict:
109 setattr(self, k, populate_dict[k])
109 setattr(self, k, populate_dict[k])
110
110
111 @classmethod
111 @classmethod
112 def query(cls):
112 def query(cls):
113 return Session().query(cls)
113 return Session().query(cls)
114
114
115 @classmethod
115 @classmethod
116 def get(cls, id_):
116 def get(cls, id_):
117 if id_:
117 if id_:
118 return cls.query().get(id_)
118 return cls.query().get(id_)
119
119
120 @classmethod
120 @classmethod
121 def get_or_404(cls, id_):
121 def get_or_404(cls, id_):
122 try:
122 try:
123 id_ = int(id_)
123 id_ = int(id_)
124 except (TypeError, ValueError):
124 except (TypeError, ValueError):
125 raise HTTPNotFound
125 raise HTTPNotFound
126
126
127 res = cls.query().get(id_)
127 res = cls.query().get(id_)
128 if not res:
128 if not res:
129 raise HTTPNotFound
129 raise HTTPNotFound
130 return res
130 return res
131
131
132 @classmethod
132 @classmethod
133 def getAll(cls):
133 def getAll(cls):
134 return cls.query().all()
134 return cls.query().all()
135
135
136 @classmethod
136 @classmethod
137 def delete(cls, id_):
137 def delete(cls, id_):
138 obj = cls.query().get(id_)
138 obj = cls.query().get(id_)
139 Session().delete(obj)
139 Session().delete(obj)
140
140
141 def __repr__(self):
141 def __repr__(self):
142 if hasattr(self, '__unicode__'):
142 if hasattr(self, '__unicode__'):
143 # python repr needs to return str
143 # python repr needs to return str
144 return safe_str(self.__unicode__())
144 return safe_str(self.__unicode__())
145 return '<DB:%s>' % (self.__class__.__name__)
145 return '<DB:%s>' % (self.__class__.__name__)
146
146
147
147
148 class RhodeCodeSetting(Base, BaseModel):
148 class RhodeCodeSetting(Base, BaseModel):
149 __tablename__ = 'rhodecode_settings'
149 __tablename__ = 'rhodecode_settings'
150 __table_args__ = (
150 __table_args__ = (
151 UniqueConstraint('app_settings_name'),
151 UniqueConstraint('app_settings_name'),
152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
153 'mysql_charset': 'utf8'}
153 'mysql_charset': 'utf8'}
154 )
154 )
155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
158
158
159 def __init__(self, k='', v=''):
159 def __init__(self, k='', v=''):
160 self.app_settings_name = k
160 self.app_settings_name = k
161 self.app_settings_value = v
161 self.app_settings_value = v
162
162
163 @validates('_app_settings_value')
163 @validates('_app_settings_value')
164 def validate_settings_value(self, key, val):
164 def validate_settings_value(self, key, val):
165 assert type(val) == unicode
165 assert type(val) == unicode
166 return val
166 return val
167
167
168 @hybrid_property
168 @hybrid_property
169 def app_settings_value(self):
169 def app_settings_value(self):
170 v = self._app_settings_value
170 v = self._app_settings_value
171 if self.app_settings_name in ["ldap_active",
171 if self.app_settings_name in ["ldap_active",
172 "default_repo_enable_statistics",
172 "default_repo_enable_statistics",
173 "default_repo_enable_locking",
173 "default_repo_enable_locking",
174 "default_repo_private",
174 "default_repo_private",
175 "default_repo_enable_downloads"]:
175 "default_repo_enable_downloads"]:
176 v = str2bool(v)
176 v = str2bool(v)
177 return v
177 return v
178
178
179 @app_settings_value.setter
179 @app_settings_value.setter
180 def app_settings_value(self, val):
180 def app_settings_value(self, val):
181 """
181 """
182 Setter that will always make sure we use unicode in app_settings_value
182 Setter that will always make sure we use unicode in app_settings_value
183
183
184 :param val:
184 :param val:
185 """
185 """
186 self._app_settings_value = safe_unicode(val)
186 self._app_settings_value = safe_unicode(val)
187
187
188 def __unicode__(self):
188 def __unicode__(self):
189 return u"<%s('%s:%s')>" % (
189 return u"<%s('%s:%s')>" % (
190 self.__class__.__name__,
190 self.__class__.__name__,
191 self.app_settings_name, self.app_settings_value
191 self.app_settings_name, self.app_settings_value
192 )
192 )
193
193
194 @classmethod
194 @classmethod
195 def get_by_name(cls, key):
195 def get_by_name(cls, key):
196 return cls.query()\
196 return cls.query()\
197 .filter(cls.app_settings_name == key).scalar()
197 .filter(cls.app_settings_name == key).scalar()
198
198
199 @classmethod
199 @classmethod
200 def get_by_name_or_create(cls, key):
200 def get_by_name_or_create(cls, key):
201 res = cls.get_by_name(key)
201 res = cls.get_by_name(key)
202 if not res:
202 if not res:
203 res = cls(key)
203 res = cls(key)
204 return res
204 return res
205
205
206 @classmethod
206 @classmethod
207 def get_app_settings(cls, cache=False):
207 def get_app_settings(cls, cache=False):
208
208
209 ret = cls.query()
209 ret = cls.query()
210
210
211 if cache:
211 if cache:
212 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
212 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
213
213
214 if not ret:
214 if not ret:
215 raise Exception('Could not get application settings !')
215 raise Exception('Could not get application settings !')
216 settings = {}
216 settings = {}
217 for each in ret:
217 for each in ret:
218 settings['rhodecode_' + each.app_settings_name] = \
218 settings['rhodecode_' + each.app_settings_name] = \
219 each.app_settings_value
219 each.app_settings_value
220
220
221 return settings
221 return settings
222
222
223 @classmethod
223 @classmethod
224 def get_ldap_settings(cls, cache=False):
224 def get_ldap_settings(cls, cache=False):
225 ret = cls.query()\
225 ret = cls.query()\
226 .filter(cls.app_settings_name.startswith('ldap_')).all()
226 .filter(cls.app_settings_name.startswith('ldap_')).all()
227 fd = {}
227 fd = {}
228 for row in ret:
228 for row in ret:
229 fd.update({row.app_settings_name: row.app_settings_value})
229 fd.update({row.app_settings_name: row.app_settings_value})
230
230
231 return fd
231 return fd
232
232
233 @classmethod
233 @classmethod
234 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
234 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
235 ret = cls.query()\
235 ret = cls.query()\
236 .filter(cls.app_settings_name.startswith('default_')).all()
236 .filter(cls.app_settings_name.startswith('default_')).all()
237 fd = {}
237 fd = {}
238 for row in ret:
238 for row in ret:
239 key = row.app_settings_name
239 key = row.app_settings_name
240 if strip_prefix:
240 if strip_prefix:
241 key = remove_prefix(key, prefix='default_')
241 key = remove_prefix(key, prefix='default_')
242 fd.update({key: row.app_settings_value})
242 fd.update({key: row.app_settings_value})
243
243
244 return fd
244 return fd
245
245
246
246
247 class RhodeCodeUi(Base, BaseModel):
247 class RhodeCodeUi(Base, BaseModel):
248 __tablename__ = 'rhodecode_ui'
248 __tablename__ = 'rhodecode_ui'
249 __table_args__ = (
249 __table_args__ = (
250 UniqueConstraint('ui_key'),
250 UniqueConstraint('ui_key'),
251 {'extend_existing': True, 'mysql_engine': 'InnoDB',
251 {'extend_existing': True, 'mysql_engine': 'InnoDB',
252 'mysql_charset': 'utf8'}
252 'mysql_charset': 'utf8'}
253 )
253 )
254
254
255 HOOK_UPDATE = 'changegroup.update'
255 HOOK_UPDATE = 'changegroup.update'
256 HOOK_REPO_SIZE = 'changegroup.repo_size'
256 HOOK_REPO_SIZE = 'changegroup.repo_size'
257 HOOK_PUSH = 'changegroup.push_logger'
257 HOOK_PUSH = 'changegroup.push_logger'
258 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
258 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
259 HOOK_PULL = 'outgoing.pull_logger'
259 HOOK_PULL = 'outgoing.pull_logger'
260 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
260 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
261
261
262 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
262 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
263 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
263 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
266 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
266 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
267
267
268 @classmethod
268 @classmethod
269 def get_by_key(cls, key):
269 def get_by_key(cls, key):
270 return cls.query().filter(cls.ui_key == key).scalar()
270 return cls.query().filter(cls.ui_key == key).scalar()
271
271
272 @classmethod
272 @classmethod
273 def get_builtin_hooks(cls):
273 def get_builtin_hooks(cls):
274 q = cls.query()
274 q = cls.query()
275 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
275 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
276 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
276 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
277 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
277 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
278 return q.all()
278 return q.all()
279
279
280 @classmethod
280 @classmethod
281 def get_custom_hooks(cls):
281 def get_custom_hooks(cls):
282 q = cls.query()
282 q = cls.query()
283 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
283 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
284 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
284 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
285 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
285 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
286 q = q.filter(cls.ui_section == 'hooks')
286 q = q.filter(cls.ui_section == 'hooks')
287 return q.all()
287 return q.all()
288
288
289 @classmethod
289 @classmethod
290 def get_repos_location(cls):
290 def get_repos_location(cls):
291 return cls.get_by_key('/').ui_value
291 return cls.get_by_key('/').ui_value
292
292
293 @classmethod
293 @classmethod
294 def create_or_update_hook(cls, key, val):
294 def create_or_update_hook(cls, key, val):
295 new_ui = cls.get_by_key(key) or cls()
295 new_ui = cls.get_by_key(key) or cls()
296 new_ui.ui_section = 'hooks'
296 new_ui.ui_section = 'hooks'
297 new_ui.ui_active = True
297 new_ui.ui_active = True
298 new_ui.ui_key = key
298 new_ui.ui_key = key
299 new_ui.ui_value = val
299 new_ui.ui_value = val
300
300
301 Session().add(new_ui)
301 Session().add(new_ui)
302
302
303 def __repr__(self):
303 def __repr__(self):
304 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
304 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
305 self.ui_value)
305 self.ui_value)
306
306
307
307
308 class User(Base, BaseModel):
308 class User(Base, BaseModel):
309 __tablename__ = 'users'
309 __tablename__ = 'users'
310 __table_args__ = (
310 __table_args__ = (
311 UniqueConstraint('username'), UniqueConstraint('email'),
311 UniqueConstraint('username'), UniqueConstraint('email'),
312 Index('u_username_idx', 'username'),
312 Index('u_username_idx', 'username'),
313 Index('u_email_idx', 'email'),
313 Index('u_email_idx', 'email'),
314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
315 'mysql_charset': 'utf8'}
315 'mysql_charset': 'utf8'}
316 )
316 )
317 DEFAULT_USER = 'default'
317 DEFAULT_USER = 'default'
318 DEFAULT_PERMISSIONS = [
318 DEFAULT_PERMISSIONS = [
319 'hg.register.manual_activate', 'hg.create.repository',
319 'hg.register.manual_activate', 'hg.create.repository',
320 'hg.fork.repository', 'repository.read', 'group.read'
320 'hg.fork.repository', 'repository.read', 'group.read'
321 ]
321 ]
322 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
322 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
323 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
323 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
326 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
326 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
327 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
330 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
331 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
333 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
333 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
334
334
335 user_log = relationship('UserLog')
335 user_log = relationship('UserLog')
336 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
336 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
337
337
338 repositories = relationship('Repository')
338 repositories = relationship('Repository')
339 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
339 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
340 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
340 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
341
341
342 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
342 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
343 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
343 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
344
344
345 group_member = relationship('UserGroupMember', cascade='all')
345 group_member = relationship('UserGroupMember', cascade='all')
346
346
347 notifications = relationship('UserNotification', cascade='all')
347 notifications = relationship('UserNotification', cascade='all')
348 # notifications assigned to this user
348 # notifications assigned to this user
349 user_created_notifications = relationship('Notification', cascade='all')
349 user_created_notifications = relationship('Notification', cascade='all')
350 # comments created by this user
350 # comments created by this user
351 user_comments = relationship('ChangesetComment', cascade='all')
351 user_comments = relationship('ChangesetComment', cascade='all')
352 #extra emails for this user
352 #extra emails for this user
353 user_emails = relationship('UserEmailMap', cascade='all')
353 user_emails = relationship('UserEmailMap', cascade='all')
354
354
355 @hybrid_property
355 @hybrid_property
356 def email(self):
356 def email(self):
357 return self._email
357 return self._email
358
358
359 @email.setter
359 @email.setter
360 def email(self, val):
360 def email(self, val):
361 self._email = val.lower() if val else None
361 self._email = val.lower() if val else None
362
362
363 @property
363 @property
364 def firstname(self):
364 def firstname(self):
365 # alias for future
365 # alias for future
366 return self.name
366 return self.name
367
367
368 @property
368 @property
369 def emails(self):
369 def emails(self):
370 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
370 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
371 return [self.email] + [x.email for x in other]
371 return [self.email] + [x.email for x in other]
372
372
373 @property
373 @property
374 def ip_addresses(self):
374 def ip_addresses(self):
375 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
375 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
376 return [x.ip_addr for x in ret]
376 return [x.ip_addr for x in ret]
377
377
378 @property
378 @property
379 def username_and_name(self):
379 def username_and_name(self):
380 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
380 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
381
381
382 @property
382 @property
383 def full_name(self):
383 def full_name(self):
384 return '%s %s' % (self.firstname, self.lastname)
384 return '%s %s' % (self.firstname, self.lastname)
385
385
386 @property
386 @property
387 def full_name_or_username(self):
387 def full_name_or_username(self):
388 return ('%s %s' % (self.firstname, self.lastname)
388 return ('%s %s' % (self.firstname, self.lastname)
389 if (self.firstname and self.lastname) else self.username)
389 if (self.firstname and self.lastname) else self.username)
390
390
391 @property
391 @property
392 def full_contact(self):
392 def full_contact(self):
393 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
393 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
394
394
395 @property
395 @property
396 def short_contact(self):
396 def short_contact(self):
397 return '%s %s' % (self.firstname, self.lastname)
397 return '%s %s' % (self.firstname, self.lastname)
398
398
399 @property
399 @property
400 def is_admin(self):
400 def is_admin(self):
401 return self.admin
401 return self.admin
402
402
403 @property
403 @property
404 def AuthUser(self):
404 def AuthUser(self):
405 """
405 """
406 Returns instance of AuthUser for this user
406 Returns instance of AuthUser for this user
407 """
407 """
408 from rhodecode.lib.auth import AuthUser
408 from rhodecode.lib.auth import AuthUser
409 return AuthUser(user_id=self.user_id, api_key=self.api_key,
409 return AuthUser(user_id=self.user_id, api_key=self.api_key,
410 username=self.username)
410 username=self.username)
411
411
412 def __unicode__(self):
412 def __unicode__(self):
413 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
413 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
414 self.user_id, self.username)
414 self.user_id, self.username)
415
415
416 @classmethod
416 @classmethod
417 def get_by_username(cls, username, case_insensitive=False, cache=False):
417 def get_by_username(cls, username, case_insensitive=False, cache=False):
418 if case_insensitive:
418 if case_insensitive:
419 q = cls.query().filter(cls.username.ilike(username))
419 q = cls.query().filter(cls.username.ilike(username))
420 else:
420 else:
421 q = cls.query().filter(cls.username == username)
421 q = cls.query().filter(cls.username == username)
422
422
423 if cache:
423 if cache:
424 q = q.options(FromCache(
424 q = q.options(FromCache(
425 "sql_cache_short",
425 "sql_cache_short",
426 "get_user_%s" % _hash_key(username)
426 "get_user_%s" % _hash_key(username)
427 )
427 )
428 )
428 )
429 return q.scalar()
429 return q.scalar()
430
430
431 @classmethod
431 @classmethod
432 def get_by_api_key(cls, api_key, cache=False):
432 def get_by_api_key(cls, api_key, cache=False):
433 q = cls.query().filter(cls.api_key == api_key)
433 q = cls.query().filter(cls.api_key == api_key)
434
434
435 if cache:
435 if cache:
436 q = q.options(FromCache("sql_cache_short",
436 q = q.options(FromCache("sql_cache_short",
437 "get_api_key_%s" % api_key))
437 "get_api_key_%s" % api_key))
438 return q.scalar()
438 return q.scalar()
439
439
440 @classmethod
440 @classmethod
441 def get_by_email(cls, email, case_insensitive=False, cache=False):
441 def get_by_email(cls, email, case_insensitive=False, cache=False):
442 if case_insensitive:
442 if case_insensitive:
443 q = cls.query().filter(cls.email.ilike(email))
443 q = cls.query().filter(cls.email.ilike(email))
444 else:
444 else:
445 q = cls.query().filter(cls.email == email)
445 q = cls.query().filter(cls.email == email)
446
446
447 if cache:
447 if cache:
448 q = q.options(FromCache("sql_cache_short",
448 q = q.options(FromCache("sql_cache_short",
449 "get_email_key_%s" % email))
449 "get_email_key_%s" % email))
450
450
451 ret = q.scalar()
451 ret = q.scalar()
452 if ret is None:
452 if ret is None:
453 q = UserEmailMap.query()
453 q = UserEmailMap.query()
454 # try fetching in alternate email map
454 # try fetching in alternate email map
455 if case_insensitive:
455 if case_insensitive:
456 q = q.filter(UserEmailMap.email.ilike(email))
456 q = q.filter(UserEmailMap.email.ilike(email))
457 else:
457 else:
458 q = q.filter(UserEmailMap.email == email)
458 q = q.filter(UserEmailMap.email == email)
459 q = q.options(joinedload(UserEmailMap.user))
459 q = q.options(joinedload(UserEmailMap.user))
460 if cache:
460 if cache:
461 q = q.options(FromCache("sql_cache_short",
461 q = q.options(FromCache("sql_cache_short",
462 "get_email_map_key_%s" % email))
462 "get_email_map_key_%s" % email))
463 ret = getattr(q.scalar(), 'user', None)
463 ret = getattr(q.scalar(), 'user', None)
464
464
465 return ret
465 return ret
466
466
467 @classmethod
467 @classmethod
468 def get_from_cs_author(cls, author):
468 def get_from_cs_author(cls, author):
469 """
469 """
470 Tries to get User objects out of commit author string
470 Tries to get User objects out of commit author string
471
471
472 :param author:
472 :param author:
473 """
473 """
474 from rhodecode.lib.helpers import email, author_name
474 from rhodecode.lib.helpers import email, author_name
475 # Valid email in the attribute passed, see if they're in the system
475 # Valid email in the attribute passed, see if they're in the system
476 _email = email(author)
476 _email = email(author)
477 if _email:
477 if _email:
478 user = cls.get_by_email(_email, case_insensitive=True)
478 user = cls.get_by_email(_email, case_insensitive=True)
479 if user:
479 if user:
480 return user
480 return user
481 # Maybe we can match by username?
481 # Maybe we can match by username?
482 _author = author_name(author)
482 _author = author_name(author)
483 user = cls.get_by_username(_author, case_insensitive=True)
483 user = cls.get_by_username(_author, case_insensitive=True)
484 if user:
484 if user:
485 return user
485 return user
486
486
487 def update_lastlogin(self):
487 def update_lastlogin(self):
488 """Update user lastlogin"""
488 """Update user lastlogin"""
489 self.last_login = datetime.datetime.now()
489 self.last_login = datetime.datetime.now()
490 Session().add(self)
490 Session().add(self)
491 log.debug('updated user %s lastlogin' % self.username)
491 log.debug('updated user %s lastlogin' % self.username)
492
492
493 def get_api_data(self):
493 def get_api_data(self):
494 """
494 """
495 Common function for generating user related data for API
495 Common function for generating user related data for API
496 """
496 """
497 user = self
497 user = self
498 data = dict(
498 data = dict(
499 user_id=user.user_id,
499 user_id=user.user_id,
500 username=user.username,
500 username=user.username,
501 firstname=user.name,
501 firstname=user.name,
502 lastname=user.lastname,
502 lastname=user.lastname,
503 email=user.email,
503 email=user.email,
504 emails=user.emails,
504 emails=user.emails,
505 api_key=user.api_key,
505 api_key=user.api_key,
506 active=user.active,
506 active=user.active,
507 admin=user.admin,
507 admin=user.admin,
508 ldap_dn=user.ldap_dn,
508 ldap_dn=user.ldap_dn,
509 last_login=user.last_login,
509 last_login=user.last_login,
510 ip_addresses=user.ip_addresses
510 ip_addresses=user.ip_addresses
511 )
511 )
512 return data
512 return data
513
513
514 def __json__(self):
514 def __json__(self):
515 data = dict(
515 data = dict(
516 full_name=self.full_name,
516 full_name=self.full_name,
517 full_name_or_username=self.full_name_or_username,
517 full_name_or_username=self.full_name_or_username,
518 short_contact=self.short_contact,
518 short_contact=self.short_contact,
519 full_contact=self.full_contact
519 full_contact=self.full_contact
520 )
520 )
521 data.update(self.get_api_data())
521 data.update(self.get_api_data())
522 return data
522 return data
523
523
524
524
525 class UserEmailMap(Base, BaseModel):
525 class UserEmailMap(Base, BaseModel):
526 __tablename__ = 'user_email_map'
526 __tablename__ = 'user_email_map'
527 __table_args__ = (
527 __table_args__ = (
528 Index('uem_email_idx', 'email'),
528 Index('uem_email_idx', 'email'),
529 UniqueConstraint('email'),
529 UniqueConstraint('email'),
530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
531 'mysql_charset': 'utf8'}
531 'mysql_charset': 'utf8'}
532 )
532 )
533 __mapper_args__ = {}
533 __mapper_args__ = {}
534
534
535 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
535 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
537 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
537 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
538 user = relationship('User', lazy='joined')
538 user = relationship('User', lazy='joined')
539
539
540 @validates('_email')
540 @validates('_email')
541 def validate_email(self, key, email):
541 def validate_email(self, key, email):
542 # check if this email is not main one
542 # check if this email is not main one
543 main_email = Session().query(User).filter(User.email == email).scalar()
543 main_email = Session().query(User).filter(User.email == email).scalar()
544 if main_email is not None:
544 if main_email is not None:
545 raise AttributeError('email %s is present is user table' % email)
545 raise AttributeError('email %s is present is user table' % email)
546 return email
546 return email
547
547
548 @hybrid_property
548 @hybrid_property
549 def email(self):
549 def email(self):
550 return self._email
550 return self._email
551
551
552 @email.setter
552 @email.setter
553 def email(self, val):
553 def email(self, val):
554 self._email = val.lower() if val else None
554 self._email = val.lower() if val else None
555
555
556
556
557 class UserIpMap(Base, BaseModel):
557 class UserIpMap(Base, BaseModel):
558 __tablename__ = 'user_ip_map'
558 __tablename__ = 'user_ip_map'
559 __table_args__ = (
559 __table_args__ = (
560 UniqueConstraint('user_id', 'ip_addr'),
560 UniqueConstraint('user_id', 'ip_addr'),
561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
562 'mysql_charset': 'utf8'}
562 'mysql_charset': 'utf8'}
563 )
563 )
564 __mapper_args__ = {}
564 __mapper_args__ = {}
565
565
566 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
566 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
567 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
567 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
568 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
568 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
569 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
569 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
570 user = relationship('User', lazy='joined')
570 user = relationship('User', lazy='joined')
571
571
572 @classmethod
572 @classmethod
573 def _get_ip_range(cls, ip_addr):
573 def _get_ip_range(cls, ip_addr):
574 from rhodecode.lib import ipaddr
574 from rhodecode.lib import ipaddr
575 net = ipaddr.IPNetwork(address=ip_addr)
575 net = ipaddr.IPNetwork(address=ip_addr)
576 return [str(net.network), str(net.broadcast)]
576 return [str(net.network), str(net.broadcast)]
577
577
578 def __json__(self):
578 def __json__(self):
579 return dict(
579 return dict(
580 ip_addr=self.ip_addr,
580 ip_addr=self.ip_addr,
581 ip_range=self._get_ip_range(self.ip_addr)
581 ip_range=self._get_ip_range(self.ip_addr)
582 )
582 )
583
583
584
584
585 class UserLog(Base, BaseModel):
585 class UserLog(Base, BaseModel):
586 __tablename__ = 'user_logs'
586 __tablename__ = 'user_logs'
587 __table_args__ = (
587 __table_args__ = (
588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
589 'mysql_charset': 'utf8'},
589 'mysql_charset': 'utf8'},
590 )
590 )
591 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
593 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
593 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
594 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
594 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
595 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
595 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
596 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
596 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
597 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
597 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
598 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
598 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
599
599
600 @property
600 @property
601 def action_as_day(self):
601 def action_as_day(self):
602 return datetime.date(*self.action_date.timetuple()[:3])
602 return datetime.date(*self.action_date.timetuple()[:3])
603
603
604 user = relationship('User')
604 user = relationship('User')
605 repository = relationship('Repository', cascade='')
605 repository = relationship('Repository', cascade='')
606
606
607
607
608 class UserGroup(Base, BaseModel):
608 class UserGroup(Base, BaseModel):
609 __tablename__ = 'users_groups'
609 __tablename__ = 'users_groups'
610 __table_args__ = (
610 __table_args__ = (
611 {'extend_existing': True, 'mysql_engine': 'InnoDB',
611 {'extend_existing': True, 'mysql_engine': 'InnoDB',
612 'mysql_charset': 'utf8'},
612 'mysql_charset': 'utf8'},
613 )
613 )
614
614
615 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
615 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
616 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
616 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
617 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
617 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
618 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
618 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
619
619
620 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
620 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
621 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
621 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
622 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
622 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
623
623
624 def __unicode__(self):
624 def __unicode__(self):
625 return u'<userGroup(%s)>' % (self.users_group_name)
625 return u'<userGroup(%s)>' % (self.users_group_name)
626
626
627 @classmethod
627 @classmethod
628 def get_by_group_name(cls, group_name, cache=False,
628 def get_by_group_name(cls, group_name, cache=False,
629 case_insensitive=False):
629 case_insensitive=False):
630 if case_insensitive:
630 if case_insensitive:
631 q = cls.query().filter(cls.users_group_name.ilike(group_name))
631 q = cls.query().filter(cls.users_group_name.ilike(group_name))
632 else:
632 else:
633 q = cls.query().filter(cls.users_group_name == group_name)
633 q = cls.query().filter(cls.users_group_name == group_name)
634 if cache:
634 if cache:
635 q = q.options(FromCache(
635 q = q.options(FromCache(
636 "sql_cache_short",
636 "sql_cache_short",
637 "get_user_%s" % _hash_key(group_name)
637 "get_user_%s" % _hash_key(group_name)
638 )
638 )
639 )
639 )
640 return q.scalar()
640 return q.scalar()
641
641
642 @classmethod
642 @classmethod
643 def get(cls, users_group_id, cache=False):
643 def get(cls, users_group_id, cache=False):
644 users_group = cls.query()
644 users_group = cls.query()
645 if cache:
645 if cache:
646 users_group = users_group.options(FromCache("sql_cache_short",
646 users_group = users_group.options(FromCache("sql_cache_short",
647 "get_users_group_%s" % users_group_id))
647 "get_users_group_%s" % users_group_id))
648 return users_group.get(users_group_id)
648 return users_group.get(users_group_id)
649
649
650 def get_api_data(self):
650 def get_api_data(self):
651 users_group = self
651 users_group = self
652
652
653 data = dict(
653 data = dict(
654 users_group_id=users_group.users_group_id,
654 users_group_id=users_group.users_group_id,
655 group_name=users_group.users_group_name,
655 group_name=users_group.users_group_name,
656 active=users_group.users_group_active,
656 active=users_group.users_group_active,
657 )
657 )
658
658
659 return data
659 return data
660
660
661
661
662 class UserGroupMember(Base, BaseModel):
662 class UserGroupMember(Base, BaseModel):
663 __tablename__ = 'users_groups_members'
663 __tablename__ = 'users_groups_members'
664 __table_args__ = (
664 __table_args__ = (
665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
666 'mysql_charset': 'utf8'},
666 'mysql_charset': 'utf8'},
667 )
667 )
668
668
669 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
669 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
670 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
670 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
671 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
671 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
672
672
673 user = relationship('User', lazy='joined')
673 user = relationship('User', lazy='joined')
674 users_group = relationship('UserGroup')
674 users_group = relationship('UserGroup')
675
675
676 def __init__(self, gr_id='', u_id=''):
676 def __init__(self, gr_id='', u_id=''):
677 self.users_group_id = gr_id
677 self.users_group_id = gr_id
678 self.user_id = u_id
678 self.user_id = u_id
679
679
680
680
681 class RepositoryField(Base, BaseModel):
681 class RepositoryField(Base, BaseModel):
682 __tablename__ = 'repositories_fields'
682 __tablename__ = 'repositories_fields'
683 __table_args__ = (
683 __table_args__ = (
684 UniqueConstraint('repository_id', 'field_key'), # no-multi field
684 UniqueConstraint('repository_id', 'field_key'), # no-multi field
685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
686 'mysql_charset': 'utf8'},
686 'mysql_charset': 'utf8'},
687 )
687 )
688 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
688 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
689
689
690 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
690 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
691 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
691 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
692 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
692 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
693 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
693 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
694 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
694 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
695 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
695 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
696 field_type = Column("field_type", String(256), nullable=False, unique=None)
696 field_type = Column("field_type", String(256), nullable=False, unique=None)
697 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
697 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
698
698
699 repository = relationship('Repository')
699 repository = relationship('Repository')
700
700
701 @property
701 @property
702 def field_key_prefixed(self):
702 def field_key_prefixed(self):
703 return 'ex_%s' % self.field_key
703 return 'ex_%s' % self.field_key
704
704
705 @classmethod
705 @classmethod
706 def un_prefix_key(cls, key):
706 def un_prefix_key(cls, key):
707 if key.startswith(cls.PREFIX):
707 if key.startswith(cls.PREFIX):
708 return key[len(cls.PREFIX):]
708 return key[len(cls.PREFIX):]
709 return key
709 return key
710
710
711 @classmethod
711 @classmethod
712 def get_by_key_name(cls, key, repo):
712 def get_by_key_name(cls, key, repo):
713 row = cls.query()\
713 row = cls.query()\
714 .filter(cls.repository == repo)\
714 .filter(cls.repository == repo)\
715 .filter(cls.field_key == key).scalar()
715 .filter(cls.field_key == key).scalar()
716 return row
716 return row
717
717
718
718
719 class Repository(Base, BaseModel):
719 class Repository(Base, BaseModel):
720 __tablename__ = 'repositories'
720 __tablename__ = 'repositories'
721 __table_args__ = (
721 __table_args__ = (
722 UniqueConstraint('repo_name'),
722 UniqueConstraint('repo_name'),
723 Index('r_repo_name_idx', 'repo_name'),
723 Index('r_repo_name_idx', 'repo_name'),
724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
725 'mysql_charset': 'utf8'},
725 'mysql_charset': 'utf8'},
726 )
726 )
727
727
728 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
728 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
729 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
729 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
730 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
730 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
731 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
731 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
732 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
732 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
733 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
733 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
734 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
734 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
735 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
735 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
736 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
736 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
737 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
737 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
738 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
738 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
739 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
739 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
740 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
740 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
741 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
741 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
742 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
742 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
743
743
744 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
744 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
745 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
745 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
746
746
747 user = relationship('User')
747 user = relationship('User')
748 fork = relationship('Repository', remote_side=repo_id)
748 fork = relationship('Repository', remote_side=repo_id)
749 group = relationship('RepoGroup')
749 group = relationship('RepoGroup')
750 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
750 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
751 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
751 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
752 stats = relationship('Statistics', cascade='all', uselist=False)
752 stats = relationship('Statistics', cascade='all', uselist=False)
753
753
754 followers = relationship('UserFollowing',
754 followers = relationship('UserFollowing',
755 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
755 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
756 cascade='all')
756 cascade='all')
757 extra_fields = relationship('RepositoryField',
757 extra_fields = relationship('RepositoryField',
758 cascade="all, delete, delete-orphan")
758 cascade="all, delete, delete-orphan")
759
759
760 logs = relationship('UserLog')
760 logs = relationship('UserLog')
761 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
761 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
762
762
763 pull_requests_org = relationship('PullRequest',
763 pull_requests_org = relationship('PullRequest',
764 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
764 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
765 cascade="all, delete, delete-orphan")
765 cascade="all, delete, delete-orphan")
766
766
767 pull_requests_other = relationship('PullRequest',
767 pull_requests_other = relationship('PullRequest',
768 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
768 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
769 cascade="all, delete, delete-orphan")
769 cascade="all, delete, delete-orphan")
770
770
771 def __unicode__(self):
771 def __unicode__(self):
772 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
772 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
773 self.repo_name)
773 self.repo_name)
774
774
775 @hybrid_property
775 @hybrid_property
776 def locked(self):
776 def locked(self):
777 # always should return [user_id, timelocked]
777 # always should return [user_id, timelocked]
778 if self._locked:
778 if self._locked:
779 _lock_info = self._locked.split(':')
779 _lock_info = self._locked.split(':')
780 return int(_lock_info[0]), _lock_info[1]
780 return int(_lock_info[0]), _lock_info[1]
781 return [None, None]
781 return [None, None]
782
782
783 @locked.setter
783 @locked.setter
784 def locked(self, val):
784 def locked(self, val):
785 if val and isinstance(val, (list, tuple)):
785 if val and isinstance(val, (list, tuple)):
786 self._locked = ':'.join(map(str, val))
786 self._locked = ':'.join(map(str, val))
787 else:
787 else:
788 self._locked = None
788 self._locked = None
789
789
790 @hybrid_property
790 @hybrid_property
791 def changeset_cache(self):
791 def changeset_cache(self):
792 from rhodecode.lib.vcs.backends.base import EmptyChangeset
792 from rhodecode.lib.vcs.backends.base import EmptyChangeset
793 dummy = EmptyChangeset().__json__()
793 dummy = EmptyChangeset().__json__()
794 if not self._changeset_cache:
794 if not self._changeset_cache:
795 return dummy
795 return dummy
796 try:
796 try:
797 return json.loads(self._changeset_cache)
797 return json.loads(self._changeset_cache)
798 except TypeError:
798 except TypeError:
799 return dummy
799 return dummy
800
800
801 @changeset_cache.setter
801 @changeset_cache.setter
802 def changeset_cache(self, val):
802 def changeset_cache(self, val):
803 try:
803 try:
804 self._changeset_cache = json.dumps(val)
804 self._changeset_cache = json.dumps(val)
805 except:
805 except:
806 log.error(traceback.format_exc())
806 log.error(traceback.format_exc())
807
807
808 @classmethod
808 @classmethod
809 def url_sep(cls):
809 def url_sep(cls):
810 return URL_SEP
810 return URL_SEP
811
811
812 @classmethod
812 @classmethod
813 def normalize_repo_name(cls, repo_name):
813 def normalize_repo_name(cls, repo_name):
814 """
814 """
815 Normalizes os specific repo_name to the format internally stored inside
815 Normalizes os specific repo_name to the format internally stored inside
816 dabatabase using URL_SEP
816 dabatabase using URL_SEP
817
817
818 :param cls:
818 :param cls:
819 :param repo_name:
819 :param repo_name:
820 """
820 """
821 return cls.url_sep().join(repo_name.split(os.sep))
821 return cls.url_sep().join(repo_name.split(os.sep))
822
822
823 @classmethod
823 @classmethod
824 def get_by_repo_name(cls, repo_name):
824 def get_by_repo_name(cls, repo_name):
825 q = Session().query(cls).filter(cls.repo_name == repo_name)
825 q = Session().query(cls).filter(cls.repo_name == repo_name)
826 q = q.options(joinedload(Repository.fork))\
826 q = q.options(joinedload(Repository.fork))\
827 .options(joinedload(Repository.user))\
827 .options(joinedload(Repository.user))\
828 .options(joinedload(Repository.group))
828 .options(joinedload(Repository.group))
829 return q.scalar()
829 return q.scalar()
830
830
831 @classmethod
831 @classmethod
832 def get_by_full_path(cls, repo_full_path):
832 def get_by_full_path(cls, repo_full_path):
833 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
833 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
834 repo_name = cls.normalize_repo_name(repo_name)
834 repo_name = cls.normalize_repo_name(repo_name)
835 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
835 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
836
836
837 @classmethod
837 @classmethod
838 def get_repo_forks(cls, repo_id):
838 def get_repo_forks(cls, repo_id):
839 return cls.query().filter(Repository.fork_id == repo_id)
839 return cls.query().filter(Repository.fork_id == repo_id)
840
840
841 @classmethod
841 @classmethod
842 def base_path(cls):
842 def base_path(cls):
843 """
843 """
844 Returns base path when all repos are stored
844 Returns base path when all repos are stored
845
845
846 :param cls:
846 :param cls:
847 """
847 """
848 q = Session().query(RhodeCodeUi)\
848 q = Session().query(RhodeCodeUi)\
849 .filter(RhodeCodeUi.ui_key == cls.url_sep())
849 .filter(RhodeCodeUi.ui_key == cls.url_sep())
850 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
850 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
851 return q.one().ui_value
851 return q.one().ui_value
852
852
853 @property
853 @property
854 def forks(self):
854 def forks(self):
855 """
855 """
856 Return forks of this repo
856 Return forks of this repo
857 """
857 """
858 return Repository.get_repo_forks(self.repo_id)
858 return Repository.get_repo_forks(self.repo_id)
859
859
860 @property
860 @property
861 def parent(self):
861 def parent(self):
862 """
862 """
863 Returns fork parent
863 Returns fork parent
864 """
864 """
865 return self.fork
865 return self.fork
866
866
867 @property
867 @property
868 def just_name(self):
868 def just_name(self):
869 return self.repo_name.split(Repository.url_sep())[-1]
869 return self.repo_name.split(Repository.url_sep())[-1]
870
870
871 @property
871 @property
872 def groups_with_parents(self):
872 def groups_with_parents(self):
873 groups = []
873 groups = []
874 if self.group is None:
874 if self.group is None:
875 return groups
875 return groups
876
876
877 cur_gr = self.group
877 cur_gr = self.group
878 groups.insert(0, cur_gr)
878 groups.insert(0, cur_gr)
879 while 1:
879 while 1:
880 gr = getattr(cur_gr, 'parent_group', None)
880 gr = getattr(cur_gr, 'parent_group', None)
881 cur_gr = cur_gr.parent_group
881 cur_gr = cur_gr.parent_group
882 if gr is None:
882 if gr is None:
883 break
883 break
884 groups.insert(0, gr)
884 groups.insert(0, gr)
885
885
886 return groups
886 return groups
887
887
888 @property
888 @property
889 def groups_and_repo(self):
889 def groups_and_repo(self):
890 return self.groups_with_parents, self.just_name
890 return self.groups_with_parents, self.just_name
891
891
892 @LazyProperty
892 @LazyProperty
893 def repo_path(self):
893 def repo_path(self):
894 """
894 """
895 Returns base full path for that repository means where it actually
895 Returns base full path for that repository means where it actually
896 exists on a filesystem
896 exists on a filesystem
897 """
897 """
898 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
898 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
899 Repository.url_sep())
899 Repository.url_sep())
900 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
900 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
901 return q.one().ui_value
901 return q.one().ui_value
902
902
903 @property
903 @property
904 def repo_full_path(self):
904 def repo_full_path(self):
905 p = [self.repo_path]
905 p = [self.repo_path]
906 # we need to split the name by / since this is how we store the
906 # we need to split the name by / since this is how we store the
907 # names in the database, but that eventually needs to be converted
907 # names in the database, but that eventually needs to be converted
908 # into a valid system path
908 # into a valid system path
909 p += self.repo_name.split(Repository.url_sep())
909 p += self.repo_name.split(Repository.url_sep())
910 return os.path.join(*p)
910 return os.path.join(*p)
911
911
912 @property
912 @property
913 def cache_keys(self):
913 def cache_keys(self):
914 """
914 """
915 Returns associated cache keys for that repo
915 Returns associated cache keys for that repo
916 """
916 """
917 return CacheInvalidation.query()\
917 return CacheInvalidation.query()\
918 .filter(CacheInvalidation.cache_args == self.repo_name)\
918 .filter(CacheInvalidation.cache_args == self.repo_name)\
919 .order_by(CacheInvalidation.cache_key)\
919 .order_by(CacheInvalidation.cache_key)\
920 .all()
920 .all()
921
921
922 def get_new_name(self, repo_name):
922 def get_new_name(self, repo_name):
923 """
923 """
924 returns new full repository name based on assigned group and new new
924 returns new full repository name based on assigned group and new new
925
925
926 :param group_name:
926 :param group_name:
927 """
927 """
928 path_prefix = self.group.full_path_splitted if self.group else []
928 path_prefix = self.group.full_path_splitted if self.group else []
929 return Repository.url_sep().join(path_prefix + [repo_name])
929 return Repository.url_sep().join(path_prefix + [repo_name])
930
930
931 @property
931 @property
932 def _ui(self):
932 def _ui(self):
933 """
933 """
934 Creates an db based ui object for this repository
934 Creates an db based ui object for this repository
935 """
935 """
936 from rhodecode.lib.utils import make_ui
936 from rhodecode.lib.utils import make_ui
937 return make_ui('db', clear_session=False)
937 return make_ui('db', clear_session=False)
938
938
939 @classmethod
939 @classmethod
940 def inject_ui(cls, repo, extras={}):
941 repo.inject_ui(extras)
942
943 @classmethod
944 def is_valid(cls, repo_name):
940 def is_valid(cls, repo_name):
945 """
941 """
946 returns True if given repo name is a valid filesystem repository
942 returns True if given repo name is a valid filesystem repository
947
943
948 :param cls:
944 :param cls:
949 :param repo_name:
945 :param repo_name:
950 """
946 """
951 from rhodecode.lib.utils import is_valid_repo
947 from rhodecode.lib.utils import is_valid_repo
952
948
953 return is_valid_repo(repo_name, cls.base_path())
949 return is_valid_repo(repo_name, cls.base_path())
954
950
955 def get_api_data(self):
951 def get_api_data(self):
956 """
952 """
957 Common function for generating repo api data
953 Common function for generating repo api data
958
954
959 """
955 """
960 repo = self
956 repo = self
961 data = dict(
957 data = dict(
962 repo_id=repo.repo_id,
958 repo_id=repo.repo_id,
963 repo_name=repo.repo_name,
959 repo_name=repo.repo_name,
964 repo_type=repo.repo_type,
960 repo_type=repo.repo_type,
965 clone_uri=repo.clone_uri,
961 clone_uri=repo.clone_uri,
966 private=repo.private,
962 private=repo.private,
967 created_on=repo.created_on,
963 created_on=repo.created_on,
968 description=repo.description,
964 description=repo.description,
969 landing_rev=repo.landing_rev,
965 landing_rev=repo.landing_rev,
970 owner=repo.user.username,
966 owner=repo.user.username,
971 fork_of=repo.fork.repo_name if repo.fork else None,
967 fork_of=repo.fork.repo_name if repo.fork else None,
972 enable_statistics=repo.enable_statistics,
968 enable_statistics=repo.enable_statistics,
973 enable_locking=repo.enable_locking,
969 enable_locking=repo.enable_locking,
974 enable_downloads=repo.enable_downloads,
970 enable_downloads=repo.enable_downloads,
975 last_changeset=repo.changeset_cache,
971 last_changeset=repo.changeset_cache,
976 locked_by=User.get(self.locked[0]).get_api_data() \
972 locked_by=User.get(self.locked[0]).get_api_data() \
977 if self.locked[0] else None,
973 if self.locked[0] else None,
978 locked_date=time_to_datetime(self.locked[1]) \
974 locked_date=time_to_datetime(self.locked[1]) \
979 if self.locked[1] else None
975 if self.locked[1] else None
980 )
976 )
981 rc_config = RhodeCodeSetting.get_app_settings()
977 rc_config = RhodeCodeSetting.get_app_settings()
982 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
978 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
983 if repository_fields:
979 if repository_fields:
984 for f in self.extra_fields:
980 for f in self.extra_fields:
985 data[f.field_key_prefixed] = f.field_value
981 data[f.field_key_prefixed] = f.field_value
986
982
987 return data
983 return data
988
984
989 @classmethod
985 @classmethod
990 def lock(cls, repo, user_id):
986 def lock(cls, repo, user_id):
991 repo.locked = [user_id, time.time()]
987 repo.locked = [user_id, time.time()]
992 Session().add(repo)
988 Session().add(repo)
993 Session().commit()
989 Session().commit()
994
990
995 @classmethod
991 @classmethod
996 def unlock(cls, repo):
992 def unlock(cls, repo):
997 repo.locked = None
993 repo.locked = None
998 Session().add(repo)
994 Session().add(repo)
999 Session().commit()
995 Session().commit()
1000
996
1001 @classmethod
997 @classmethod
1002 def getlock(cls, repo):
998 def getlock(cls, repo):
1003 return repo.locked
999 return repo.locked
1004
1000
1005 @property
1001 @property
1006 def last_db_change(self):
1002 def last_db_change(self):
1007 return self.updated_on
1003 return self.updated_on
1008
1004
1009 def clone_url(self, **override):
1005 def clone_url(self, **override):
1010 from pylons import url
1006 from pylons import url
1011 from urlparse import urlparse
1007 from urlparse import urlparse
1012 import urllib
1008 import urllib
1013 parsed_url = urlparse(url('home', qualified=True))
1009 parsed_url = urlparse(url('home', qualified=True))
1014 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1010 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1015 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1011 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1016 args = {
1012 args = {
1017 'user': '',
1013 'user': '',
1018 'pass': '',
1014 'pass': '',
1019 'scheme': parsed_url.scheme,
1015 'scheme': parsed_url.scheme,
1020 'netloc': parsed_url.netloc,
1016 'netloc': parsed_url.netloc,
1021 'prefix': decoded_path,
1017 'prefix': decoded_path,
1022 'path': self.repo_name
1018 'path': self.repo_name
1023 }
1019 }
1024
1020
1025 args.update(override)
1021 args.update(override)
1026 return default_clone_uri % args
1022 return default_clone_uri % args
1027
1023
1028 #==========================================================================
1024 #==========================================================================
1029 # SCM PROPERTIES
1025 # SCM PROPERTIES
1030 #==========================================================================
1026 #==========================================================================
1031
1027
1032 def get_changeset(self, rev=None):
1028 def get_changeset(self, rev=None):
1033 return get_changeset_safe(self.scm_instance, rev)
1029 return get_changeset_safe(self.scm_instance, rev)
1034
1030
1035 def get_landing_changeset(self):
1031 def get_landing_changeset(self):
1036 """
1032 """
1037 Returns landing changeset, or if that doesn't exist returns the tip
1033 Returns landing changeset, or if that doesn't exist returns the tip
1038 """
1034 """
1039 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1035 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1040 return cs
1036 return cs
1041
1037
1042 def update_changeset_cache(self, cs_cache=None):
1038 def update_changeset_cache(self, cs_cache=None):
1043 """
1039 """
1044 Update cache of last changeset for repository, keys should be::
1040 Update cache of last changeset for repository, keys should be::
1045
1041
1046 short_id
1042 short_id
1047 raw_id
1043 raw_id
1048 revision
1044 revision
1049 message
1045 message
1050 date
1046 date
1051 author
1047 author
1052
1048
1053 :param cs_cache:
1049 :param cs_cache:
1054 """
1050 """
1055 from rhodecode.lib.vcs.backends.base import BaseChangeset
1051 from rhodecode.lib.vcs.backends.base import BaseChangeset
1056 if cs_cache is None:
1052 if cs_cache is None:
1057 cs_cache = EmptyChangeset()
1053 cs_cache = EmptyChangeset()
1058 # use no-cache version here
1054 # use no-cache version here
1059 scm_repo = self.scm_instance_no_cache()
1055 scm_repo = self.scm_instance_no_cache()
1060 if scm_repo:
1056 if scm_repo:
1061 cs_cache = scm_repo.get_changeset()
1057 cs_cache = scm_repo.get_changeset()
1062
1058
1063 if isinstance(cs_cache, BaseChangeset):
1059 if isinstance(cs_cache, BaseChangeset):
1064 cs_cache = cs_cache.__json__()
1060 cs_cache = cs_cache.__json__()
1065
1061
1066 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1062 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1067 _default = datetime.datetime.fromtimestamp(0)
1063 _default = datetime.datetime.fromtimestamp(0)
1068 last_change = cs_cache.get('date') or _default
1064 last_change = cs_cache.get('date') or _default
1069 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1065 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1070 self.updated_on = last_change
1066 self.updated_on = last_change
1071 self.changeset_cache = cs_cache
1067 self.changeset_cache = cs_cache
1072 Session().add(self)
1068 Session().add(self)
1073 Session().commit()
1069 Session().commit()
1074 else:
1070 else:
1075 log.debug('Skipping repo:%s already with latest changes' % self)
1071 log.debug('Skipping repo:%s already with latest changes' % self)
1076
1072
1077 @property
1073 @property
1078 def tip(self):
1074 def tip(self):
1079 return self.get_changeset('tip')
1075 return self.get_changeset('tip')
1080
1076
1081 @property
1077 @property
1082 def author(self):
1078 def author(self):
1083 return self.tip.author
1079 return self.tip.author
1084
1080
1085 @property
1081 @property
1086 def last_change(self):
1082 def last_change(self):
1087 return self.scm_instance.last_change
1083 return self.scm_instance.last_change
1088
1084
1089 def get_comments(self, revisions=None):
1085 def get_comments(self, revisions=None):
1090 """
1086 """
1091 Returns comments for this repository grouped by revisions
1087 Returns comments for this repository grouped by revisions
1092
1088
1093 :param revisions: filter query by revisions only
1089 :param revisions: filter query by revisions only
1094 """
1090 """
1095 cmts = ChangesetComment.query()\
1091 cmts = ChangesetComment.query()\
1096 .filter(ChangesetComment.repo == self)
1092 .filter(ChangesetComment.repo == self)
1097 if revisions:
1093 if revisions:
1098 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1094 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1099 grouped = defaultdict(list)
1095 grouped = defaultdict(list)
1100 for cmt in cmts.all():
1096 for cmt in cmts.all():
1101 grouped[cmt.revision].append(cmt)
1097 grouped[cmt.revision].append(cmt)
1102 return grouped
1098 return grouped
1103
1099
1104 def statuses(self, revisions=None):
1100 def statuses(self, revisions=None):
1105 """
1101 """
1106 Returns statuses for this repository
1102 Returns statuses for this repository
1107
1103
1108 :param revisions: list of revisions to get statuses for
1104 :param revisions: list of revisions to get statuses for
1109 :type revisions: list
1105 :type revisions: list
1110 """
1106 """
1111
1107
1112 statuses = ChangesetStatus.query()\
1108 statuses = ChangesetStatus.query()\
1113 .filter(ChangesetStatus.repo == self)\
1109 .filter(ChangesetStatus.repo == self)\
1114 .filter(ChangesetStatus.version == 0)
1110 .filter(ChangesetStatus.version == 0)
1115 if revisions:
1111 if revisions:
1116 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1112 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1117 grouped = {}
1113 grouped = {}
1118
1114
1119 #maybe we have open new pullrequest without a status ?
1115 #maybe we have open new pullrequest without a status ?
1120 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1116 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1121 status_lbl = ChangesetStatus.get_status_lbl(stat)
1117 status_lbl = ChangesetStatus.get_status_lbl(stat)
1122 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1118 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1123 for rev in pr.revisions:
1119 for rev in pr.revisions:
1124 pr_id = pr.pull_request_id
1120 pr_id = pr.pull_request_id
1125 pr_repo = pr.other_repo.repo_name
1121 pr_repo = pr.other_repo.repo_name
1126 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1122 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1127
1123
1128 for stat in statuses.all():
1124 for stat in statuses.all():
1129 pr_id = pr_repo = None
1125 pr_id = pr_repo = None
1130 if stat.pull_request:
1126 if stat.pull_request:
1131 pr_id = stat.pull_request.pull_request_id
1127 pr_id = stat.pull_request.pull_request_id
1132 pr_repo = stat.pull_request.other_repo.repo_name
1128 pr_repo = stat.pull_request.other_repo.repo_name
1133 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1129 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1134 pr_id, pr_repo]
1130 pr_id, pr_repo]
1135 return grouped
1131 return grouped
1136
1132
1137 def _repo_size(self):
1133 def _repo_size(self):
1138 from rhodecode.lib import helpers as h
1134 from rhodecode.lib import helpers as h
1139 log.debug('calculating repository size...')
1135 log.debug('calculating repository size...')
1140 return h.format_byte_size(self.scm_instance.size)
1136 return h.format_byte_size(self.scm_instance.size)
1141
1137
1142 #==========================================================================
1138 #==========================================================================
1143 # SCM CACHE INSTANCE
1139 # SCM CACHE INSTANCE
1144 #==========================================================================
1140 #==========================================================================
1145
1141
1146 @property
1142 @property
1147 def invalidate(self):
1143 def invalidate(self):
1148 return CacheInvalidation.invalidate(self.repo_name)
1144 return CacheInvalidation.invalidate(self.repo_name)
1149
1145
1150 def set_invalidate(self):
1146 def set_invalidate(self):
1151 """
1147 """
1152 set a cache for invalidation for this instance
1148 set a cache for invalidation for this instance
1153 """
1149 """
1154 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1150 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1155
1151
1156 def scm_instance_no_cache(self):
1152 def scm_instance_no_cache(self):
1157 return self.__get_instance()
1153 return self.__get_instance()
1158
1154
1159 @LazyProperty
1155 @LazyProperty
1160 def scm_instance(self):
1156 def scm_instance(self):
1161 import rhodecode
1157 import rhodecode
1162 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1158 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1163 if full_cache:
1159 if full_cache:
1164 return self.scm_instance_cached()
1160 return self.scm_instance_cached()
1165 return self.__get_instance()
1161 return self.__get_instance()
1166
1162
1167 def scm_instance_cached(self, cache_map=None):
1163 def scm_instance_cached(self, cache_map=None):
1168 @cache_region('long_term')
1164 @cache_region('long_term')
1169 def _c(repo_name):
1165 def _c(repo_name):
1170 return self.__get_instance()
1166 return self.__get_instance()
1171 rn = self.repo_name
1167 rn = self.repo_name
1172 log.debug('Getting cached instance of repo')
1168 log.debug('Getting cached instance of repo')
1173
1169
1174 if cache_map:
1170 if cache_map:
1175 # get using prefilled cache_map
1171 # get using prefilled cache_map
1176 invalidate_repo = cache_map[self.repo_name]
1172 invalidate_repo = cache_map[self.repo_name]
1177 if invalidate_repo:
1173 if invalidate_repo:
1178 invalidate_repo = (None if invalidate_repo.cache_active
1174 invalidate_repo = (None if invalidate_repo.cache_active
1179 else invalidate_repo)
1175 else invalidate_repo)
1180 else:
1176 else:
1181 # get from invalidate
1177 # get from invalidate
1182 invalidate_repo = self.invalidate
1178 invalidate_repo = self.invalidate
1183
1179
1184 if invalidate_repo is not None:
1180 if invalidate_repo is not None:
1185 region_invalidate(_c, None, rn)
1181 region_invalidate(_c, None, rn)
1186 # update our cache
1182 # update our cache
1187 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1183 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1188 return _c(rn)
1184 return _c(rn)
1189
1185
1190 def __get_instance(self):
1186 def __get_instance(self):
1191 repo_full_path = self.repo_full_path
1187 repo_full_path = self.repo_full_path
1192 try:
1188 try:
1193 alias = get_scm(repo_full_path)[0]
1189 alias = get_scm(repo_full_path)[0]
1194 log.debug('Creating instance of %s repository from %s'
1190 log.debug('Creating instance of %s repository from %s'
1195 % (alias, repo_full_path))
1191 % (alias, repo_full_path))
1196 backend = get_backend(alias)
1192 backend = get_backend(alias)
1197 except VCSError:
1193 except VCSError:
1198 log.error(traceback.format_exc())
1194 log.error(traceback.format_exc())
1199 log.error('Perhaps this repository is in db and not in '
1195 log.error('Perhaps this repository is in db and not in '
1200 'filesystem run rescan repositories with '
1196 'filesystem run rescan repositories with '
1201 '"destroy old data " option from admin panel')
1197 '"destroy old data " option from admin panel')
1202 return
1198 return
1203
1199
1204 if alias == 'hg':
1200 if alias == 'hg':
1205
1201
1206 repo = backend(safe_str(repo_full_path), create=False,
1202 repo = backend(safe_str(repo_full_path), create=False,
1207 baseui=self._ui)
1203 baseui=self._ui)
1208 # skip hidden web repository
1204 # skip hidden web repository
1209 if repo._get_hidden():
1205 if repo._get_hidden():
1210 return
1206 return
1211 else:
1207 else:
1212 repo = backend(repo_full_path, create=False)
1208 repo = backend(repo_full_path, create=False)
1213
1209
1214 return repo
1210 return repo
1215
1211
1216
1212
1217 class RepoGroup(Base, BaseModel):
1213 class RepoGroup(Base, BaseModel):
1218 __tablename__ = 'groups'
1214 __tablename__ = 'groups'
1219 __table_args__ = (
1215 __table_args__ = (
1220 UniqueConstraint('group_name', 'group_parent_id'),
1216 UniqueConstraint('group_name', 'group_parent_id'),
1221 CheckConstraint('group_id != group_parent_id'),
1217 CheckConstraint('group_id != group_parent_id'),
1222 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1223 'mysql_charset': 'utf8'},
1219 'mysql_charset': 'utf8'},
1224 )
1220 )
1225 __mapper_args__ = {'order_by': 'group_name'}
1221 __mapper_args__ = {'order_by': 'group_name'}
1226
1222
1227 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1223 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1228 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1224 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1229 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1225 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1230 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1226 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1231 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1227 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1232
1228
1233 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1229 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1234 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1230 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1235
1231
1236 parent_group = relationship('RepoGroup', remote_side=group_id)
1232 parent_group = relationship('RepoGroup', remote_side=group_id)
1237
1233
1238 def __init__(self, group_name='', parent_group=None):
1234 def __init__(self, group_name='', parent_group=None):
1239 self.group_name = group_name
1235 self.group_name = group_name
1240 self.parent_group = parent_group
1236 self.parent_group = parent_group
1241
1237
1242 def __unicode__(self):
1238 def __unicode__(self):
1243 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1239 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1244 self.group_name)
1240 self.group_name)
1245
1241
1246 @classmethod
1242 @classmethod
1247 def groups_choices(cls, groups=None, show_empty_group=True):
1243 def groups_choices(cls, groups=None, show_empty_group=True):
1248 from webhelpers.html import literal as _literal
1244 from webhelpers.html import literal as _literal
1249 if not groups:
1245 if not groups:
1250 groups = cls.query().all()
1246 groups = cls.query().all()
1251
1247
1252 repo_groups = []
1248 repo_groups = []
1253 if show_empty_group:
1249 if show_empty_group:
1254 repo_groups = [('-1', '-- %s --' % _('top level'))]
1250 repo_groups = [('-1', '-- %s --' % _('top level'))]
1255 sep = ' &raquo; '
1251 sep = ' &raquo; '
1256 _name = lambda k: _literal(sep.join(k))
1252 _name = lambda k: _literal(sep.join(k))
1257
1253
1258 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1254 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1259 for x in groups])
1255 for x in groups])
1260
1256
1261 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1257 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1262 return repo_groups
1258 return repo_groups
1263
1259
1264 @classmethod
1260 @classmethod
1265 def url_sep(cls):
1261 def url_sep(cls):
1266 return URL_SEP
1262 return URL_SEP
1267
1263
1268 @classmethod
1264 @classmethod
1269 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1265 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1270 if case_insensitive:
1266 if case_insensitive:
1271 gr = cls.query()\
1267 gr = cls.query()\
1272 .filter(cls.group_name.ilike(group_name))
1268 .filter(cls.group_name.ilike(group_name))
1273 else:
1269 else:
1274 gr = cls.query()\
1270 gr = cls.query()\
1275 .filter(cls.group_name == group_name)
1271 .filter(cls.group_name == group_name)
1276 if cache:
1272 if cache:
1277 gr = gr.options(FromCache(
1273 gr = gr.options(FromCache(
1278 "sql_cache_short",
1274 "sql_cache_short",
1279 "get_group_%s" % _hash_key(group_name)
1275 "get_group_%s" % _hash_key(group_name)
1280 )
1276 )
1281 )
1277 )
1282 return gr.scalar()
1278 return gr.scalar()
1283
1279
1284 @property
1280 @property
1285 def parents(self):
1281 def parents(self):
1286 parents_recursion_limit = 5
1282 parents_recursion_limit = 5
1287 groups = []
1283 groups = []
1288 if self.parent_group is None:
1284 if self.parent_group is None:
1289 return groups
1285 return groups
1290 cur_gr = self.parent_group
1286 cur_gr = self.parent_group
1291 groups.insert(0, cur_gr)
1287 groups.insert(0, cur_gr)
1292 cnt = 0
1288 cnt = 0
1293 while 1:
1289 while 1:
1294 cnt += 1
1290 cnt += 1
1295 gr = getattr(cur_gr, 'parent_group', None)
1291 gr = getattr(cur_gr, 'parent_group', None)
1296 cur_gr = cur_gr.parent_group
1292 cur_gr = cur_gr.parent_group
1297 if gr is None:
1293 if gr is None:
1298 break
1294 break
1299 if cnt == parents_recursion_limit:
1295 if cnt == parents_recursion_limit:
1300 # this will prevent accidental infinit loops
1296 # this will prevent accidental infinit loops
1301 log.error('group nested more than %s' %
1297 log.error('group nested more than %s' %
1302 parents_recursion_limit)
1298 parents_recursion_limit)
1303 break
1299 break
1304
1300
1305 groups.insert(0, gr)
1301 groups.insert(0, gr)
1306 return groups
1302 return groups
1307
1303
1308 @property
1304 @property
1309 def children(self):
1305 def children(self):
1310 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1306 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1311
1307
1312 @property
1308 @property
1313 def name(self):
1309 def name(self):
1314 return self.group_name.split(RepoGroup.url_sep())[-1]
1310 return self.group_name.split(RepoGroup.url_sep())[-1]
1315
1311
1316 @property
1312 @property
1317 def full_path(self):
1313 def full_path(self):
1318 return self.group_name
1314 return self.group_name
1319
1315
1320 @property
1316 @property
1321 def full_path_splitted(self):
1317 def full_path_splitted(self):
1322 return self.group_name.split(RepoGroup.url_sep())
1318 return self.group_name.split(RepoGroup.url_sep())
1323
1319
1324 @property
1320 @property
1325 def repositories(self):
1321 def repositories(self):
1326 return Repository.query()\
1322 return Repository.query()\
1327 .filter(Repository.group == self)\
1323 .filter(Repository.group == self)\
1328 .order_by(Repository.repo_name)
1324 .order_by(Repository.repo_name)
1329
1325
1330 @property
1326 @property
1331 def repositories_recursive_count(self):
1327 def repositories_recursive_count(self):
1332 cnt = self.repositories.count()
1328 cnt = self.repositories.count()
1333
1329
1334 def children_count(group):
1330 def children_count(group):
1335 cnt = 0
1331 cnt = 0
1336 for child in group.children:
1332 for child in group.children:
1337 cnt += child.repositories.count()
1333 cnt += child.repositories.count()
1338 cnt += children_count(child)
1334 cnt += children_count(child)
1339 return cnt
1335 return cnt
1340
1336
1341 return cnt + children_count(self)
1337 return cnt + children_count(self)
1342
1338
1343 def _recursive_objects(self, include_repos=True):
1339 def _recursive_objects(self, include_repos=True):
1344 all_ = []
1340 all_ = []
1345
1341
1346 def _get_members(root_gr):
1342 def _get_members(root_gr):
1347 if include_repos:
1343 if include_repos:
1348 for r in root_gr.repositories:
1344 for r in root_gr.repositories:
1349 all_.append(r)
1345 all_.append(r)
1350 childs = root_gr.children.all()
1346 childs = root_gr.children.all()
1351 if childs:
1347 if childs:
1352 for gr in childs:
1348 for gr in childs:
1353 all_.append(gr)
1349 all_.append(gr)
1354 _get_members(gr)
1350 _get_members(gr)
1355
1351
1356 _get_members(self)
1352 _get_members(self)
1357 return [self] + all_
1353 return [self] + all_
1358
1354
1359 def recursive_groups_and_repos(self):
1355 def recursive_groups_and_repos(self):
1360 """
1356 """
1361 Recursive return all groups, with repositories in those groups
1357 Recursive return all groups, with repositories in those groups
1362 """
1358 """
1363 return self._recursive_objects()
1359 return self._recursive_objects()
1364
1360
1365 def recursive_groups(self):
1361 def recursive_groups(self):
1366 """
1362 """
1367 Returns all children groups for this group including children of children
1363 Returns all children groups for this group including children of children
1368 """
1364 """
1369 return self._recursive_objects(include_repos=False)
1365 return self._recursive_objects(include_repos=False)
1370
1366
1371 def get_new_name(self, group_name):
1367 def get_new_name(self, group_name):
1372 """
1368 """
1373 returns new full group name based on parent and new name
1369 returns new full group name based on parent and new name
1374
1370
1375 :param group_name:
1371 :param group_name:
1376 """
1372 """
1377 path_prefix = (self.parent_group.full_path_splitted if
1373 path_prefix = (self.parent_group.full_path_splitted if
1378 self.parent_group else [])
1374 self.parent_group else [])
1379 return RepoGroup.url_sep().join(path_prefix + [group_name])
1375 return RepoGroup.url_sep().join(path_prefix + [group_name])
1380
1376
1381
1377
1382 class Permission(Base, BaseModel):
1378 class Permission(Base, BaseModel):
1383 __tablename__ = 'permissions'
1379 __tablename__ = 'permissions'
1384 __table_args__ = (
1380 __table_args__ = (
1385 Index('p_perm_name_idx', 'permission_name'),
1381 Index('p_perm_name_idx', 'permission_name'),
1386 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1382 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1387 'mysql_charset': 'utf8'},
1383 'mysql_charset': 'utf8'},
1388 )
1384 )
1389 PERMS = [
1385 PERMS = [
1390 ('repository.none', _('Repository no access')),
1386 ('repository.none', _('Repository no access')),
1391 ('repository.read', _('Repository read access')),
1387 ('repository.read', _('Repository read access')),
1392 ('repository.write', _('Repository write access')),
1388 ('repository.write', _('Repository write access')),
1393 ('repository.admin', _('Repository admin access')),
1389 ('repository.admin', _('Repository admin access')),
1394
1390
1395 ('group.none', _('Repository group no access')),
1391 ('group.none', _('Repository group no access')),
1396 ('group.read', _('Repository group read access')),
1392 ('group.read', _('Repository group read access')),
1397 ('group.write', _('Repository group write access')),
1393 ('group.write', _('Repository group write access')),
1398 ('group.admin', _('Repository group admin access')),
1394 ('group.admin', _('Repository group admin access')),
1399
1395
1400 ('hg.admin', _('RhodeCode Administrator')),
1396 ('hg.admin', _('RhodeCode Administrator')),
1401 ('hg.create.none', _('Repository creation disabled')),
1397 ('hg.create.none', _('Repository creation disabled')),
1402 ('hg.create.repository', _('Repository creation enabled')),
1398 ('hg.create.repository', _('Repository creation enabled')),
1403 ('hg.fork.none', _('Repository forking disabled')),
1399 ('hg.fork.none', _('Repository forking disabled')),
1404 ('hg.fork.repository', _('Repository forking enabled')),
1400 ('hg.fork.repository', _('Repository forking enabled')),
1405 ('hg.register.none', _('Register disabled')),
1401 ('hg.register.none', _('Register disabled')),
1406 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1402 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1407 'with manual activation')),
1403 'with manual activation')),
1408
1404
1409 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1405 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1410 'with auto activation')),
1406 'with auto activation')),
1411 ]
1407 ]
1412
1408
1413 # defines which permissions are more important higher the more important
1409 # defines which permissions are more important higher the more important
1414 PERM_WEIGHTS = {
1410 PERM_WEIGHTS = {
1415 'repository.none': 0,
1411 'repository.none': 0,
1416 'repository.read': 1,
1412 'repository.read': 1,
1417 'repository.write': 3,
1413 'repository.write': 3,
1418 'repository.admin': 4,
1414 'repository.admin': 4,
1419
1415
1420 'group.none': 0,
1416 'group.none': 0,
1421 'group.read': 1,
1417 'group.read': 1,
1422 'group.write': 3,
1418 'group.write': 3,
1423 'group.admin': 4,
1419 'group.admin': 4,
1424
1420
1425 'hg.fork.none': 0,
1421 'hg.fork.none': 0,
1426 'hg.fork.repository': 1,
1422 'hg.fork.repository': 1,
1427 'hg.create.none': 0,
1423 'hg.create.none': 0,
1428 'hg.create.repository':1
1424 'hg.create.repository':1
1429 }
1425 }
1430
1426
1431 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1427 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1432 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1428 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1433 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1429 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1434
1430
1435 def __unicode__(self):
1431 def __unicode__(self):
1436 return u"<%s('%s:%s')>" % (
1432 return u"<%s('%s:%s')>" % (
1437 self.__class__.__name__, self.permission_id, self.permission_name
1433 self.__class__.__name__, self.permission_id, self.permission_name
1438 )
1434 )
1439
1435
1440 @classmethod
1436 @classmethod
1441 def get_by_key(cls, key):
1437 def get_by_key(cls, key):
1442 return cls.query().filter(cls.permission_name == key).scalar()
1438 return cls.query().filter(cls.permission_name == key).scalar()
1443
1439
1444 @classmethod
1440 @classmethod
1445 def get_default_perms(cls, default_user_id):
1441 def get_default_perms(cls, default_user_id):
1446 q = Session().query(UserRepoToPerm, Repository, cls)\
1442 q = Session().query(UserRepoToPerm, Repository, cls)\
1447 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1443 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1448 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1444 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1449 .filter(UserRepoToPerm.user_id == default_user_id)
1445 .filter(UserRepoToPerm.user_id == default_user_id)
1450
1446
1451 return q.all()
1447 return q.all()
1452
1448
1453 @classmethod
1449 @classmethod
1454 def get_default_group_perms(cls, default_user_id):
1450 def get_default_group_perms(cls, default_user_id):
1455 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1451 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1456 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1452 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1457 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1453 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1458 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1454 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1459
1455
1460 return q.all()
1456 return q.all()
1461
1457
1462
1458
1463 class UserRepoToPerm(Base, BaseModel):
1459 class UserRepoToPerm(Base, BaseModel):
1464 __tablename__ = 'repo_to_perm'
1460 __tablename__ = 'repo_to_perm'
1465 __table_args__ = (
1461 __table_args__ = (
1466 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1462 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1467 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1468 'mysql_charset': 'utf8'}
1464 'mysql_charset': 'utf8'}
1469 )
1465 )
1470 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1466 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1471 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1467 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1472 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1468 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1473 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1469 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1474
1470
1475 user = relationship('User')
1471 user = relationship('User')
1476 repository = relationship('Repository')
1472 repository = relationship('Repository')
1477 permission = relationship('Permission')
1473 permission = relationship('Permission')
1478
1474
1479 @classmethod
1475 @classmethod
1480 def create(cls, user, repository, permission):
1476 def create(cls, user, repository, permission):
1481 n = cls()
1477 n = cls()
1482 n.user = user
1478 n.user = user
1483 n.repository = repository
1479 n.repository = repository
1484 n.permission = permission
1480 n.permission = permission
1485 Session().add(n)
1481 Session().add(n)
1486 return n
1482 return n
1487
1483
1488 def __unicode__(self):
1484 def __unicode__(self):
1489 return u'<user:%s => %s >' % (self.user, self.repository)
1485 return u'<user:%s => %s >' % (self.user, self.repository)
1490
1486
1491
1487
1492 class UserToPerm(Base, BaseModel):
1488 class UserToPerm(Base, BaseModel):
1493 __tablename__ = 'user_to_perm'
1489 __tablename__ = 'user_to_perm'
1494 __table_args__ = (
1490 __table_args__ = (
1495 UniqueConstraint('user_id', 'permission_id'),
1491 UniqueConstraint('user_id', 'permission_id'),
1496 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1492 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1497 'mysql_charset': 'utf8'}
1493 'mysql_charset': 'utf8'}
1498 )
1494 )
1499 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1495 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1500 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1496 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1501 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1502
1498
1503 user = relationship('User')
1499 user = relationship('User')
1504 permission = relationship('Permission', lazy='joined')
1500 permission = relationship('Permission', lazy='joined')
1505
1501
1506
1502
1507 class UserGroupRepoToPerm(Base, BaseModel):
1503 class UserGroupRepoToPerm(Base, BaseModel):
1508 __tablename__ = 'users_group_repo_to_perm'
1504 __tablename__ = 'users_group_repo_to_perm'
1509 __table_args__ = (
1505 __table_args__ = (
1510 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1506 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1511 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1507 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1512 'mysql_charset': 'utf8'}
1508 'mysql_charset': 'utf8'}
1513 )
1509 )
1514 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1510 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1515 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1511 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1516 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1512 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1517 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1513 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1518
1514
1519 users_group = relationship('UserGroup')
1515 users_group = relationship('UserGroup')
1520 permission = relationship('Permission')
1516 permission = relationship('Permission')
1521 repository = relationship('Repository')
1517 repository = relationship('Repository')
1522
1518
1523 @classmethod
1519 @classmethod
1524 def create(cls, users_group, repository, permission):
1520 def create(cls, users_group, repository, permission):
1525 n = cls()
1521 n = cls()
1526 n.users_group = users_group
1522 n.users_group = users_group
1527 n.repository = repository
1523 n.repository = repository
1528 n.permission = permission
1524 n.permission = permission
1529 Session().add(n)
1525 Session().add(n)
1530 return n
1526 return n
1531
1527
1532 def __unicode__(self):
1528 def __unicode__(self):
1533 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1529 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1534
1530
1535
1531
1536 class UserGroupToPerm(Base, BaseModel):
1532 class UserGroupToPerm(Base, BaseModel):
1537 __tablename__ = 'users_group_to_perm'
1533 __tablename__ = 'users_group_to_perm'
1538 __table_args__ = (
1534 __table_args__ = (
1539 UniqueConstraint('users_group_id', 'permission_id',),
1535 UniqueConstraint('users_group_id', 'permission_id',),
1540 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1536 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1541 'mysql_charset': 'utf8'}
1537 'mysql_charset': 'utf8'}
1542 )
1538 )
1543 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1539 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1544 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1540 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1541 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1546
1542
1547 users_group = relationship('UserGroup')
1543 users_group = relationship('UserGroup')
1548 permission = relationship('Permission')
1544 permission = relationship('Permission')
1549
1545
1550
1546
1551 class UserRepoGroupToPerm(Base, BaseModel):
1547 class UserRepoGroupToPerm(Base, BaseModel):
1552 __tablename__ = 'user_repo_group_to_perm'
1548 __tablename__ = 'user_repo_group_to_perm'
1553 __table_args__ = (
1549 __table_args__ = (
1554 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1550 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1555 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1551 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1556 'mysql_charset': 'utf8'}
1552 'mysql_charset': 'utf8'}
1557 )
1553 )
1558
1554
1559 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1555 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1560 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1556 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1561 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1557 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1562 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1558 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1563
1559
1564 user = relationship('User')
1560 user = relationship('User')
1565 group = relationship('RepoGroup')
1561 group = relationship('RepoGroup')
1566 permission = relationship('Permission')
1562 permission = relationship('Permission')
1567
1563
1568
1564
1569 class UserGroupRepoGroupToPerm(Base, BaseModel):
1565 class UserGroupRepoGroupToPerm(Base, BaseModel):
1570 __tablename__ = 'users_group_repo_group_to_perm'
1566 __tablename__ = 'users_group_repo_group_to_perm'
1571 __table_args__ = (
1567 __table_args__ = (
1572 UniqueConstraint('users_group_id', 'group_id'),
1568 UniqueConstraint('users_group_id', 'group_id'),
1573 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1574 'mysql_charset': 'utf8'}
1570 'mysql_charset': 'utf8'}
1575 )
1571 )
1576
1572
1577 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)
1573 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)
1578 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1574 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1579 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1575 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1576 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1581
1577
1582 users_group = relationship('UserGroup')
1578 users_group = relationship('UserGroup')
1583 permission = relationship('Permission')
1579 permission = relationship('Permission')
1584 group = relationship('RepoGroup')
1580 group = relationship('RepoGroup')
1585
1581
1586
1582
1587 class Statistics(Base, BaseModel):
1583 class Statistics(Base, BaseModel):
1588 __tablename__ = 'statistics'
1584 __tablename__ = 'statistics'
1589 __table_args__ = (
1585 __table_args__ = (
1590 UniqueConstraint('repository_id'),
1586 UniqueConstraint('repository_id'),
1591 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1592 'mysql_charset': 'utf8'}
1588 'mysql_charset': 'utf8'}
1593 )
1589 )
1594 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1590 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1595 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1591 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1596 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1592 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1597 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1593 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1598 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1594 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1599 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1595 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1600
1596
1601 repository = relationship('Repository', single_parent=True)
1597 repository = relationship('Repository', single_parent=True)
1602
1598
1603
1599
1604 class UserFollowing(Base, BaseModel):
1600 class UserFollowing(Base, BaseModel):
1605 __tablename__ = 'user_followings'
1601 __tablename__ = 'user_followings'
1606 __table_args__ = (
1602 __table_args__ = (
1607 UniqueConstraint('user_id', 'follows_repository_id'),
1603 UniqueConstraint('user_id', 'follows_repository_id'),
1608 UniqueConstraint('user_id', 'follows_user_id'),
1604 UniqueConstraint('user_id', 'follows_user_id'),
1609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1605 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1610 'mysql_charset': 'utf8'}
1606 'mysql_charset': 'utf8'}
1611 )
1607 )
1612
1608
1613 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1609 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1614 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1610 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1615 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1611 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1616 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1612 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1617 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1613 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1618
1614
1619 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1615 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1620
1616
1621 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1617 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1622 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1618 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1623
1619
1624 @classmethod
1620 @classmethod
1625 def get_repo_followers(cls, repo_id):
1621 def get_repo_followers(cls, repo_id):
1626 return cls.query().filter(cls.follows_repo_id == repo_id)
1622 return cls.query().filter(cls.follows_repo_id == repo_id)
1627
1623
1628
1624
1629 class CacheInvalidation(Base, BaseModel):
1625 class CacheInvalidation(Base, BaseModel):
1630 __tablename__ = 'cache_invalidation'
1626 __tablename__ = 'cache_invalidation'
1631 __table_args__ = (
1627 __table_args__ = (
1632 UniqueConstraint('cache_key'),
1628 UniqueConstraint('cache_key'),
1633 Index('key_idx', 'cache_key'),
1629 Index('key_idx', 'cache_key'),
1634 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1630 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1635 'mysql_charset': 'utf8'},
1631 'mysql_charset': 'utf8'},
1636 )
1632 )
1637 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1633 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1638 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1634 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1639 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1635 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1640 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1636 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1641
1637
1642 def __init__(self, cache_key, cache_args=''):
1638 def __init__(self, cache_key, cache_args=''):
1643 self.cache_key = cache_key
1639 self.cache_key = cache_key
1644 self.cache_args = cache_args
1640 self.cache_args = cache_args
1645 self.cache_active = False
1641 self.cache_active = False
1646
1642
1647 def __unicode__(self):
1643 def __unicode__(self):
1648 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1644 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1649 self.cache_id, self.cache_key)
1645 self.cache_id, self.cache_key)
1650
1646
1651 @property
1647 @property
1652 def prefix(self):
1648 def prefix(self):
1653 _split = self.cache_key.split(self.cache_args, 1)
1649 _split = self.cache_key.split(self.cache_args, 1)
1654 if _split and len(_split) == 2:
1650 if _split and len(_split) == 2:
1655 return _split[0]
1651 return _split[0]
1656 return ''
1652 return ''
1657
1653
1658 @classmethod
1654 @classmethod
1659 def clear_cache(cls):
1655 def clear_cache(cls):
1660 cls.query().delete()
1656 cls.query().delete()
1661
1657
1662 @classmethod
1658 @classmethod
1663 def _get_key(cls, key):
1659 def _get_key(cls, key):
1664 """
1660 """
1665 Wrapper for generating a key, together with a prefix
1661 Wrapper for generating a key, together with a prefix
1666
1662
1667 :param key:
1663 :param key:
1668 """
1664 """
1669 import rhodecode
1665 import rhodecode
1670 prefix = ''
1666 prefix = ''
1671 org_key = key
1667 org_key = key
1672 iid = rhodecode.CONFIG.get('instance_id')
1668 iid = rhodecode.CONFIG.get('instance_id')
1673 if iid:
1669 if iid:
1674 prefix = iid
1670 prefix = iid
1675
1671
1676 return "%s%s" % (prefix, key), prefix, org_key
1672 return "%s%s" % (prefix, key), prefix, org_key
1677
1673
1678 @classmethod
1674 @classmethod
1679 def get_by_key(cls, key):
1675 def get_by_key(cls, key):
1680 return cls.query().filter(cls.cache_key == key).scalar()
1676 return cls.query().filter(cls.cache_key == key).scalar()
1681
1677
1682 @classmethod
1678 @classmethod
1683 def get_by_repo_name(cls, repo_name):
1679 def get_by_repo_name(cls, repo_name):
1684 return cls.query().filter(cls.cache_args == repo_name).all()
1680 return cls.query().filter(cls.cache_args == repo_name).all()
1685
1681
1686 @classmethod
1682 @classmethod
1687 def _get_or_create_key(cls, key, repo_name, commit=True):
1683 def _get_or_create_key(cls, key, repo_name, commit=True):
1688 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1684 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1689 if not inv_obj:
1685 if not inv_obj:
1690 try:
1686 try:
1691 inv_obj = CacheInvalidation(key, repo_name)
1687 inv_obj = CacheInvalidation(key, repo_name)
1692 Session().add(inv_obj)
1688 Session().add(inv_obj)
1693 if commit:
1689 if commit:
1694 Session().commit()
1690 Session().commit()
1695 except Exception:
1691 except Exception:
1696 log.error(traceback.format_exc())
1692 log.error(traceback.format_exc())
1697 Session().rollback()
1693 Session().rollback()
1698 return inv_obj
1694 return inv_obj
1699
1695
1700 @classmethod
1696 @classmethod
1701 def invalidate(cls, key):
1697 def invalidate(cls, key):
1702 """
1698 """
1703 Returns Invalidation object if this given key should be invalidated
1699 Returns Invalidation object if this given key should be invalidated
1704 None otherwise. `cache_active = False` means that this cache
1700 None otherwise. `cache_active = False` means that this cache
1705 state is not valid and needs to be invalidated
1701 state is not valid and needs to be invalidated
1706
1702
1707 :param key:
1703 :param key:
1708 """
1704 """
1709 repo_name = key
1705 repo_name = key
1710 repo_name = remove_suffix(repo_name, '_README')
1706 repo_name = remove_suffix(repo_name, '_README')
1711 repo_name = remove_suffix(repo_name, '_RSS')
1707 repo_name = remove_suffix(repo_name, '_RSS')
1712 repo_name = remove_suffix(repo_name, '_ATOM')
1708 repo_name = remove_suffix(repo_name, '_ATOM')
1713
1709
1714 # adds instance prefix
1710 # adds instance prefix
1715 key, _prefix, _org_key = cls._get_key(key)
1711 key, _prefix, _org_key = cls._get_key(key)
1716 inv = cls._get_or_create_key(key, repo_name)
1712 inv = cls._get_or_create_key(key, repo_name)
1717
1713
1718 if inv and inv.cache_active is False:
1714 if inv and inv.cache_active is False:
1719 return inv
1715 return inv
1720
1716
1721 @classmethod
1717 @classmethod
1722 def set_invalidate(cls, key=None, repo_name=None):
1718 def set_invalidate(cls, key=None, repo_name=None):
1723 """
1719 """
1724 Mark this Cache key for invalidation, either by key or whole
1720 Mark this Cache key for invalidation, either by key or whole
1725 cache sets based on repo_name
1721 cache sets based on repo_name
1726
1722
1727 :param key:
1723 :param key:
1728 """
1724 """
1729 invalidated_keys = []
1725 invalidated_keys = []
1730 if key:
1726 if key:
1731 key, _prefix, _org_key = cls._get_key(key)
1727 key, _prefix, _org_key = cls._get_key(key)
1732 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1728 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1733 elif repo_name:
1729 elif repo_name:
1734 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1730 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1735
1731
1736 try:
1732 try:
1737 for inv_obj in inv_objs:
1733 for inv_obj in inv_objs:
1738 inv_obj.cache_active = False
1734 inv_obj.cache_active = False
1739 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1735 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1740 % (inv_obj, key, safe_str(repo_name)))
1736 % (inv_obj, key, safe_str(repo_name)))
1741 invalidated_keys.append(inv_obj.cache_key)
1737 invalidated_keys.append(inv_obj.cache_key)
1742 Session().add(inv_obj)
1738 Session().add(inv_obj)
1743 Session().commit()
1739 Session().commit()
1744 except Exception:
1740 except Exception:
1745 log.error(traceback.format_exc())
1741 log.error(traceback.format_exc())
1746 Session().rollback()
1742 Session().rollback()
1747 return invalidated_keys
1743 return invalidated_keys
1748
1744
1749 @classmethod
1745 @classmethod
1750 def set_valid(cls, key):
1746 def set_valid(cls, key):
1751 """
1747 """
1752 Mark this cache key as active and currently cached
1748 Mark this cache key as active and currently cached
1753
1749
1754 :param key:
1750 :param key:
1755 """
1751 """
1756 inv_obj = cls.get_by_key(key)
1752 inv_obj = cls.get_by_key(key)
1757 inv_obj.cache_active = True
1753 inv_obj.cache_active = True
1758 Session().add(inv_obj)
1754 Session().add(inv_obj)
1759 Session().commit()
1755 Session().commit()
1760
1756
1761 @classmethod
1757 @classmethod
1762 def get_cache_map(cls):
1758 def get_cache_map(cls):
1763
1759
1764 class cachemapdict(dict):
1760 class cachemapdict(dict):
1765
1761
1766 def __init__(self, *args, **kwargs):
1762 def __init__(self, *args, **kwargs):
1767 fixkey = kwargs.get('fixkey')
1763 fixkey = kwargs.get('fixkey')
1768 if fixkey:
1764 if fixkey:
1769 del kwargs['fixkey']
1765 del kwargs['fixkey']
1770 self.fixkey = fixkey
1766 self.fixkey = fixkey
1771 super(cachemapdict, self).__init__(*args, **kwargs)
1767 super(cachemapdict, self).__init__(*args, **kwargs)
1772
1768
1773 def __getattr__(self, name):
1769 def __getattr__(self, name):
1774 key = name
1770 key = name
1775 if self.fixkey:
1771 if self.fixkey:
1776 key, _prefix, _org_key = cls._get_key(key)
1772 key, _prefix, _org_key = cls._get_key(key)
1777 if key in self.__dict__:
1773 if key in self.__dict__:
1778 return self.__dict__[key]
1774 return self.__dict__[key]
1779 else:
1775 else:
1780 return self[key]
1776 return self[key]
1781
1777
1782 def __getitem__(self, key):
1778 def __getitem__(self, key):
1783 if self.fixkey:
1779 if self.fixkey:
1784 key, _prefix, _org_key = cls._get_key(key)
1780 key, _prefix, _org_key = cls._get_key(key)
1785 try:
1781 try:
1786 return super(cachemapdict, self).__getitem__(key)
1782 return super(cachemapdict, self).__getitem__(key)
1787 except KeyError:
1783 except KeyError:
1788 return
1784 return
1789
1785
1790 cache_map = cachemapdict(fixkey=True)
1786 cache_map = cachemapdict(fixkey=True)
1791 for obj in cls.query().all():
1787 for obj in cls.query().all():
1792 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1788 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1793 return cache_map
1789 return cache_map
1794
1790
1795
1791
1796 class ChangesetComment(Base, BaseModel):
1792 class ChangesetComment(Base, BaseModel):
1797 __tablename__ = 'changeset_comments'
1793 __tablename__ = 'changeset_comments'
1798 __table_args__ = (
1794 __table_args__ = (
1799 Index('cc_revision_idx', 'revision'),
1795 Index('cc_revision_idx', 'revision'),
1800 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1796 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1801 'mysql_charset': 'utf8'},
1797 'mysql_charset': 'utf8'},
1802 )
1798 )
1803 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1799 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1804 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1800 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1805 revision = Column('revision', String(40), nullable=True)
1801 revision = Column('revision', String(40), nullable=True)
1806 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1802 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1807 line_no = Column('line_no', Unicode(10), nullable=True)
1803 line_no = Column('line_no', Unicode(10), nullable=True)
1808 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1804 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1809 f_path = Column('f_path', Unicode(1000), nullable=True)
1805 f_path = Column('f_path', Unicode(1000), nullable=True)
1810 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1806 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1811 text = Column('text', UnicodeText(25000), nullable=False)
1807 text = Column('text', UnicodeText(25000), nullable=False)
1812 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1808 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1813 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1809 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1814
1810
1815 author = relationship('User', lazy='joined')
1811 author = relationship('User', lazy='joined')
1816 repo = relationship('Repository')
1812 repo = relationship('Repository')
1817 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1813 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1818 pull_request = relationship('PullRequest', lazy='joined')
1814 pull_request = relationship('PullRequest', lazy='joined')
1819
1815
1820 @classmethod
1816 @classmethod
1821 def get_users(cls, revision=None, pull_request_id=None):
1817 def get_users(cls, revision=None, pull_request_id=None):
1822 """
1818 """
1823 Returns user associated with this ChangesetComment. ie those
1819 Returns user associated with this ChangesetComment. ie those
1824 who actually commented
1820 who actually commented
1825
1821
1826 :param cls:
1822 :param cls:
1827 :param revision:
1823 :param revision:
1828 """
1824 """
1829 q = Session().query(User)\
1825 q = Session().query(User)\
1830 .join(ChangesetComment.author)
1826 .join(ChangesetComment.author)
1831 if revision:
1827 if revision:
1832 q = q.filter(cls.revision == revision)
1828 q = q.filter(cls.revision == revision)
1833 elif pull_request_id:
1829 elif pull_request_id:
1834 q = q.filter(cls.pull_request_id == pull_request_id)
1830 q = q.filter(cls.pull_request_id == pull_request_id)
1835 return q.all()
1831 return q.all()
1836
1832
1837
1833
1838 class ChangesetStatus(Base, BaseModel):
1834 class ChangesetStatus(Base, BaseModel):
1839 __tablename__ = 'changeset_statuses'
1835 __tablename__ = 'changeset_statuses'
1840 __table_args__ = (
1836 __table_args__ = (
1841 Index('cs_revision_idx', 'revision'),
1837 Index('cs_revision_idx', 'revision'),
1842 Index('cs_version_idx', 'version'),
1838 Index('cs_version_idx', 'version'),
1843 UniqueConstraint('repo_id', 'revision', 'version'),
1839 UniqueConstraint('repo_id', 'revision', 'version'),
1844 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1840 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1845 'mysql_charset': 'utf8'}
1841 'mysql_charset': 'utf8'}
1846 )
1842 )
1847 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1843 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1848 STATUS_APPROVED = 'approved'
1844 STATUS_APPROVED = 'approved'
1849 STATUS_REJECTED = 'rejected'
1845 STATUS_REJECTED = 'rejected'
1850 STATUS_UNDER_REVIEW = 'under_review'
1846 STATUS_UNDER_REVIEW = 'under_review'
1851
1847
1852 STATUSES = [
1848 STATUSES = [
1853 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1849 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1854 (STATUS_APPROVED, _("Approved")),
1850 (STATUS_APPROVED, _("Approved")),
1855 (STATUS_REJECTED, _("Rejected")),
1851 (STATUS_REJECTED, _("Rejected")),
1856 (STATUS_UNDER_REVIEW, _("Under Review")),
1852 (STATUS_UNDER_REVIEW, _("Under Review")),
1857 ]
1853 ]
1858
1854
1859 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1855 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1860 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1856 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1861 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1862 revision = Column('revision', String(40), nullable=False)
1858 revision = Column('revision', String(40), nullable=False)
1863 status = Column('status', String(128), nullable=False, default=DEFAULT)
1859 status = Column('status', String(128), nullable=False, default=DEFAULT)
1864 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1860 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1865 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1861 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1866 version = Column('version', Integer(), nullable=False, default=0)
1862 version = Column('version', Integer(), nullable=False, default=0)
1867 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1863 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1868
1864
1869 author = relationship('User', lazy='joined')
1865 author = relationship('User', lazy='joined')
1870 repo = relationship('Repository')
1866 repo = relationship('Repository')
1871 comment = relationship('ChangesetComment', lazy='joined')
1867 comment = relationship('ChangesetComment', lazy='joined')
1872 pull_request = relationship('PullRequest', lazy='joined')
1868 pull_request = relationship('PullRequest', lazy='joined')
1873
1869
1874 def __unicode__(self):
1870 def __unicode__(self):
1875 return u"<%s('%s:%s')>" % (
1871 return u"<%s('%s:%s')>" % (
1876 self.__class__.__name__,
1872 self.__class__.__name__,
1877 self.status, self.author
1873 self.status, self.author
1878 )
1874 )
1879
1875
1880 @classmethod
1876 @classmethod
1881 def get_status_lbl(cls, value):
1877 def get_status_lbl(cls, value):
1882 return dict(cls.STATUSES).get(value)
1878 return dict(cls.STATUSES).get(value)
1883
1879
1884 @property
1880 @property
1885 def status_lbl(self):
1881 def status_lbl(self):
1886 return ChangesetStatus.get_status_lbl(self.status)
1882 return ChangesetStatus.get_status_lbl(self.status)
1887
1883
1888
1884
1889 class PullRequest(Base, BaseModel):
1885 class PullRequest(Base, BaseModel):
1890 __tablename__ = 'pull_requests'
1886 __tablename__ = 'pull_requests'
1891 __table_args__ = (
1887 __table_args__ = (
1892 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1888 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1893 'mysql_charset': 'utf8'},
1889 'mysql_charset': 'utf8'},
1894 )
1890 )
1895
1891
1896 STATUS_NEW = u'new'
1892 STATUS_NEW = u'new'
1897 STATUS_OPEN = u'open'
1893 STATUS_OPEN = u'open'
1898 STATUS_CLOSED = u'closed'
1894 STATUS_CLOSED = u'closed'
1899
1895
1900 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1896 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1901 title = Column('title', Unicode(256), nullable=True)
1897 title = Column('title', Unicode(256), nullable=True)
1902 description = Column('description', UnicodeText(10240), nullable=True)
1898 description = Column('description', UnicodeText(10240), nullable=True)
1903 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1899 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1904 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1905 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1901 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1906 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1902 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1907 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1903 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1908 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1904 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1909 org_ref = Column('org_ref', Unicode(256), nullable=False)
1905 org_ref = Column('org_ref', Unicode(256), nullable=False)
1910 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1906 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1911 other_ref = Column('other_ref', Unicode(256), nullable=False)
1907 other_ref = Column('other_ref', Unicode(256), nullable=False)
1912
1908
1913 @hybrid_property
1909 @hybrid_property
1914 def revisions(self):
1910 def revisions(self):
1915 return self._revisions.split(':')
1911 return self._revisions.split(':')
1916
1912
1917 @revisions.setter
1913 @revisions.setter
1918 def revisions(self, val):
1914 def revisions(self, val):
1919 self._revisions = ':'.join(val)
1915 self._revisions = ':'.join(val)
1920
1916
1921 @property
1917 @property
1922 def org_ref_parts(self):
1918 def org_ref_parts(self):
1923 return self.org_ref.split(':')
1919 return self.org_ref.split(':')
1924
1920
1925 @property
1921 @property
1926 def other_ref_parts(self):
1922 def other_ref_parts(self):
1927 return self.other_ref.split(':')
1923 return self.other_ref.split(':')
1928
1924
1929 author = relationship('User', lazy='joined')
1925 author = relationship('User', lazy='joined')
1930 reviewers = relationship('PullRequestReviewers',
1926 reviewers = relationship('PullRequestReviewers',
1931 cascade="all, delete, delete-orphan")
1927 cascade="all, delete, delete-orphan")
1932 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1928 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1933 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1929 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1934 statuses = relationship('ChangesetStatus')
1930 statuses = relationship('ChangesetStatus')
1935 comments = relationship('ChangesetComment',
1931 comments = relationship('ChangesetComment',
1936 cascade="all, delete, delete-orphan")
1932 cascade="all, delete, delete-orphan")
1937
1933
1938 def is_closed(self):
1934 def is_closed(self):
1939 return self.status == self.STATUS_CLOSED
1935 return self.status == self.STATUS_CLOSED
1940
1936
1941 @property
1937 @property
1942 def last_review_status(self):
1938 def last_review_status(self):
1943 return self.statuses[-1].status if self.statuses else ''
1939 return self.statuses[-1].status if self.statuses else ''
1944
1940
1945 def __json__(self):
1941 def __json__(self):
1946 return dict(
1942 return dict(
1947 revisions=self.revisions
1943 revisions=self.revisions
1948 )
1944 )
1949
1945
1950
1946
1951 class PullRequestReviewers(Base, BaseModel):
1947 class PullRequestReviewers(Base, BaseModel):
1952 __tablename__ = 'pull_request_reviewers'
1948 __tablename__ = 'pull_request_reviewers'
1953 __table_args__ = (
1949 __table_args__ = (
1954 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1950 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1955 'mysql_charset': 'utf8'},
1951 'mysql_charset': 'utf8'},
1956 )
1952 )
1957
1953
1958 def __init__(self, user=None, pull_request=None):
1954 def __init__(self, user=None, pull_request=None):
1959 self.user = user
1955 self.user = user
1960 self.pull_request = pull_request
1956 self.pull_request = pull_request
1961
1957
1962 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1958 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1963 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1959 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1964 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1965
1961
1966 user = relationship('User')
1962 user = relationship('User')
1967 pull_request = relationship('PullRequest')
1963 pull_request = relationship('PullRequest')
1968
1964
1969
1965
1970 class Notification(Base, BaseModel):
1966 class Notification(Base, BaseModel):
1971 __tablename__ = 'notifications'
1967 __tablename__ = 'notifications'
1972 __table_args__ = (
1968 __table_args__ = (
1973 Index('notification_type_idx', 'type'),
1969 Index('notification_type_idx', 'type'),
1974 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1970 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1975 'mysql_charset': 'utf8'},
1971 'mysql_charset': 'utf8'},
1976 )
1972 )
1977
1973
1978 TYPE_CHANGESET_COMMENT = u'cs_comment'
1974 TYPE_CHANGESET_COMMENT = u'cs_comment'
1979 TYPE_MESSAGE = u'message'
1975 TYPE_MESSAGE = u'message'
1980 TYPE_MENTION = u'mention'
1976 TYPE_MENTION = u'mention'
1981 TYPE_REGISTRATION = u'registration'
1977 TYPE_REGISTRATION = u'registration'
1982 TYPE_PULL_REQUEST = u'pull_request'
1978 TYPE_PULL_REQUEST = u'pull_request'
1983 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1979 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1984
1980
1985 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1981 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1986 subject = Column('subject', Unicode(512), nullable=True)
1982 subject = Column('subject', Unicode(512), nullable=True)
1987 body = Column('body', UnicodeText(50000), nullable=True)
1983 body = Column('body', UnicodeText(50000), nullable=True)
1988 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1984 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1989 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1985 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1990 type_ = Column('type', Unicode(256))
1986 type_ = Column('type', Unicode(256))
1991
1987
1992 created_by_user = relationship('User')
1988 created_by_user = relationship('User')
1993 notifications_to_users = relationship('UserNotification', lazy='joined',
1989 notifications_to_users = relationship('UserNotification', lazy='joined',
1994 cascade="all, delete, delete-orphan")
1990 cascade="all, delete, delete-orphan")
1995
1991
1996 @property
1992 @property
1997 def recipients(self):
1993 def recipients(self):
1998 return [x.user for x in UserNotification.query()\
1994 return [x.user for x in UserNotification.query()\
1999 .filter(UserNotification.notification == self)\
1995 .filter(UserNotification.notification == self)\
2000 .order_by(UserNotification.user_id.asc()).all()]
1996 .order_by(UserNotification.user_id.asc()).all()]
2001
1997
2002 @classmethod
1998 @classmethod
2003 def create(cls, created_by, subject, body, recipients, type_=None):
1999 def create(cls, created_by, subject, body, recipients, type_=None):
2004 if type_ is None:
2000 if type_ is None:
2005 type_ = Notification.TYPE_MESSAGE
2001 type_ = Notification.TYPE_MESSAGE
2006
2002
2007 notification = cls()
2003 notification = cls()
2008 notification.created_by_user = created_by
2004 notification.created_by_user = created_by
2009 notification.subject = subject
2005 notification.subject = subject
2010 notification.body = body
2006 notification.body = body
2011 notification.type_ = type_
2007 notification.type_ = type_
2012 notification.created_on = datetime.datetime.now()
2008 notification.created_on = datetime.datetime.now()
2013
2009
2014 for u in recipients:
2010 for u in recipients:
2015 assoc = UserNotification()
2011 assoc = UserNotification()
2016 assoc.notification = notification
2012 assoc.notification = notification
2017 u.notifications.append(assoc)
2013 u.notifications.append(assoc)
2018 Session().add(notification)
2014 Session().add(notification)
2019 return notification
2015 return notification
2020
2016
2021 @property
2017 @property
2022 def description(self):
2018 def description(self):
2023 from rhodecode.model.notification import NotificationModel
2019 from rhodecode.model.notification import NotificationModel
2024 return NotificationModel().make_description(self)
2020 return NotificationModel().make_description(self)
2025
2021
2026
2022
2027 class UserNotification(Base, BaseModel):
2023 class UserNotification(Base, BaseModel):
2028 __tablename__ = 'user_to_notification'
2024 __tablename__ = 'user_to_notification'
2029 __table_args__ = (
2025 __table_args__ = (
2030 UniqueConstraint('user_id', 'notification_id'),
2026 UniqueConstraint('user_id', 'notification_id'),
2031 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2027 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2032 'mysql_charset': 'utf8'}
2028 'mysql_charset': 'utf8'}
2033 )
2029 )
2034 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2030 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2035 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2031 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2036 read = Column('read', Boolean, default=False)
2032 read = Column('read', Boolean, default=False)
2037 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2033 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2038
2034
2039 user = relationship('User', lazy="joined")
2035 user = relationship('User', lazy="joined")
2040 notification = relationship('Notification', lazy="joined",
2036 notification = relationship('Notification', lazy="joined",
2041 order_by=lambda: Notification.created_on.desc(),)
2037 order_by=lambda: Notification.created_on.desc(),)
2042
2038
2043 def mark_as_read(self):
2039 def mark_as_read(self):
2044 self.read = True
2040 self.read = True
2045 Session().add(self)
2041 Session().add(self)
2046
2042
2047
2043
2048 class DbMigrateVersion(Base, BaseModel):
2044 class DbMigrateVersion(Base, BaseModel):
2049 __tablename__ = 'db_migrate_version'
2045 __tablename__ = 'db_migrate_version'
2050 __table_args__ = (
2046 __table_args__ = (
2051 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2047 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2052 'mysql_charset': 'utf8'},
2048 'mysql_charset': 'utf8'},
2053 )
2049 )
2054 repository_id = Column('repository_id', String(250), primary_key=True)
2050 repository_id = Column('repository_id', String(250), primary_key=True)
2055 repository_path = Column('repository_path', Text)
2051 repository_path = Column('repository_path', Text)
2056 version = Column('version', Integer)
2052 version = Column('version', Integer)
@@ -1,672 +1,673 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.scm
3 rhodecode.model.scm
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Scm model for RhodeCode
6 Scm model for RhodeCode
7
7
8 :created_on: Apr 9, 2010
8 :created_on: Apr 9, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 from __future__ import with_statement
25 from __future__ import with_statement
26 import os
26 import os
27 import re
27 import re
28 import time
28 import time
29 import traceback
29 import traceback
30 import logging
30 import logging
31 import cStringIO
31 import cStringIO
32 import pkg_resources
32 import pkg_resources
33 from os.path import dirname as dn, join as jn
33 from os.path import dirname as dn, join as jn
34
34
35 from sqlalchemy import func
35 from sqlalchemy import func
36 from pylons.i18n.translation import _
36 from pylons.i18n.translation import _
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.lib.vcs import get_backend
39 from rhodecode.lib.vcs import get_backend
40 from rhodecode.lib.vcs.exceptions import RepositoryError
40 from rhodecode.lib.vcs.exceptions import RepositoryError
41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 from rhodecode.lib.vcs.nodes import FileNode
42 from rhodecode.lib.vcs.nodes import FileNode
43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44
44
45 from rhodecode import BACKENDS
45 from rhodecode import BACKENDS
46 from rhodecode.lib import helpers as h
46 from rhodecode.lib import helpers as h
47 from rhodecode.lib.utils2 import safe_str, safe_unicode, get_server_url
47 from rhodecode.lib.utils2 import safe_str, safe_unicode, get_server_url,\
48 _set_extras
48 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
49 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
49 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
50 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
50 action_logger, REMOVED_REPO_PAT
51 action_logger, REMOVED_REPO_PAT
51 from rhodecode.model import BaseModel
52 from rhodecode.model import BaseModel
52 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
53 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
53 UserFollowing, UserLog, User, RepoGroup, PullRequest
54 UserFollowing, UserLog, User, RepoGroup, PullRequest
54 from rhodecode.lib.hooks import log_push_action
55 from rhodecode.lib.hooks import log_push_action
55
56
56 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
58
59
59 class UserTemp(object):
60 class UserTemp(object):
60 def __init__(self, user_id):
61 def __init__(self, user_id):
61 self.user_id = user_id
62 self.user_id = user_id
62
63
63 def __repr__(self):
64 def __repr__(self):
64 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
65 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
65
66
66
67
67 class RepoTemp(object):
68 class RepoTemp(object):
68 def __init__(self, repo_id):
69 def __init__(self, repo_id):
69 self.repo_id = repo_id
70 self.repo_id = repo_id
70
71
71 def __repr__(self):
72 def __repr__(self):
72 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
73 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
73
74
74
75
75 class CachedRepoList(object):
76 class CachedRepoList(object):
76 """
77 """
77 Cached repo list, uses in-memory cache after initialization, that is
78 Cached repo list, uses in-memory cache after initialization, that is
78 super fast
79 super fast
79 """
80 """
80
81
81 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
82 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
82 self.db_repo_list = db_repo_list
83 self.db_repo_list = db_repo_list
83 self.repos_path = repos_path
84 self.repos_path = repos_path
84 self.order_by = order_by
85 self.order_by = order_by
85 self.reversed = (order_by or '').startswith('-')
86 self.reversed = (order_by or '').startswith('-')
86 if not perm_set:
87 if not perm_set:
87 perm_set = ['repository.read', 'repository.write',
88 perm_set = ['repository.read', 'repository.write',
88 'repository.admin']
89 'repository.admin']
89 self.perm_set = perm_set
90 self.perm_set = perm_set
90
91
91 def __len__(self):
92 def __len__(self):
92 return len(self.db_repo_list)
93 return len(self.db_repo_list)
93
94
94 def __repr__(self):
95 def __repr__(self):
95 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
96 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
96
97
97 def __iter__(self):
98 def __iter__(self):
98 # pre-propagated cache_map to save executing select statements
99 # pre-propagated cache_map to save executing select statements
99 # for each repo
100 # for each repo
100 cache_map = CacheInvalidation.get_cache_map()
101 cache_map = CacheInvalidation.get_cache_map()
101
102
102 for dbr in self.db_repo_list:
103 for dbr in self.db_repo_list:
103 scmr = dbr.scm_instance_cached(cache_map)
104 scmr = dbr.scm_instance_cached(cache_map)
104 # check permission at this level
105 # check permission at this level
105 if not HasRepoPermissionAny(
106 if not HasRepoPermissionAny(
106 *self.perm_set
107 *self.perm_set
107 )(dbr.repo_name, 'get repo check'):
108 )(dbr.repo_name, 'get repo check'):
108 continue
109 continue
109
110
110 try:
111 try:
111 last_change = scmr.last_change
112 last_change = scmr.last_change
112 tip = h.get_changeset_safe(scmr, 'tip')
113 tip = h.get_changeset_safe(scmr, 'tip')
113 except Exception:
114 except Exception:
114 log.error(
115 log.error(
115 '%s this repository is present in database but it '
116 '%s this repository is present in database but it '
116 'cannot be created as an scm instance, org_exc:%s'
117 'cannot be created as an scm instance, org_exc:%s'
117 % (dbr.repo_name, traceback.format_exc())
118 % (dbr.repo_name, traceback.format_exc())
118 )
119 )
119 continue
120 continue
120
121
121 tmp_d = {}
122 tmp_d = {}
122 tmp_d['name'] = dbr.repo_name
123 tmp_d['name'] = dbr.repo_name
123 tmp_d['name_sort'] = tmp_d['name'].lower()
124 tmp_d['name_sort'] = tmp_d['name'].lower()
124 tmp_d['raw_name'] = tmp_d['name'].lower()
125 tmp_d['raw_name'] = tmp_d['name'].lower()
125 tmp_d['description'] = dbr.description
126 tmp_d['description'] = dbr.description
126 tmp_d['description_sort'] = tmp_d['description'].lower()
127 tmp_d['description_sort'] = tmp_d['description'].lower()
127 tmp_d['last_change'] = last_change
128 tmp_d['last_change'] = last_change
128 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
129 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
129 tmp_d['tip'] = tip.raw_id
130 tmp_d['tip'] = tip.raw_id
130 tmp_d['tip_sort'] = tip.revision
131 tmp_d['tip_sort'] = tip.revision
131 tmp_d['rev'] = tip.revision
132 tmp_d['rev'] = tip.revision
132 tmp_d['contact'] = dbr.user.full_contact
133 tmp_d['contact'] = dbr.user.full_contact
133 tmp_d['contact_sort'] = tmp_d['contact']
134 tmp_d['contact_sort'] = tmp_d['contact']
134 tmp_d['owner_sort'] = tmp_d['contact']
135 tmp_d['owner_sort'] = tmp_d['contact']
135 tmp_d['repo_archives'] = list(scmr._get_archives())
136 tmp_d['repo_archives'] = list(scmr._get_archives())
136 tmp_d['last_msg'] = tip.message
137 tmp_d['last_msg'] = tip.message
137 tmp_d['author'] = tip.author
138 tmp_d['author'] = tip.author
138 tmp_d['dbrepo'] = dbr.get_dict()
139 tmp_d['dbrepo'] = dbr.get_dict()
139 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
140 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
140 yield tmp_d
141 yield tmp_d
141
142
142
143
143 class SimpleCachedRepoList(CachedRepoList):
144 class SimpleCachedRepoList(CachedRepoList):
144 """
145 """
145 Lighter version of CachedRepoList without the scm initialisation
146 Lighter version of CachedRepoList without the scm initialisation
146 """
147 """
147
148
148 def __iter__(self):
149 def __iter__(self):
149 for dbr in self.db_repo_list:
150 for dbr in self.db_repo_list:
150 # check permission at this level
151 # check permission at this level
151 if not HasRepoPermissionAny(
152 if not HasRepoPermissionAny(
152 *self.perm_set
153 *self.perm_set
153 )(dbr.repo_name, 'get repo check'):
154 )(dbr.repo_name, 'get repo check'):
154 continue
155 continue
155
156
156 tmp_d = {}
157 tmp_d = {}
157 tmp_d['name'] = dbr.repo_name
158 tmp_d['name'] = dbr.repo_name
158 tmp_d['name_sort'] = tmp_d['name'].lower()
159 tmp_d['name_sort'] = tmp_d['name'].lower()
159 tmp_d['raw_name'] = tmp_d['name'].lower()
160 tmp_d['raw_name'] = tmp_d['name'].lower()
160 tmp_d['description'] = dbr.description
161 tmp_d['description'] = dbr.description
161 tmp_d['description_sort'] = tmp_d['description'].lower()
162 tmp_d['description_sort'] = tmp_d['description'].lower()
162 tmp_d['dbrepo'] = dbr.get_dict()
163 tmp_d['dbrepo'] = dbr.get_dict()
163 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
164 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
164 yield tmp_d
165 yield tmp_d
165
166
166
167
167 class GroupList(object):
168 class GroupList(object):
168
169
169 def __init__(self, db_repo_group_list, perm_set=None):
170 def __init__(self, db_repo_group_list, perm_set=None):
170 """
171 """
171 Creates iterator from given list of group objects, additionally
172 Creates iterator from given list of group objects, additionally
172 checking permission for them from perm_set var
173 checking permission for them from perm_set var
173
174
174 :param db_repo_group_list:
175 :param db_repo_group_list:
175 :param perm_set: list of permissons to check
176 :param perm_set: list of permissons to check
176 """
177 """
177 self.db_repo_group_list = db_repo_group_list
178 self.db_repo_group_list = db_repo_group_list
178 if not perm_set:
179 if not perm_set:
179 perm_set = ['group.read', 'group.write', 'group.admin']
180 perm_set = ['group.read', 'group.write', 'group.admin']
180 self.perm_set = perm_set
181 self.perm_set = perm_set
181
182
182 def __len__(self):
183 def __len__(self):
183 return len(self.db_repo_group_list)
184 return len(self.db_repo_group_list)
184
185
185 def __repr__(self):
186 def __repr__(self):
186 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
187 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
187
188
188 def __iter__(self):
189 def __iter__(self):
189 for dbgr in self.db_repo_group_list:
190 for dbgr in self.db_repo_group_list:
190 # check permission at this level
191 # check permission at this level
191 if not HasReposGroupPermissionAny(
192 if not HasReposGroupPermissionAny(
192 *self.perm_set
193 *self.perm_set
193 )(dbgr.group_name, 'get group repo check'):
194 )(dbgr.group_name, 'get group repo check'):
194 continue
195 continue
195
196
196 yield dbgr
197 yield dbgr
197
198
198
199
199 class ScmModel(BaseModel):
200 class ScmModel(BaseModel):
200 """
201 """
201 Generic Scm Model
202 Generic Scm Model
202 """
203 """
203
204
204 def __get_repo(self, instance):
205 def __get_repo(self, instance):
205 cls = Repository
206 cls = Repository
206 if isinstance(instance, cls):
207 if isinstance(instance, cls):
207 return instance
208 return instance
208 elif isinstance(instance, int) or safe_str(instance).isdigit():
209 elif isinstance(instance, int) or safe_str(instance).isdigit():
209 return cls.get(instance)
210 return cls.get(instance)
210 elif isinstance(instance, basestring):
211 elif isinstance(instance, basestring):
211 return cls.get_by_repo_name(instance)
212 return cls.get_by_repo_name(instance)
212 elif instance:
213 elif instance:
213 raise Exception('given object must be int, basestr or Instance'
214 raise Exception('given object must be int, basestr or Instance'
214 ' of %s got %s' % (type(cls), type(instance)))
215 ' of %s got %s' % (type(cls), type(instance)))
215
216
216 @LazyProperty
217 @LazyProperty
217 def repos_path(self):
218 def repos_path(self):
218 """
219 """
219 Get's the repositories root path from database
220 Get's the repositories root path from database
220 """
221 """
221
222
222 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
223 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
223
224
224 return q.ui_value
225 return q.ui_value
225
226
226 def repo_scan(self, repos_path=None):
227 def repo_scan(self, repos_path=None):
227 """
228 """
228 Listing of repositories in given path. This path should not be a
229 Listing of repositories in given path. This path should not be a
229 repository itself. Return a dictionary of repository objects
230 repository itself. Return a dictionary of repository objects
230
231
231 :param repos_path: path to directory containing repositories
232 :param repos_path: path to directory containing repositories
232 """
233 """
233
234
234 if repos_path is None:
235 if repos_path is None:
235 repos_path = self.repos_path
236 repos_path = self.repos_path
236
237
237 log.info('scanning for repositories in %s' % repos_path)
238 log.info('scanning for repositories in %s' % repos_path)
238
239
239 baseui = make_ui('db')
240 baseui = make_ui('db')
240 repos = {}
241 repos = {}
241
242
242 for name, path in get_filesystem_repos(repos_path, recursive=True):
243 for name, path in get_filesystem_repos(repos_path, recursive=True):
243 # name need to be decomposed and put back together using the /
244 # name need to be decomposed and put back together using the /
244 # since this is internal storage separator for rhodecode
245 # since this is internal storage separator for rhodecode
245 name = Repository.normalize_repo_name(name)
246 name = Repository.normalize_repo_name(name)
246
247
247 try:
248 try:
248 if name in repos:
249 if name in repos:
249 raise RepositoryError('Duplicate repository name %s '
250 raise RepositoryError('Duplicate repository name %s '
250 'found in %s' % (name, path))
251 'found in %s' % (name, path))
251 else:
252 else:
252
253
253 klass = get_backend(path[0])
254 klass = get_backend(path[0])
254
255
255 if path[0] == 'hg' and path[0] in BACKENDS.keys():
256 if path[0] == 'hg' and path[0] in BACKENDS.keys():
256 repos[name] = klass(safe_str(path[1]), baseui=baseui)
257 repos[name] = klass(safe_str(path[1]), baseui=baseui)
257
258
258 if path[0] == 'git' and path[0] in BACKENDS.keys():
259 if path[0] == 'git' and path[0] in BACKENDS.keys():
259 repos[name] = klass(path[1])
260 repos[name] = klass(path[1])
260 except OSError:
261 except OSError:
261 continue
262 continue
262 log.debug('found %s paths with repositories' % (len(repos)))
263 log.debug('found %s paths with repositories' % (len(repos)))
263 return repos
264 return repos
264
265
265 def get_repos(self, all_repos=None, sort_key=None, simple=False):
266 def get_repos(self, all_repos=None, sort_key=None, simple=False):
266 """
267 """
267 Get all repos from db and for each repo create it's
268 Get all repos from db and for each repo create it's
268 backend instance and fill that backed with information from database
269 backend instance and fill that backed with information from database
269
270
270 :param all_repos: list of repository names as strings
271 :param all_repos: list of repository names as strings
271 give specific repositories list, good for filtering
272 give specific repositories list, good for filtering
272
273
273 :param sort_key: initial sorting of repos
274 :param sort_key: initial sorting of repos
274 :param simple: use SimpleCachedList - one without the SCM info
275 :param simple: use SimpleCachedList - one without the SCM info
275 """
276 """
276 if all_repos is None:
277 if all_repos is None:
277 all_repos = self.sa.query(Repository)\
278 all_repos = self.sa.query(Repository)\
278 .filter(Repository.group_id == None)\
279 .filter(Repository.group_id == None)\
279 .order_by(func.lower(Repository.repo_name)).all()
280 .order_by(func.lower(Repository.repo_name)).all()
280 if simple:
281 if simple:
281 repo_iter = SimpleCachedRepoList(all_repos,
282 repo_iter = SimpleCachedRepoList(all_repos,
282 repos_path=self.repos_path,
283 repos_path=self.repos_path,
283 order_by=sort_key)
284 order_by=sort_key)
284 else:
285 else:
285 repo_iter = CachedRepoList(all_repos,
286 repo_iter = CachedRepoList(all_repos,
286 repos_path=self.repos_path,
287 repos_path=self.repos_path,
287 order_by=sort_key)
288 order_by=sort_key)
288
289
289 return repo_iter
290 return repo_iter
290
291
291 def get_repos_groups(self, all_groups=None):
292 def get_repos_groups(self, all_groups=None):
292 if all_groups is None:
293 if all_groups is None:
293 all_groups = RepoGroup.query()\
294 all_groups = RepoGroup.query()\
294 .filter(RepoGroup.group_parent_id == None).all()
295 .filter(RepoGroup.group_parent_id == None).all()
295 return [x for x in GroupList(all_groups)]
296 return [x for x in GroupList(all_groups)]
296
297
297 def mark_for_invalidation(self, repo_name):
298 def mark_for_invalidation(self, repo_name):
298 """
299 """
299 Puts cache invalidation task into db for
300 Puts cache invalidation task into db for
300 further global cache invalidation
301 further global cache invalidation
301
302
302 :param repo_name: this repo that should invalidation take place
303 :param repo_name: this repo that should invalidation take place
303 """
304 """
304 invalidated_keys = CacheInvalidation.set_invalidate(repo_name=repo_name)
305 invalidated_keys = CacheInvalidation.set_invalidate(repo_name=repo_name)
305 repo = Repository.get_by_repo_name(repo_name)
306 repo = Repository.get_by_repo_name(repo_name)
306 if repo:
307 if repo:
307 repo.update_changeset_cache()
308 repo.update_changeset_cache()
308 return invalidated_keys
309 return invalidated_keys
309
310
310 def toggle_following_repo(self, follow_repo_id, user_id):
311 def toggle_following_repo(self, follow_repo_id, user_id):
311
312
312 f = self.sa.query(UserFollowing)\
313 f = self.sa.query(UserFollowing)\
313 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
314 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
314 .filter(UserFollowing.user_id == user_id).scalar()
315 .filter(UserFollowing.user_id == user_id).scalar()
315
316
316 if f is not None:
317 if f is not None:
317 try:
318 try:
318 self.sa.delete(f)
319 self.sa.delete(f)
319 action_logger(UserTemp(user_id),
320 action_logger(UserTemp(user_id),
320 'stopped_following_repo',
321 'stopped_following_repo',
321 RepoTemp(follow_repo_id))
322 RepoTemp(follow_repo_id))
322 return
323 return
323 except:
324 except:
324 log.error(traceback.format_exc())
325 log.error(traceback.format_exc())
325 raise
326 raise
326
327
327 try:
328 try:
328 f = UserFollowing()
329 f = UserFollowing()
329 f.user_id = user_id
330 f.user_id = user_id
330 f.follows_repo_id = follow_repo_id
331 f.follows_repo_id = follow_repo_id
331 self.sa.add(f)
332 self.sa.add(f)
332
333
333 action_logger(UserTemp(user_id),
334 action_logger(UserTemp(user_id),
334 'started_following_repo',
335 'started_following_repo',
335 RepoTemp(follow_repo_id))
336 RepoTemp(follow_repo_id))
336 except:
337 except:
337 log.error(traceback.format_exc())
338 log.error(traceback.format_exc())
338 raise
339 raise
339
340
340 def toggle_following_user(self, follow_user_id, user_id):
341 def toggle_following_user(self, follow_user_id, user_id):
341 f = self.sa.query(UserFollowing)\
342 f = self.sa.query(UserFollowing)\
342 .filter(UserFollowing.follows_user_id == follow_user_id)\
343 .filter(UserFollowing.follows_user_id == follow_user_id)\
343 .filter(UserFollowing.user_id == user_id).scalar()
344 .filter(UserFollowing.user_id == user_id).scalar()
344
345
345 if f is not None:
346 if f is not None:
346 try:
347 try:
347 self.sa.delete(f)
348 self.sa.delete(f)
348 return
349 return
349 except:
350 except:
350 log.error(traceback.format_exc())
351 log.error(traceback.format_exc())
351 raise
352 raise
352
353
353 try:
354 try:
354 f = UserFollowing()
355 f = UserFollowing()
355 f.user_id = user_id
356 f.user_id = user_id
356 f.follows_user_id = follow_user_id
357 f.follows_user_id = follow_user_id
357 self.sa.add(f)
358 self.sa.add(f)
358 except:
359 except:
359 log.error(traceback.format_exc())
360 log.error(traceback.format_exc())
360 raise
361 raise
361
362
362 def is_following_repo(self, repo_name, user_id, cache=False):
363 def is_following_repo(self, repo_name, user_id, cache=False):
363 r = self.sa.query(Repository)\
364 r = self.sa.query(Repository)\
364 .filter(Repository.repo_name == repo_name).scalar()
365 .filter(Repository.repo_name == repo_name).scalar()
365
366
366 f = self.sa.query(UserFollowing)\
367 f = self.sa.query(UserFollowing)\
367 .filter(UserFollowing.follows_repository == r)\
368 .filter(UserFollowing.follows_repository == r)\
368 .filter(UserFollowing.user_id == user_id).scalar()
369 .filter(UserFollowing.user_id == user_id).scalar()
369
370
370 return f is not None
371 return f is not None
371
372
372 def is_following_user(self, username, user_id, cache=False):
373 def is_following_user(self, username, user_id, cache=False):
373 u = User.get_by_username(username)
374 u = User.get_by_username(username)
374
375
375 f = self.sa.query(UserFollowing)\
376 f = self.sa.query(UserFollowing)\
376 .filter(UserFollowing.follows_user == u)\
377 .filter(UserFollowing.follows_user == u)\
377 .filter(UserFollowing.user_id == user_id).scalar()
378 .filter(UserFollowing.user_id == user_id).scalar()
378
379
379 return f is not None
380 return f is not None
380
381
381 def get_followers(self, repo):
382 def get_followers(self, repo):
382 repo = self._get_repo(repo)
383 repo = self._get_repo(repo)
383
384
384 return self.sa.query(UserFollowing)\
385 return self.sa.query(UserFollowing)\
385 .filter(UserFollowing.follows_repository == repo).count()
386 .filter(UserFollowing.follows_repository == repo).count()
386
387
387 def get_forks(self, repo):
388 def get_forks(self, repo):
388 repo = self._get_repo(repo)
389 repo = self._get_repo(repo)
389 return self.sa.query(Repository)\
390 return self.sa.query(Repository)\
390 .filter(Repository.fork == repo).count()
391 .filter(Repository.fork == repo).count()
391
392
392 def get_pull_requests(self, repo):
393 def get_pull_requests(self, repo):
393 repo = self._get_repo(repo)
394 repo = self._get_repo(repo)
394 return self.sa.query(PullRequest)\
395 return self.sa.query(PullRequest)\
395 .filter(PullRequest.other_repo == repo).count()
396 .filter(PullRequest.other_repo == repo).count()
396
397
397 def mark_as_fork(self, repo, fork, user):
398 def mark_as_fork(self, repo, fork, user):
398 repo = self.__get_repo(repo)
399 repo = self.__get_repo(repo)
399 fork = self.__get_repo(fork)
400 fork = self.__get_repo(fork)
400 if fork and repo.repo_id == fork.repo_id:
401 if fork and repo.repo_id == fork.repo_id:
401 raise Exception("Cannot set repository as fork of itself")
402 raise Exception("Cannot set repository as fork of itself")
402 repo.fork = fork
403 repo.fork = fork
403 self.sa.add(repo)
404 self.sa.add(repo)
404 return repo
405 return repo
405
406
406 def _handle_push(self, repo, username, action, repo_name, revisions):
407 def _handle_push(self, repo, username, action, repo_name, revisions):
407 """
408 """
408 Triggers push action hooks
409 Triggers push action hooks
409
410
410 :param repo: SCM repo
411 :param repo: SCM repo
411 :param username: username who pushes
412 :param username: username who pushes
412 :param action: push/push_loca/push_remote
413 :param action: push/push_loca/push_remote
413 :param repo_name: name of repo
414 :param repo_name: name of repo
414 :param revisions: list of revisions that we pushed
415 :param revisions: list of revisions that we pushed
415 """
416 """
416 from rhodecode import CONFIG
417 from rhodecode import CONFIG
417 from rhodecode.lib.base import _get_ip_addr
418 from rhodecode.lib.base import _get_ip_addr
418 try:
419 try:
419 from pylons import request
420 from pylons import request
420 environ = request.environ
421 environ = request.environ
421 except TypeError:
422 except TypeError:
422 # we might use this outside of request context, let's fake the
423 # we might use this outside of request context, let's fake the
423 # environ data
424 # environ data
424 from webob import Request
425 from webob import Request
425 environ = Request.blank('').environ
426 environ = Request.blank('').environ
426
427
427 #trigger push hook
428 #trigger push hook
428 extras = {
429 extras = {
429 'ip': _get_ip_addr(environ),
430 'ip': _get_ip_addr(environ),
430 'username': username,
431 'username': username,
431 'action': 'push_local',
432 'action': 'push_local',
432 'repository': repo_name,
433 'repository': repo_name,
433 'scm': repo.alias,
434 'scm': repo.alias,
434 'config': CONFIG['__file__'],
435 'config': CONFIG['__file__'],
435 'server_url': get_server_url(environ),
436 'server_url': get_server_url(environ),
436 'make_lock': None,
437 'make_lock': None,
437 'locked_by': [None, None]
438 'locked_by': [None, None]
438 }
439 }
439 _scm_repo = repo._repo
440 _scm_repo = repo._repo
440 repo.inject_ui(**extras)
441 _set_extras(extras)
441 if repo.alias == 'hg':
442 if repo.alias == 'hg':
442 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
443 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
443 elif repo.alias == 'git':
444 elif repo.alias == 'git':
444 log_push_action(_scm_repo.ui, _scm_repo, _git_revs=revisions)
445 log_push_action(_scm_repo.ui, _scm_repo, _git_revs=revisions)
445
446
446 def _get_IMC_module(self, scm_type):
447 def _get_IMC_module(self, scm_type):
447 """
448 """
448 Returns InMemoryCommit class based on scm_type
449 Returns InMemoryCommit class based on scm_type
449
450
450 :param scm_type:
451 :param scm_type:
451 """
452 """
452 if scm_type == 'hg':
453 if scm_type == 'hg':
453 from rhodecode.lib.vcs.backends.hg import \
454 from rhodecode.lib.vcs.backends.hg import \
454 MercurialInMemoryChangeset as IMC
455 MercurialInMemoryChangeset as IMC
455 elif scm_type == 'git':
456 elif scm_type == 'git':
456 from rhodecode.lib.vcs.backends.git import \
457 from rhodecode.lib.vcs.backends.git import \
457 GitInMemoryChangeset as IMC
458 GitInMemoryChangeset as IMC
458 return IMC
459 return IMC
459
460
460 def pull_changes(self, repo, username):
461 def pull_changes(self, repo, username):
461 dbrepo = self.__get_repo(repo)
462 dbrepo = self.__get_repo(repo)
462 clone_uri = dbrepo.clone_uri
463 clone_uri = dbrepo.clone_uri
463 if not clone_uri:
464 if not clone_uri:
464 raise Exception("This repository doesn't have a clone uri")
465 raise Exception("This repository doesn't have a clone uri")
465
466
466 repo = dbrepo.scm_instance
467 repo = dbrepo.scm_instance
467 repo_name = dbrepo.repo_name
468 repo_name = dbrepo.repo_name
468 try:
469 try:
469 if repo.alias == 'git':
470 if repo.alias == 'git':
470 repo.fetch(clone_uri)
471 repo.fetch(clone_uri)
471 else:
472 else:
472 repo.pull(clone_uri)
473 repo.pull(clone_uri)
473 self.mark_for_invalidation(repo_name)
474 self.mark_for_invalidation(repo_name)
474 except:
475 except:
475 log.error(traceback.format_exc())
476 log.error(traceback.format_exc())
476 raise
477 raise
477
478
478 def commit_change(self, repo, repo_name, cs, user, author, message,
479 def commit_change(self, repo, repo_name, cs, user, author, message,
479 content, f_path):
480 content, f_path):
480 """
481 """
481 Commits changes
482 Commits changes
482
483
483 :param repo: SCM instance
484 :param repo: SCM instance
484
485
485 """
486 """
486 user = self._get_user(user)
487 user = self._get_user(user)
487 IMC = self._get_IMC_module(repo.alias)
488 IMC = self._get_IMC_module(repo.alias)
488
489
489 # decoding here will force that we have proper encoded values
490 # decoding here will force that we have proper encoded values
490 # in any other case this will throw exceptions and deny commit
491 # in any other case this will throw exceptions and deny commit
491 content = safe_str(content)
492 content = safe_str(content)
492 path = safe_str(f_path)
493 path = safe_str(f_path)
493 # message and author needs to be unicode
494 # message and author needs to be unicode
494 # proper backend should then translate that into required type
495 # proper backend should then translate that into required type
495 message = safe_unicode(message)
496 message = safe_unicode(message)
496 author = safe_unicode(author)
497 author = safe_unicode(author)
497 m = IMC(repo)
498 m = IMC(repo)
498 m.change(FileNode(path, content))
499 m.change(FileNode(path, content))
499 tip = m.commit(message=message,
500 tip = m.commit(message=message,
500 author=author,
501 author=author,
501 parents=[cs], branch=cs.branch)
502 parents=[cs], branch=cs.branch)
502
503
503 self.mark_for_invalidation(repo_name)
504 self.mark_for_invalidation(repo_name)
504 self._handle_push(repo,
505 self._handle_push(repo,
505 username=user.username,
506 username=user.username,
506 action='push_local',
507 action='push_local',
507 repo_name=repo_name,
508 repo_name=repo_name,
508 revisions=[tip.raw_id])
509 revisions=[tip.raw_id])
509 return tip
510 return tip
510
511
511 def create_node(self, repo, repo_name, cs, user, author, message, content,
512 def create_node(self, repo, repo_name, cs, user, author, message, content,
512 f_path):
513 f_path):
513 user = self._get_user(user)
514 user = self._get_user(user)
514 IMC = self._get_IMC_module(repo.alias)
515 IMC = self._get_IMC_module(repo.alias)
515
516
516 # decoding here will force that we have proper encoded values
517 # decoding here will force that we have proper encoded values
517 # in any other case this will throw exceptions and deny commit
518 # in any other case this will throw exceptions and deny commit
518 if isinstance(content, (basestring,)):
519 if isinstance(content, (basestring,)):
519 content = safe_str(content)
520 content = safe_str(content)
520 elif isinstance(content, (file, cStringIO.OutputType,)):
521 elif isinstance(content, (file, cStringIO.OutputType,)):
521 content = content.read()
522 content = content.read()
522 else:
523 else:
523 raise Exception('Content is of unrecognized type %s' % (
524 raise Exception('Content is of unrecognized type %s' % (
524 type(content)
525 type(content)
525 ))
526 ))
526
527
527 message = safe_unicode(message)
528 message = safe_unicode(message)
528 author = safe_unicode(author)
529 author = safe_unicode(author)
529 path = safe_str(f_path)
530 path = safe_str(f_path)
530 m = IMC(repo)
531 m = IMC(repo)
531
532
532 if isinstance(cs, EmptyChangeset):
533 if isinstance(cs, EmptyChangeset):
533 # EmptyChangeset means we we're editing empty repository
534 # EmptyChangeset means we we're editing empty repository
534 parents = None
535 parents = None
535 else:
536 else:
536 parents = [cs]
537 parents = [cs]
537
538
538 m.add(FileNode(path, content=content))
539 m.add(FileNode(path, content=content))
539 tip = m.commit(message=message,
540 tip = m.commit(message=message,
540 author=author,
541 author=author,
541 parents=parents, branch=cs.branch)
542 parents=parents, branch=cs.branch)
542
543
543 self.mark_for_invalidation(repo_name)
544 self.mark_for_invalidation(repo_name)
544 self._handle_push(repo,
545 self._handle_push(repo,
545 username=user.username,
546 username=user.username,
546 action='push_local',
547 action='push_local',
547 repo_name=repo_name,
548 repo_name=repo_name,
548 revisions=[tip.raw_id])
549 revisions=[tip.raw_id])
549 return tip
550 return tip
550
551
551 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
552 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
552 """
553 """
553 recursive walk in root dir and return a set of all path in that dir
554 recursive walk in root dir and return a set of all path in that dir
554 based on repository walk function
555 based on repository walk function
555
556
556 :param repo_name: name of repository
557 :param repo_name: name of repository
557 :param revision: revision for which to list nodes
558 :param revision: revision for which to list nodes
558 :param root_path: root path to list
559 :param root_path: root path to list
559 :param flat: return as a list, if False returns a dict with decription
560 :param flat: return as a list, if False returns a dict with decription
560
561
561 """
562 """
562 _files = list()
563 _files = list()
563 _dirs = list()
564 _dirs = list()
564 try:
565 try:
565 _repo = self.__get_repo(repo_name)
566 _repo = self.__get_repo(repo_name)
566 changeset = _repo.scm_instance.get_changeset(revision)
567 changeset = _repo.scm_instance.get_changeset(revision)
567 root_path = root_path.lstrip('/')
568 root_path = root_path.lstrip('/')
568 for topnode, dirs, files in changeset.walk(root_path):
569 for topnode, dirs, files in changeset.walk(root_path):
569 for f in files:
570 for f in files:
570 _files.append(f.path if flat else {"name": f.path,
571 _files.append(f.path if flat else {"name": f.path,
571 "type": "file"})
572 "type": "file"})
572 for d in dirs:
573 for d in dirs:
573 _dirs.append(d.path if flat else {"name": d.path,
574 _dirs.append(d.path if flat else {"name": d.path,
574 "type": "dir"})
575 "type": "dir"})
575 except RepositoryError:
576 except RepositoryError:
576 log.debug(traceback.format_exc())
577 log.debug(traceback.format_exc())
577 raise
578 raise
578
579
579 return _dirs, _files
580 return _dirs, _files
580
581
581 def get_unread_journal(self):
582 def get_unread_journal(self):
582 return self.sa.query(UserLog).count()
583 return self.sa.query(UserLog).count()
583
584
584 def get_repo_landing_revs(self, repo=None):
585 def get_repo_landing_revs(self, repo=None):
585 """
586 """
586 Generates select option with tags branches and bookmarks (for hg only)
587 Generates select option with tags branches and bookmarks (for hg only)
587 grouped by type
588 grouped by type
588
589
589 :param repo:
590 :param repo:
590 :type repo:
591 :type repo:
591 """
592 """
592
593
593 hist_l = []
594 hist_l = []
594 choices = []
595 choices = []
595 repo = self.__get_repo(repo)
596 repo = self.__get_repo(repo)
596 hist_l.append(['tip', _('latest tip')])
597 hist_l.append(['tip', _('latest tip')])
597 choices.append('tip')
598 choices.append('tip')
598 if not repo:
599 if not repo:
599 return choices, hist_l
600 return choices, hist_l
600
601
601 repo = repo.scm_instance
602 repo = repo.scm_instance
602
603
603 branches_group = ([(k, k) for k, v in
604 branches_group = ([(k, k) for k, v in
604 repo.branches.iteritems()], _("Branches"))
605 repo.branches.iteritems()], _("Branches"))
605 hist_l.append(branches_group)
606 hist_l.append(branches_group)
606 choices.extend([x[0] for x in branches_group[0]])
607 choices.extend([x[0] for x in branches_group[0]])
607
608
608 if repo.alias == 'hg':
609 if repo.alias == 'hg':
609 bookmarks_group = ([(k, k) for k, v in
610 bookmarks_group = ([(k, k) for k, v in
610 repo.bookmarks.iteritems()], _("Bookmarks"))
611 repo.bookmarks.iteritems()], _("Bookmarks"))
611 hist_l.append(bookmarks_group)
612 hist_l.append(bookmarks_group)
612 choices.extend([x[0] for x in bookmarks_group[0]])
613 choices.extend([x[0] for x in bookmarks_group[0]])
613
614
614 tags_group = ([(k, k) for k, v in
615 tags_group = ([(k, k) for k, v in
615 repo.tags.iteritems()], _("Tags"))
616 repo.tags.iteritems()], _("Tags"))
616 hist_l.append(tags_group)
617 hist_l.append(tags_group)
617 choices.extend([x[0] for x in tags_group[0]])
618 choices.extend([x[0] for x in tags_group[0]])
618
619
619 return choices, hist_l
620 return choices, hist_l
620
621
621 def install_git_hook(self, repo, force_create=False):
622 def install_git_hook(self, repo, force_create=False):
622 """
623 """
623 Creates a rhodecode hook inside a git repository
624 Creates a rhodecode hook inside a git repository
624
625
625 :param repo: Instance of VCS repo
626 :param repo: Instance of VCS repo
626 :param force_create: Create even if same name hook exists
627 :param force_create: Create even if same name hook exists
627 """
628 """
628
629
629 loc = jn(repo.path, 'hooks')
630 loc = jn(repo.path, 'hooks')
630 if not repo.bare:
631 if not repo.bare:
631 loc = jn(repo.path, '.git', 'hooks')
632 loc = jn(repo.path, '.git', 'hooks')
632 if not os.path.isdir(loc):
633 if not os.path.isdir(loc):
633 os.makedirs(loc)
634 os.makedirs(loc)
634
635
635 tmpl_post = pkg_resources.resource_string(
636 tmpl_post = pkg_resources.resource_string(
636 'rhodecode', jn('config', 'post_receive_tmpl.py')
637 'rhodecode', jn('config', 'post_receive_tmpl.py')
637 )
638 )
638 tmpl_pre = pkg_resources.resource_string(
639 tmpl_pre = pkg_resources.resource_string(
639 'rhodecode', jn('config', 'pre_receive_tmpl.py')
640 'rhodecode', jn('config', 'pre_receive_tmpl.py')
640 )
641 )
641
642
642 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
643 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
643 _hook_file = jn(loc, '%s-receive' % h_type)
644 _hook_file = jn(loc, '%s-receive' % h_type)
644 _rhodecode_hook = False
645 _rhodecode_hook = False
645 log.debug('Installing git hook in repo %s' % repo)
646 log.debug('Installing git hook in repo %s' % repo)
646 if os.path.exists(_hook_file):
647 if os.path.exists(_hook_file):
647 # let's take a look at this hook, maybe it's rhodecode ?
648 # let's take a look at this hook, maybe it's rhodecode ?
648 log.debug('hook exists, checking if it is from rhodecode')
649 log.debug('hook exists, checking if it is from rhodecode')
649 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
650 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
650 with open(_hook_file, 'rb') as f:
651 with open(_hook_file, 'rb') as f:
651 data = f.read()
652 data = f.read()
652 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
653 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
653 % 'RC_HOOK_VER').search(data)
654 % 'RC_HOOK_VER').search(data)
654 if matches:
655 if matches:
655 try:
656 try:
656 ver = matches.groups()[0]
657 ver = matches.groups()[0]
657 log.debug('got %s it is rhodecode' % (ver))
658 log.debug('got %s it is rhodecode' % (ver))
658 _rhodecode_hook = True
659 _rhodecode_hook = True
659 except:
660 except:
660 log.error(traceback.format_exc())
661 log.error(traceback.format_exc())
661 else:
662 else:
662 # there is no hook in this dir, so we want to create one
663 # there is no hook in this dir, so we want to create one
663 _rhodecode_hook = True
664 _rhodecode_hook = True
664
665
665 if _rhodecode_hook or force_create:
666 if _rhodecode_hook or force_create:
666 log.debug('writing %s hook file !' % h_type)
667 log.debug('writing %s hook file !' % h_type)
667 with open(_hook_file, 'wb') as f:
668 with open(_hook_file, 'wb') as f:
668 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
669 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
669 f.write(tmpl)
670 f.write(tmpl)
670 os.chmod(_hook_file, 0755)
671 os.chmod(_hook_file, 0755)
671 else:
672 else:
672 log.debug('skipping writing hook file')
673 log.debug('skipping writing hook file')
General Comments 0
You need to be logged in to leave comments. Login now