##// 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 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.hooks
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Hooks runned by rhodecode
7 7
8 8 :created_on: Aug 6, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import sys
27 27 import time
28 28 import binascii
29 import traceback
29 30 from inspect import isfunction
30 31
31 32 from mercurial.scmutil import revrange
32 33 from mercurial.node import nullrev
33 34
34 35 from rhodecode.lib import helpers as h
35 36 from rhodecode.lib.utils import action_logger
36 37 from rhodecode.lib.vcs.backends.base import EmptyChangeset
37 38 from rhodecode.lib.compat import json
38 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 41 from rhodecode.model.db import Repository, User
41 42
42 43
43 44 def _get_scm_size(alias, root_path):
44 45
45 46 if not alias.startswith('.'):
46 47 alias += '.'
47 48
48 49 size_scm, size_root = 0, 0
49 50 for path, dirs, files in os.walk(safe_str(root_path)):
50 51 if path.find(alias) != -1:
51 52 for f in files:
52 53 try:
53 54 size_scm += os.path.getsize(os.path.join(path, f))
54 55 except OSError:
55 56 pass
56 57 else:
57 58 for f in files:
58 59 try:
59 60 size_root += os.path.getsize(os.path.join(path, f))
60 61 except OSError:
61 62 pass
62 63
63 64 size_scm_f = h.format_byte_size(size_scm)
64 65 size_root_f = h.format_byte_size(size_root)
65 66 size_total_f = h.format_byte_size(size_root + size_scm)
66 67
67 68 return size_scm_f, size_root_f, size_total_f
68 69
69 70
70 71 def repo_size(ui, repo, hooktype=None, **kwargs):
71 72 """
72 73 Presents size of repository after push
73 74
74 75 :param ui:
75 76 :param repo:
76 77 :param hooktype:
77 78 """
78 79
79 80 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
80 81
81 82 last_cs = repo[len(repo) - 1]
82 83
83 84 msg = ('Repository size .hg:%s repo:%s total:%s\n'
84 85 'Last revision is now r%s:%s\n') % (
85 86 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
86 87 )
87 88
88 89 sys.stdout.write(msg)
89 90
90 91
91 92 def pre_push(ui, repo, **kwargs):
92 93 # pre push function, currently used to ban pushing when
93 94 # repository is locked
94 try:
95 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
96 except:
97 rc_extras = {}
98 extras = dict(repo.ui.configitems('rhodecode_extras'))
95 ex = _extract_extras()
99 96
100 if 'username' in extras:
101 username = extras['username']
102 repository = extras['repository']
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
97 usr = User.get_by_username(ex.username)
98 if ex.locked_by[0] and usr.user_id != int(ex.locked_by[0]):
99 locked_by = User.get(ex.locked_by[0]).username
116 100 # this exception is interpreted in git/hg middlewares and based
117 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 103 if str(_http_ret.code).startswith('2'):
120 104 #2xx Codes don't raise exceptions
121 105 sys.stdout.write(_http_ret.title)
122 106 else:
123 107 raise _http_ret
124 108
125 109
126 110 def pre_pull(ui, repo, **kwargs):
127 111 # pre push function, currently used to ban pushing when
128 112 # repository is locked
129 try:
130 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
131 except:
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
113 ex = _extract_extras()
114 if ex.locked_by[0]:
115 locked_by = User.get(ex.locked_by[0]).username
149 116 # this exception is interpreted in git/hg middlewares and based
150 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 119 if str(_http_ret.code).startswith('2'):
153 120 #2xx Codes don't raise exceptions
154 121 sys.stdout.write(_http_ret.title)
155 122 else:
156 123 raise _http_ret
157 124
158 125
159 126 def log_pull_action(ui, repo, **kwargs):
160 127 """
161 128 Logs user last pull action
162 129
163 130 :param ui:
164 131 :param repo:
165 132 """
166 try:
167 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
168 except:
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)
133 ex = _extract_extras()
134
135 user = User.get_by_username(ex.username)
188 136 action = 'pull'
189 action_logger(user, action, repository, ip, commit=True)
137 action_logger(user, action, ex.repository, ex.ip, commit=True)
190 138 # extension hook call
191 139 from rhodecode import EXTENSIONS
192 140 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
193
194 141 if isfunction(callback):
195 142 kw = {}
196 kw.update(extras)
143 kw.update(ex)
197 144 callback(**kw)
198 145
199 if make_lock is True:
200 Repository.lock(Repository.get_by_repo_name(repository), user.user_id)
146 if ex.make_lock is True:
147 Repository.lock(Repository.get_by_repo_name(ex.repository), user.user_id)
201 148 #msg = 'Made lock on repo `%s`' % repository
202 149 #sys.stdout.write(msg)
203 150
204 if locked_by[0]:
205 locked_by = User.get(locked_by[0]).username
206 _http_ret = HTTPLockedRC(repository, locked_by)
151 if ex.locked_by[0]:
152 locked_by = User.get(ex.locked_by[0]).username
153 _http_ret = HTTPLockedRC(ex.repository, locked_by)
207 154 if str(_http_ret.code).startswith('2'):
208 155 #2xx Codes don't raise exceptions
209 156 sys.stdout.write(_http_ret.title)
210 157 return 0
211 158
212 159
213 160 def log_push_action(ui, repo, **kwargs):
214 161 """
215 162 Maps user last push action to new changeset id, from mercurial
216 163
217 164 :param ui:
218 165 :param repo: repo object containing the `ui` object
219 166 """
220 167
221 try:
222 rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}"))
223 except:
224 rc_extras = {}
168 ex = _extract_extras()
225 169
226 extras = dict(repo.ui.configitems('rhodecode_extras'))
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')
170 action = ex.action + ':%s'
243 171
244 action = action + ':%s'
245
246 if scm == 'hg':
172 if ex.scm == 'hg':
247 173 node = kwargs['node']
248 174
249 175 def get_revs(repo, rev_opt):
250 176 if rev_opt:
251 177 revs = revrange(repo, rev_opt)
252 178
253 179 if len(revs) == 0:
254 180 return (nullrev, nullrev)
255 181 return (max(revs), min(revs))
256 182 else:
257 183 return (len(repo) - 1, 0)
258 184
259 185 stop, start = get_revs(repo, [node + ':'])
260 186 h = binascii.hexlify
261 187 revs = [h(repo[r].node()) for r in xrange(start, stop + 1)]
262 elif scm == 'git':
188 elif ex.scm == 'git':
263 189 revs = kwargs.get('_git_revs', [])
264 190 if '_git_revs' in kwargs:
265 191 kwargs.pop('_git_revs')
266 192
267 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 197 # extension hook call
272 198 from rhodecode import EXTENSIONS
273 199 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
274 200 if isfunction(callback):
275 201 kw = {'pushed_revs': revs}
276 kw.update(extras)
202 kw.update(ex)
277 203 callback(**kw)
278 204
279 if make_lock is False:
280 Repository.unlock(Repository.get_by_repo_name(repository))
281 msg = 'Released lock on repo `%s`\n' % repository
205 if ex.make_lock is False:
206 Repository.unlock(Repository.get_by_repo_name(ex.repository))
207 msg = 'Released lock on repo `%s`\n' % ex.repository
282 208 sys.stdout.write(msg)
283 209
284 if locked_by[0]:
285 locked_by = User.get(locked_by[0]).username
286 _http_ret = HTTPLockedRC(repository, locked_by)
210 if ex.locked_by[0]:
211 locked_by = User.get(ex.locked_by[0]).username
212 _http_ret = HTTPLockedRC(ex.repository, locked_by)
287 213 if str(_http_ret.code).startswith('2'):
288 214 #2xx Codes don't raise exceptions
289 215 sys.stdout.write(_http_ret.title)
290 216
291 217 return 0
292 218
293 219
294 220 def log_create_repository(repository_dict, created_by, **kwargs):
295 221 """
296 222 Post create repository Hook. This is a dummy function for admins to re-use
297 223 if needed. It's taken from rhodecode-extensions module and executed
298 224 if present
299 225
300 226 :param repository: dict dump of repository object
301 227 :param created_by: username who created repository
302 228
303 229 available keys of repository_dict:
304 230
305 231 'repo_type',
306 232 'description',
307 233 'private',
308 234 'created_on',
309 235 'enable_downloads',
310 236 'repo_id',
311 237 'user_id',
312 238 'enable_statistics',
313 239 'clone_uri',
314 240 'fork_id',
315 241 'group_id',
316 242 'repo_name'
317 243
318 244 """
319 245 from rhodecode import EXTENSIONS
320 246 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
321 247 if isfunction(callback):
322 248 kw = {}
323 249 kw.update(repository_dict)
324 250 kw.update({'created_by': created_by})
325 251 kw.update(kwargs)
326 252 return callback(**kw)
327 253
328 254 return 0
329 255
330 256
331 257 def log_delete_repository(repository_dict, deleted_by, **kwargs):
332 258 """
333 259 Post delete repository Hook. This is a dummy function for admins to re-use
334 260 if needed. It's taken from rhodecode-extensions module and executed
335 261 if present
336 262
337 263 :param repository: dict dump of repository object
338 264 :param deleted_by: username who deleted the repository
339 265
340 266 available keys of repository_dict:
341 267
342 268 'repo_type',
343 269 'description',
344 270 'private',
345 271 'created_on',
346 272 'enable_downloads',
347 273 'repo_id',
348 274 'user_id',
349 275 'enable_statistics',
350 276 'clone_uri',
351 277 'fork_id',
352 278 'group_id',
353 279 'repo_name'
354 280
355 281 """
356 282 from rhodecode import EXTENSIONS
357 283 callback = getattr(EXTENSIONS, 'DELETE_REPO_HOOK', None)
358 284 if isfunction(callback):
359 285 kw = {}
360 286 kw.update(repository_dict)
361 287 kw.update({'deleted_by': deleted_by,
362 288 'deleted_on': time.time()})
363 289 kw.update(kwargs)
364 290 return callback(**kw)
365 291
366 292 return 0
367 293
368 294
369 295 handle_git_pre_receive = (lambda repo_path, revs, env:
370 296 handle_git_receive(repo_path, revs, env, hook_type='pre'))
371 297 handle_git_post_receive = (lambda repo_path, revs, env:
372 298 handle_git_receive(repo_path, revs, env, hook_type='post'))
373 299
374 300
375 301 def handle_git_receive(repo_path, revs, env, hook_type='post'):
376 302 """
377 303 A really hacky method that is runned by git post-receive hook and logs
378 304 an push action together with pushed revisions. It's executed by subprocess
379 305 thus needs all info to be able to create a on the fly pylons enviroment,
380 306 connect to database and run the logging code. Hacky as sh*t but works.
381 307
382 308 :param repo_path:
383 309 :type repo_path:
384 310 :param revs:
385 311 :type revs:
386 312 :param env:
387 313 :type env:
388 314 """
389 315 from paste.deploy import appconfig
390 316 from sqlalchemy import engine_from_config
391 317 from rhodecode.config.environment import load_environment
392 318 from rhodecode.model import init_model
393 319 from rhodecode.model.db import RhodeCodeUi
394 320 from rhodecode.lib.utils import make_ui
395 321 extras = json.loads(env['RHODECODE_EXTRAS'])
396 322
397 323 path, ini_name = os.path.split(extras['config'])
398 324 conf = appconfig('config:%s' % ini_name, relative_to=path)
399 325 load_environment(conf.global_conf, conf.local_conf)
400 326
401 327 engine = engine_from_config(conf, 'sqlalchemy.db1.')
402 328 init_model(engine)
403 329
404 330 baseui = make_ui('db')
405 331 # fix if it's not a bare repo
406 332 if repo_path.endswith(os.sep + '.git'):
407 333 repo_path = repo_path[:-5]
408 334
409 335 repo = Repository.get_by_full_path(repo_path)
410 336 if not repo:
411 337 raise OSError('Repository %s not found in database'
412 338 % (safe_str(repo_path)))
413 339
414 340 _hooks = dict(baseui.configitems('hooks')) or {}
415 341
416 342 for k, v in extras.items():
417 343 baseui.setconfig('rhodecode_extras', k, v)
418 344 if hook_type == 'pre':
419 345 repo = repo.scm_instance
420 346 else:
421 347 #post push shouldn't use the cached instance never
422 348 repo = repo.scm_instance_no_cache()
423 349
424 350 repo.ui = baseui
425 351
426 352 if hook_type == 'pre':
427 353 pre_push(baseui, repo)
428 354
429 355 # if push hook is enabled via web interface
430 356 elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
431 357
432 358 rev_data = []
433 359 for l in revs:
434 360 old_rev, new_rev, ref = l.split(' ')
435 361 _ref_data = ref.split('/')
436 362 if _ref_data[1] in ['tags', 'heads']:
437 363 rev_data.append({'old_rev': old_rev,
438 364 'new_rev': new_rev,
439 365 'ref': ref,
440 366 'type': _ref_data[1],
441 367 'name': _ref_data[2].strip()})
442 368
443 369 git_revs = []
444 370 for push_ref in rev_data:
445 371 _type = push_ref['type']
446 372 if _type == 'heads':
447 373 if push_ref['old_rev'] == EmptyChangeset().raw_id:
448 374 cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
449 375 heads = repo.run_git_command(cmd)[0]
450 376 heads = heads.replace(push_ref['ref'], '')
451 377 heads = ' '.join(map(lambda c: c.strip('\n').strip(),
452 378 heads.splitlines()))
453 379 cmd = (('log %(new_rev)s' % push_ref) +
454 380 ' --reverse --pretty=format:"%H" --not ' + heads)
455 381 git_revs += repo.run_git_command(cmd)[0].splitlines()
456 382
457 383 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
458 384 #delete branch case
459 385 git_revs += ['delete_branch=>%s' % push_ref['name']]
460 386 else:
461 387 cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
462 388 ' --reverse --pretty=format:"%H"')
463 389 git_revs += repo.run_git_command(cmd)[0].splitlines()
464 390
465 391 elif _type == 'tags':
466 392 git_revs += ['tag=>%s' % push_ref['name']]
467 393
468 394 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,203 +1,201 b''
1 1 import os
2 2 import socket
3 3 import logging
4 4 import subprocess
5 5 import traceback
6 6
7 7 from webob import Request, Response, exc
8 8
9 9 import rhodecode
10 10 from rhodecode.lib import subprocessio
11 11
12 12 log = logging.getLogger(__name__)
13 13
14 14
15 15 class FileWrapper(object):
16 16
17 17 def __init__(self, fd, content_length):
18 18 self.fd = fd
19 19 self.content_length = content_length
20 20 self.remain = content_length
21 21
22 22 def read(self, size):
23 23 if size <= self.remain:
24 24 try:
25 25 data = self.fd.read(size)
26 26 except socket.error:
27 27 raise IOError(self)
28 28 self.remain -= size
29 29 elif self.remain:
30 30 data = self.fd.read(self.remain)
31 31 self.remain = 0
32 32 else:
33 33 data = None
34 34 return data
35 35
36 36 def __repr__(self):
37 37 return '<FileWrapper %s len: %s, read: %s>' % (
38 38 self.fd, self.content_length, self.content_length - self.remain
39 39 )
40 40
41 41
42 42 class GitRepository(object):
43 43 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
44 44 commands = ['git-upload-pack', 'git-receive-pack']
45 45
46 46 def __init__(self, repo_name, content_path, extras):
47 47 files = set([f.lower() for f in os.listdir(content_path)])
48 48 if not (self.git_folder_signature.intersection(files)
49 49 == self.git_folder_signature):
50 50 raise OSError('%s missing git signature' % content_path)
51 51 self.content_path = content_path
52 52 self.valid_accepts = ['application/x-%s-result' %
53 53 c for c in self.commands]
54 54 self.repo_name = repo_name
55 55 self.extras = extras
56 56
57 57 def _get_fixedpath(self, path):
58 58 """
59 59 Small fix for repo_path
60 60
61 61 :param path:
62 62 :type path:
63 63 """
64 64 return path.split(self.repo_name, 1)[-1].strip('/')
65 65
66 66 def inforefs(self, request, environ):
67 67 """
68 68 WSGI Response producer for HTTP GET Git Smart
69 69 HTTP /info/refs request.
70 70 """
71 71
72 72 git_command = request.GET.get('service')
73 73 if git_command not in self.commands:
74 74 log.debug('command %s not allowed' % git_command)
75 75 return exc.HTTPMethodNotAllowed()
76 76
77 77 # note to self:
78 78 # please, resist the urge to add '\n' to git capture and increment
79 79 # line count by 1.
80 80 # The code in Git client not only does NOT need '\n', but actually
81 81 # blows up if you sprinkle "flush" (0000) as "0001\n".
82 82 # It reads binary, per number of bytes specified.
83 83 # if you do add '\n' as part of data, count it.
84 84 server_advert = '# service=%s' % git_command
85 85 packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
86 86 _git_path = rhodecode.CONFIG.get('git_path', 'git')
87 87 try:
88 88 out = subprocessio.SubprocessIOChunker(
89 89 r'%s %s --stateless-rpc --advertise-refs "%s"' % (
90 90 _git_path, git_command[4:], self.content_path),
91 91 starting_values=[
92 92 packet_len + server_advert + '0000'
93 93 ]
94 94 )
95 95 except EnvironmentError, e:
96 96 log.error(traceback.format_exc())
97 97 raise exc.HTTPExpectationFailed()
98 98 resp = Response()
99 99 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
100 100 resp.charset = None
101 101 resp.app_iter = out
102 102 return resp
103 103
104 104 def backend(self, request, environ):
105 105 """
106 106 WSGI Response producer for HTTP POST Git Smart HTTP requests.
107 107 Reads commands and data from HTTP POST's body.
108 108 returns an iterator obj with contents of git command's
109 109 response to stdout
110 110 """
111 111 git_command = self._get_fixedpath(request.path_info)
112 112 if git_command not in self.commands:
113 113 log.debug('command %s not allowed' % git_command)
114 114 return exc.HTTPMethodNotAllowed()
115 115
116 116 if 'CONTENT_LENGTH' in environ:
117 117 inputstream = FileWrapper(environ['wsgi.input'],
118 118 request.content_length)
119 119 else:
120 120 inputstream = environ['wsgi.input']
121 121
122 122 try:
123 123 gitenv = os.environ
124 from rhodecode.lib.compat import json
125 gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras)
126 124 # forget all configs
127 125 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
128 126 opts = dict(
129 127 env=gitenv,
130 128 cwd=os.getcwd()
131 129 )
132 130 cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
133 131 self.content_path),
134 132 log.debug('handling cmd %s' % cmd)
135 133 out = subprocessio.SubprocessIOChunker(
136 134 cmd,
137 135 inputstream=inputstream,
138 136 **opts
139 137 )
140 138 except EnvironmentError, e:
141 139 log.error(traceback.format_exc())
142 140 raise exc.HTTPExpectationFailed()
143 141
144 142 if git_command in [u'git-receive-pack']:
145 143 # updating refs manually after each push.
146 144 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
147 145 _git_path = rhodecode.CONFIG.get('git_path', 'git')
148 146 cmd = (u'%s --git-dir "%s" '
149 147 'update-server-info' % (_git_path, self.content_path))
150 148 log.debug('handling cmd %s' % cmd)
151 149 subprocess.call(cmd, shell=True)
152 150
153 151 resp = Response()
154 152 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
155 153 resp.charset = None
156 154 resp.app_iter = out
157 155 return resp
158 156
159 157 def __call__(self, environ, start_response):
160 158 request = Request(environ)
161 159 _path = self._get_fixedpath(request.path_info)
162 160 if _path.startswith('info/refs'):
163 161 app = self.inforefs
164 162 elif [a for a in self.valid_accepts if a in request.accept]:
165 163 app = self.backend
166 164 try:
167 165 resp = app(request, environ)
168 166 except exc.HTTPException, e:
169 167 resp = e
170 168 log.error(traceback.format_exc())
171 169 except Exception, e:
172 170 log.error(traceback.format_exc())
173 171 resp = exc.HTTPInternalServerError()
174 172 return resp(environ, start_response)
175 173
176 174
177 175 class GitDirectory(object):
178 176
179 177 def __init__(self, repo_root, repo_name, extras):
180 178 repo_location = os.path.join(repo_root, repo_name)
181 179 if not os.path.isdir(repo_location):
182 180 raise OSError(repo_location)
183 181
184 182 self.content_path = repo_location
185 183 self.repo_name = repo_name
186 184 self.repo_location = repo_location
187 185 self.extras = extras
188 186
189 187 def __call__(self, environ, start_response):
190 188 content_path = self.content_path
191 189 try:
192 190 app = GitRepository(self.repo_name, content_path, self.extras)
193 191 except (AssertionError, OSError):
194 192 content_path = os.path.join(content_path, '.git')
195 193 if os.path.isdir(content_path):
196 194 app = GitRepository(self.repo_name, content_path, self.extras)
197 195 else:
198 196 return exc.HTTPNotFound()(environ, start_response)
199 197 return app(environ, start_response)
200 198
201 199
202 200 def make_wsgi_app(repo_name, repo_root, extras):
203 201 return GitDirectory(repo_root, repo_name, extras)
@@ -1,342 +1,337 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplegit
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 7 It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import re
29 29 import logging
30 30 import traceback
31 31
32 32 from dulwich import server as dulserver
33 33 from dulwich.web import LimitedInputFilter, GunzipFilter
34 34 from rhodecode.lib.exceptions import HTTPLockedRC
35 35 from rhodecode.lib.hooks import pre_pull
36 36
37 37
38 38 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
39 39
40 40 def handle(self):
41 41 write = lambda x: self.proto.write_sideband(1, x)
42 42
43 43 graph_walker = dulserver.ProtocolGraphWalker(self,
44 44 self.repo.object_store,
45 45 self.repo.get_peeled)
46 46 objects_iter = self.repo.fetch_objects(
47 47 graph_walker.determine_wants, graph_walker, self.progress,
48 48 get_tagged=self.get_tagged)
49 49
50 50 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
51 51 # that the client still expects a 0-object pack in most cases.
52 52 if objects_iter is None:
53 53 return
54 54
55 55 self.progress("counting objects: %d, done.\n" % len(objects_iter))
56 56 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
57 57 objects_iter)
58 58 messages = []
59 59 messages.append('thank you for using rhodecode')
60 60
61 61 for msg in messages:
62 62 self.progress(msg + "\n")
63 63 # we are done
64 64 self.proto.write("0000")
65 65
66 66
67 67 dulserver.DEFAULT_HANDLERS = {
68 68 #git-ls-remote, git-clone, git-fetch and git-pull
69 69 'git-upload-pack': SimpleGitUploadPackHandler,
70 70 #git-push
71 71 'git-receive-pack': dulserver.ReceivePackHandler,
72 72 }
73 73
74 74 # not used for now until dulwich get's fixed
75 75 #from dulwich.repo import Repo
76 76 #from dulwich.web import make_wsgi_chain
77 77
78 78 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
79 79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
80 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 84 from rhodecode.lib.base import BaseVCSController
84 85 from rhodecode.lib.auth import get_container_username
85 86 from rhodecode.lib.utils import is_valid_repo, make_ui
86 87 from rhodecode.lib.compat import json
87 88 from rhodecode.model.db import User, RhodeCodeUi
88 89
89 90 log = logging.getLogger(__name__)
90 91
91 92
92 93 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
93 94
94 95
95 96 def is_git(environ):
96 97 path_info = environ['PATH_INFO']
97 98 isgit_path = GIT_PROTO_PAT.match(path_info)
98 99 log.debug('pathinfo: %s detected as GIT %s' % (
99 100 path_info, isgit_path != None)
100 101 )
101 102 return isgit_path
102 103
103 104
104 105 class SimpleGit(BaseVCSController):
105 106
106 107 def _handle_request(self, environ, start_response):
107 108 if not is_git(environ):
108 109 return self.application(environ, start_response)
109 110 if not self._check_ssl(environ, start_response):
110 111 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
111 112
112 113 ip_addr = self._get_ip_addr(environ)
113 114 username = None
114 115 self._git_first_op = False
115 116 # skip passing error to error controller
116 117 environ['pylons.status_code_redirect'] = True
117 118
118 119 #======================================================================
119 120 # EXTRACT REPOSITORY NAME FROM ENV
120 121 #======================================================================
121 122 try:
122 123 repo_name = self.__get_repository(environ)
123 124 log.debug('Extracted repo name is %s' % repo_name)
124 125 except:
125 126 return HTTPInternalServerError()(environ, start_response)
126 127
127 128 # quick check if that dir exists...
128 129 if is_valid_repo(repo_name, self.basepath, 'git') is False:
129 130 return HTTPNotFound()(environ, start_response)
130 131
131 132 #======================================================================
132 133 # GET ACTION PULL or PUSH
133 134 #======================================================================
134 135 action = self.__get_action(environ)
135 136
136 137 #======================================================================
137 138 # CHECK ANONYMOUS PERMISSION
138 139 #======================================================================
139 140 if action in ['pull', 'push']:
140 141 anonymous_user = self.__get_user('default')
141 142 username = anonymous_user.username
142 143 anonymous_perm = self._check_permission(action, anonymous_user,
143 144 repo_name, ip_addr)
144 145
145 146 if anonymous_perm is not True or anonymous_user.active is False:
146 147 if anonymous_perm is not True:
147 148 log.debug('Not enough credentials to access this '
148 149 'repository as anonymous user')
149 150 if anonymous_user.active is False:
150 151 log.debug('Anonymous access is disabled, running '
151 152 'authentication')
152 153 #==============================================================
153 154 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
154 155 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
155 156 #==============================================================
156 157
157 158 # Attempting to retrieve username from the container
158 159 username = get_container_username(environ, self.config)
159 160
160 161 # If not authenticated by the container, running basic auth
161 162 if not username:
162 163 self.authenticate.realm = \
163 164 safe_str(self.config['rhodecode_realm'])
164 165 result = self.authenticate(environ)
165 166 if isinstance(result, str):
166 167 AUTH_TYPE.update(environ, 'basic')
167 168 REMOTE_USER.update(environ, result)
168 169 username = result
169 170 else:
170 171 return result.wsgi_application(environ, start_response)
171 172
172 173 #==============================================================
173 174 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
174 175 #==============================================================
175 176 try:
176 177 user = self.__get_user(username)
177 178 if user is None or not user.active:
178 179 return HTTPForbidden()(environ, start_response)
179 180 username = user.username
180 181 except:
181 182 log.error(traceback.format_exc())
182 183 return HTTPInternalServerError()(environ, start_response)
183 184
184 185 #check permissions for this repository
185 186 perm = self._check_permission(action, user, repo_name, ip_addr)
186 187 if perm is not True:
187 188 return HTTPForbidden()(environ, start_response)
188 189
189 190 # extras are injected into UI object and later available
190 191 # in hooks executed by rhodecode
191 192 from rhodecode import CONFIG
192 193 server_url = get_server_url(environ)
193 194 extras = {
194 195 'ip': ip_addr,
195 196 'username': username,
196 197 'action': action,
197 198 'repository': repo_name,
198 199 'scm': 'git',
199 200 'config': CONFIG['__file__'],
200 201 'server_url': server_url,
201 202 'make_lock': None,
202 203 'locked_by': [None, None]
203 204 }
204 205
205 206 #===================================================================
206 207 # GIT REQUEST HANDLING
207 208 #===================================================================
208 209 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
209 210 log.debug('Repository path is %s' % repo_path)
210 211
211 212 # CHECK LOCKING only if it's not ANONYMOUS USER
212 213 if username != User.DEFAULT_USER:
213 214 log.debug('Checking locking on repository')
214 215 (make_lock,
215 216 locked,
216 217 locked_by) = self._check_locking_state(
217 218 environ=environ, action=action,
218 219 repo=repo_name, user_id=user.user_id
219 220 )
220 221 # store the make_lock for later evaluation in hooks
221 222 extras.update({'make_lock': make_lock,
222 223 'locked_by': locked_by})
223 224 # set the environ variables for this request
224 225 os.environ['RC_SCM_DATA'] = json.dumps(extras)
225 226 fix_PATH()
226 227 log.debug('HOOKS extras is %s' % extras)
227 228 baseui = make_ui('db')
228 229 self.__inject_extras(repo_path, baseui, extras)
229 230
230 231 try:
231 232 self._handle_githooks(repo_name, action, baseui, environ)
232 233 log.info('%s action on GIT repo "%s" by "%s" from %s' %
233 234 (action, repo_name, username, ip_addr))
234 235 app = self.__make_app(repo_name, repo_path, extras)
235 236 return app(environ, start_response)
236 237 except HTTPLockedRC, e:
237 238 _code = CONFIG.get('lock_ret_code')
238 239 log.debug('Repository LOCKED ret code %s!' % (_code))
239 240 return e(environ, start_response)
240 241 except Exception:
241 242 log.error(traceback.format_exc())
242 243 return HTTPInternalServerError()(environ, start_response)
243 244 finally:
244 245 # invalidate cache on push
245 246 if action == 'push':
246 247 self._invalidate_cache(repo_name)
247 248
248 249 def __make_app(self, repo_name, repo_path, extras):
249 250 """
250 251 Make an wsgi application using dulserver
251 252
252 253 :param repo_name: name of the repository
253 254 :param repo_path: full path to the repository
254 255 """
255 256
256 257 from rhodecode.lib.middleware.pygrack import make_wsgi_app
257 258 app = make_wsgi_app(
258 259 repo_root=safe_str(self.basepath),
259 260 repo_name=repo_name,
260 261 extras=extras,
261 262 )
262 263 app = GunzipFilter(LimitedInputFilter(app))
263 264 return app
264 265
265 266 def __get_repository(self, environ):
266 267 """
267 268 Get's repository name out of PATH_INFO header
268 269
269 270 :param environ: environ where PATH_INFO is stored
270 271 """
271 272 try:
272 273 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
273 274 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
274 275 except:
275 276 log.error(traceback.format_exc())
276 277 raise
277 278
278 279 return repo_name
279 280
280 281 def __get_user(self, username):
281 282 return User.get_by_username(username)
282 283
283 284 def __get_action(self, environ):
284 285 """
285 286 Maps git request commands into a pull or push command.
286 287
287 288 :param environ:
288 289 """
289 290 service = environ['QUERY_STRING'].split('=')
290 291
291 292 if len(service) > 1:
292 293 service_cmd = service[1]
293 294 mapping = {
294 295 'git-receive-pack': 'push',
295 296 'git-upload-pack': 'pull',
296 297 }
297 298 op = mapping[service_cmd]
298 299 self._git_stored_op = op
299 300 return op
300 301 else:
301 302 # try to fallback to stored variable as we don't know if the last
302 303 # operation is pull/push
303 304 op = getattr(self, '_git_stored_op', 'pull')
304 305 return op
305 306
306 307 def _handle_githooks(self, repo_name, action, baseui, environ):
307 308 """
308 309 Handles pull action, push is handled by post-receive hook
309 310 """
310 311 from rhodecode.lib.hooks import log_pull_action
311 312 service = environ['QUERY_STRING'].split('=')
312 313
313 314 if len(service) < 2:
314 315 return
315 316
316 317 from rhodecode.model.db import Repository
317 318 _repo = Repository.get_by_repo_name(repo_name)
318 319 _repo = _repo.scm_instance
319 320 _repo._repo.ui = baseui
320 321
321 322 _hooks = dict(baseui.configitems('hooks')) or {}
322 323 if action == 'pull':
323 324 # stupid git, emulate pre-pull hook !
324 325 pre_pull(ui=baseui, repo=_repo._repo)
325 326 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
326 327 log_pull_action(ui=baseui, repo=_repo._repo)
327 328
328 329 def __inject_extras(self, repo_path, baseui, extras={}):
329 330 """
330 331 Injects some extra params into baseui instance
331 332
332 333 :param baseui: baseui instance
333 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 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)
337 _set_extras(extras)
@@ -1,290 +1,287 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.middleware.simplehg
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 SimpleHG middleware for handling mercurial protocol request
7 7 (push/clone etc.). It's implemented with basic auth function
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30
31 31 from mercurial.error import RepoError
32 32 from mercurial.hgweb import hgweb_mod
33 33
34 34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 35 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
36 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 40 from rhodecode.lib.base import BaseVCSController
40 41 from rhodecode.lib.auth import get_container_username
41 42 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
42 43 from rhodecode.lib.compat import json
43 44 from rhodecode.model.db import User
44 45 from rhodecode.lib.exceptions import HTTPLockedRC
45 46
46 47
47 48 log = logging.getLogger(__name__)
48 49
49 50
50 51 def is_mercurial(environ):
51 52 """
52 53 Returns True if request's target is mercurial server - header
53 54 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 55 """
55 56 http_accept = environ.get('HTTP_ACCEPT')
56 57 path_info = environ['PATH_INFO']
57 58 if http_accept and http_accept.startswith('application/mercurial'):
58 59 ishg_path = True
59 60 else:
60 61 ishg_path = False
61 62
62 63 log.debug('pathinfo: %s detected as HG %s' % (
63 64 path_info, ishg_path)
64 65 )
65 66 return ishg_path
66 67
67 68
68 69 class SimpleHg(BaseVCSController):
69 70
70 71 def _handle_request(self, environ, start_response):
71 72 if not is_mercurial(environ):
72 73 return self.application(environ, start_response)
73 74 if not self._check_ssl(environ, start_response):
74 75 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
75 76
76 77 ip_addr = self._get_ip_addr(environ)
77 78 username = None
78 79 # skip passing error to error controller
79 80 environ['pylons.status_code_redirect'] = True
80 81
81 82 #======================================================================
82 83 # EXTRACT REPOSITORY NAME FROM ENV
83 84 #======================================================================
84 85 try:
85 86 repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
86 87 log.debug('Extracted repo name is %s' % repo_name)
87 88 except:
88 89 return HTTPInternalServerError()(environ, start_response)
89 90
90 91 # quick check if that dir exists...
91 92 if is_valid_repo(repo_name, self.basepath, 'hg') is False:
92 93 return HTTPNotFound()(environ, start_response)
93 94
94 95 #======================================================================
95 96 # GET ACTION PULL or PUSH
96 97 #======================================================================
97 98 action = self.__get_action(environ)
98 99
99 100 #======================================================================
100 101 # CHECK ANONYMOUS PERMISSION
101 102 #======================================================================
102 103 if action in ['pull', 'push']:
103 104 anonymous_user = self.__get_user('default')
104 105 username = anonymous_user.username
105 106 anonymous_perm = self._check_permission(action, anonymous_user,
106 107 repo_name, ip_addr)
107 108
108 109 if anonymous_perm is not True or anonymous_user.active is False:
109 110 if anonymous_perm is not True:
110 111 log.debug('Not enough credentials to access this '
111 112 'repository as anonymous user')
112 113 if anonymous_user.active is False:
113 114 log.debug('Anonymous access is disabled, running '
114 115 'authentication')
115 116 #==============================================================
116 117 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
117 118 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
118 119 #==============================================================
119 120
120 121 # Attempting to retrieve username from the container
121 122 username = get_container_username(environ, self.config)
122 123
123 124 # If not authenticated by the container, running basic auth
124 125 if not username:
125 126 self.authenticate.realm = \
126 127 safe_str(self.config['rhodecode_realm'])
127 128 result = self.authenticate(environ)
128 129 if isinstance(result, str):
129 130 AUTH_TYPE.update(environ, 'basic')
130 131 REMOTE_USER.update(environ, result)
131 132 username = result
132 133 else:
133 134 return result.wsgi_application(environ, start_response)
134 135
135 136 #==============================================================
136 137 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
137 138 #==============================================================
138 139 try:
139 140 user = self.__get_user(username)
140 141 if user is None or not user.active:
141 142 return HTTPForbidden()(environ, start_response)
142 143 username = user.username
143 144 except:
144 145 log.error(traceback.format_exc())
145 146 return HTTPInternalServerError()(environ, start_response)
146 147
147 148 #check permissions for this repository
148 149 perm = self._check_permission(action, user, repo_name, ip_addr)
149 150 if perm is not True:
150 151 return HTTPForbidden()(environ, start_response)
151 152
152 153 # extras are injected into mercurial UI object and later available
153 154 # in hg hooks executed by rhodecode
154 155 from rhodecode import CONFIG
155 156 server_url = get_server_url(environ)
156 157 extras = {
157 158 'ip': ip_addr,
158 159 'username': username,
159 160 'action': action,
160 161 'repository': repo_name,
161 162 'scm': 'hg',
162 163 'config': CONFIG['__file__'],
163 164 'server_url': server_url,
164 165 'make_lock': None,
165 166 'locked_by': [None, None]
166 167 }
167 168 #======================================================================
168 169 # MERCURIAL REQUEST HANDLING
169 170 #======================================================================
170 171 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
171 172 log.debug('Repository path is %s' % repo_path)
172 173
173 174 # CHECK LOCKING only if it's not ANONYMOUS USER
174 175 if username != User.DEFAULT_USER:
175 176 log.debug('Checking locking on repository')
176 177 (make_lock,
177 178 locked,
178 179 locked_by) = self._check_locking_state(
179 180 environ=environ, action=action,
180 181 repo=repo_name, user_id=user.user_id
181 182 )
182 183 # store the make_lock for later evaluation in hooks
183 184 extras.update({'make_lock': make_lock,
184 185 'locked_by': locked_by})
185 186
186 187 # set the environ variables for this request
187 188 os.environ['RC_SCM_DATA'] = json.dumps(extras)
188 189 fix_PATH()
189 190 log.debug('HOOKS extras is %s' % extras)
190 191 baseui = make_ui('db')
191 192 self.__inject_extras(repo_path, baseui, extras)
192 193
193 194 try:
194 195 log.info('%s action on HG repo "%s" by "%s" from %s' %
195 196 (action, repo_name, username, ip_addr))
196 197 app = self.__make_app(repo_path, baseui, extras)
197 198 return app(environ, start_response)
198 199 except RepoError, e:
199 200 if str(e).find('not found') != -1:
200 201 return HTTPNotFound()(environ, start_response)
201 202 except HTTPLockedRC, e:
202 203 _code = CONFIG.get('lock_ret_code')
203 204 log.debug('Repository LOCKED ret code %s!' % (_code))
204 205 return e(environ, start_response)
205 206 except Exception:
206 207 log.error(traceback.format_exc())
207 208 return HTTPInternalServerError()(environ, start_response)
208 209 finally:
209 210 # invalidate cache on push
210 211 if action == 'push':
211 212 self._invalidate_cache(repo_name)
212 213
213 214 def __make_app(self, repo_name, baseui, extras):
214 215 """
215 216 Make an wsgi application using hgweb, and inject generated baseui
216 217 instance, additionally inject some extras into ui object
217 218 """
218 219 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
219 220
220 221 def __get_repository(self, environ):
221 222 """
222 223 Get's repository name out of PATH_INFO header
223 224
224 225 :param environ: environ where PATH_INFO is stored
225 226 """
226 227 try:
227 228 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
228 229 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
229 230 if repo_name.endswith('/'):
230 231 repo_name = repo_name.rstrip('/')
231 232 except:
232 233 log.error(traceback.format_exc())
233 234 raise
234 235
235 236 return repo_name
236 237
237 238 def __get_user(self, username):
238 239 return User.get_by_username(username)
239 240
240 241 def __get_action(self, environ):
241 242 """
242 243 Maps mercurial request commands into a clone,pull or push command.
243 244 This should always return a valid command string
244 245
245 246 :param environ:
246 247 """
247 248 mapping = {'changegroup': 'pull',
248 249 'changegroupsubset': 'pull',
249 250 'stream_out': 'pull',
250 251 'listkeys': 'pull',
251 252 'unbundle': 'push',
252 253 'pushkey': 'push', }
253 254 for qry in environ['QUERY_STRING'].split('&'):
254 255 if qry.startswith('cmd'):
255 256 cmd = qry.split('=')[-1]
256 257 if cmd in mapping:
257 258 return mapping[cmd]
258 259
259 260 return 'pull'
260 261
261 262 raise Exception('Unable to detect pull/push action !!'
262 263 'Are you using non standard command or client ?')
263 264
264 265 def __inject_extras(self, repo_path, baseui, extras={}):
265 266 """
266 267 Injects some extra params into baseui instance
267 268
268 269 also overwrites global settings with those takes from local hgrc file
269 270
270 271 :param baseui: baseui instance
271 272 :param extras: dict with extra params to put into baseui
272 273 """
273 274
274 275 hgrc = os.path.join(repo_path, '.hg', 'hgrc')
275 276
276 277 # make our hgweb quiet so it doesn't print output
277 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 280 repoui = make_ui('file', hgrc, False)
285 281
286 282 if repoui:
287 283 #overwrite our ui instance with the section from hgrc file
288 284 for section in ui_sections:
289 285 for k, v in repoui.configitems(section):
290 286 baseui.setconfig(section, k, v)
287 _set_extras(extras)
@@ -1,580 +1,609 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 import os
26 27 import re
28 import sys
27 29 import time
28 30 import datetime
31 import traceback
29 32 import webob
30 33
31 34 from pylons.i18n.translation import _, ungettext
32 35 from rhodecode.lib.vcs.utils.lazy import LazyProperty
36 from rhodecode.lib.compat import json
33 37
34 38
35 39 def __get_lem():
36 40 """
37 41 Get language extension map based on what's inside pygments lexers
38 42 """
39 43 from pygments import lexers
40 44 from string import lower
41 45 from collections import defaultdict
42 46
43 47 d = defaultdict(lambda: [])
44 48
45 49 def __clean(s):
46 50 s = s.lstrip('*')
47 51 s = s.lstrip('.')
48 52
49 53 if s.find('[') != -1:
50 54 exts = []
51 55 start, stop = s.find('['), s.find(']')
52 56
53 57 for suffix in s[start + 1:stop]:
54 58 exts.append(s[:s.find('[')] + suffix)
55 59 return map(lower, exts)
56 60 else:
57 61 return map(lower, [s])
58 62
59 63 for lx, t in sorted(lexers.LEXERS.items()):
60 64 m = map(__clean, t[-2])
61 65 if m:
62 66 m = reduce(lambda x, y: x + y, m)
63 67 for ext in m:
64 68 desc = lx.replace('Lexer', '')
65 69 d[ext].append(desc)
66 70
67 71 return dict(d)
68 72
69 73
70 74 def str2bool(_str):
71 75 """
72 76 returs True/False value from given string, it tries to translate the
73 77 string into boolean
74 78
75 79 :param _str: string value to translate into boolean
76 80 :rtype: boolean
77 81 :returns: boolean from given string
78 82 """
79 83 if _str is None:
80 84 return False
81 85 if _str in (True, False):
82 86 return _str
83 87 _str = str(_str).strip().lower()
84 88 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
85 89
86 90
87 91 def aslist(obj, sep=None, strip=True):
88 92 """
89 93 Returns given string separated by sep as list
90 94
91 95 :param obj:
92 96 :param sep:
93 97 :param strip:
94 98 """
95 99 if isinstance(obj, (basestring)):
96 100 lst = obj.split(sep)
97 101 if strip:
98 102 lst = [v.strip() for v in lst]
99 103 return lst
100 104 elif isinstance(obj, (list, tuple)):
101 105 return obj
102 106 elif obj is None:
103 107 return []
104 108 else:
105 109 return [obj]
106 110
107 111
108 112 def convert_line_endings(line, mode):
109 113 """
110 114 Converts a given line "line end" accordingly to given mode
111 115
112 116 Available modes are::
113 117 0 - Unix
114 118 1 - Mac
115 119 2 - DOS
116 120
117 121 :param line: given line to convert
118 122 :param mode: mode to convert to
119 123 :rtype: str
120 124 :return: converted line according to mode
121 125 """
122 126 from string import replace
123 127
124 128 if mode == 0:
125 129 line = replace(line, '\r\n', '\n')
126 130 line = replace(line, '\r', '\n')
127 131 elif mode == 1:
128 132 line = replace(line, '\r\n', '\r')
129 133 line = replace(line, '\n', '\r')
130 134 elif mode == 2:
131 135 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
132 136 return line
133 137
134 138
135 139 def detect_mode(line, default):
136 140 """
137 141 Detects line break for given line, if line break couldn't be found
138 142 given default value is returned
139 143
140 144 :param line: str line
141 145 :param default: default
142 146 :rtype: int
143 147 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
144 148 """
145 149 if line.endswith('\r\n'):
146 150 return 2
147 151 elif line.endswith('\n'):
148 152 return 0
149 153 elif line.endswith('\r'):
150 154 return 1
151 155 else:
152 156 return default
153 157
154 158
155 159 def generate_api_key(username, salt=None):
156 160 """
157 161 Generates unique API key for given username, if salt is not given
158 162 it'll be generated from some random string
159 163
160 164 :param username: username as string
161 165 :param salt: salt to hash generate KEY
162 166 :rtype: str
163 167 :returns: sha1 hash from username+salt
164 168 """
165 169 from tempfile import _RandomNameSequence
166 170 import hashlib
167 171
168 172 if salt is None:
169 173 salt = _RandomNameSequence().next()
170 174
171 175 return hashlib.sha1(username + salt).hexdigest()
172 176
173 177
174 178 def safe_int(val, default=None):
175 179 """
176 180 Returns int() of val if val is not convertable to int use default
177 181 instead
178 182
179 183 :param val:
180 184 :param default:
181 185 """
182 186
183 187 try:
184 188 val = int(val)
185 189 except (ValueError, TypeError):
186 190 val = default
187 191
188 192 return val
189 193
190 194
191 195 def safe_unicode(str_, from_encoding=None):
192 196 """
193 197 safe unicode function. Does few trick to turn str_ into unicode
194 198
195 199 In case of UnicodeDecode error we try to return it with encoding detected
196 200 by chardet library if it fails fallback to unicode with errors replaced
197 201
198 202 :param str_: string to decode
199 203 :rtype: unicode
200 204 :returns: unicode object
201 205 """
202 206 if isinstance(str_, unicode):
203 207 return str_
204 208
205 209 if not from_encoding:
206 210 import rhodecode
207 211 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
208 212 'utf8'), sep=',')
209 213 from_encoding = DEFAULT_ENCODINGS
210 214
211 215 if not isinstance(from_encoding, (list, tuple)):
212 216 from_encoding = [from_encoding]
213 217
214 218 try:
215 219 return unicode(str_)
216 220 except UnicodeDecodeError:
217 221 pass
218 222
219 223 for enc in from_encoding:
220 224 try:
221 225 return unicode(str_, enc)
222 226 except UnicodeDecodeError:
223 227 pass
224 228
225 229 try:
226 230 import chardet
227 231 encoding = chardet.detect(str_)['encoding']
228 232 if encoding is None:
229 233 raise Exception()
230 234 return str_.decode(encoding)
231 235 except (ImportError, UnicodeDecodeError, Exception):
232 236 return unicode(str_, from_encoding[0], 'replace')
233 237
234 238
235 239 def safe_str(unicode_, to_encoding=None):
236 240 """
237 241 safe str function. Does few trick to turn unicode_ into string
238 242
239 243 In case of UnicodeEncodeError we try to return it with encoding detected
240 244 by chardet library if it fails fallback to string with errors replaced
241 245
242 246 :param unicode_: unicode to encode
243 247 :rtype: str
244 248 :returns: str object
245 249 """
246 250
247 251 # if it's not basestr cast to str
248 252 if not isinstance(unicode_, basestring):
249 253 return str(unicode_)
250 254
251 255 if isinstance(unicode_, str):
252 256 return unicode_
253 257
254 258 if not to_encoding:
255 259 import rhodecode
256 260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
257 261 'utf8'), sep=',')
258 262 to_encoding = DEFAULT_ENCODINGS
259 263
260 264 if not isinstance(to_encoding, (list, tuple)):
261 265 to_encoding = [to_encoding]
262 266
263 267 for enc in to_encoding:
264 268 try:
265 269 return unicode_.encode(enc)
266 270 except UnicodeEncodeError:
267 271 pass
268 272
269 273 try:
270 274 import chardet
271 275 encoding = chardet.detect(unicode_)['encoding']
272 276 if encoding is None:
273 277 raise UnicodeEncodeError()
274 278
275 279 return unicode_.encode(encoding)
276 280 except (ImportError, UnicodeEncodeError):
277 281 return unicode_.encode(to_encoding[0], 'replace')
278 282
279 283 return safe_str
280 284
281 285
282 286 def remove_suffix(s, suffix):
283 287 if s.endswith(suffix):
284 288 s = s[:-1 * len(suffix)]
285 289 return s
286 290
287 291
288 292 def remove_prefix(s, prefix):
289 293 if s.startswith(prefix):
290 294 s = s[len(prefix):]
291 295 return s
292 296
293 297
294 298 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
295 299 """
296 300 Custom engine_from_config functions that makes sure we use NullPool for
297 301 file based sqlite databases. This prevents errors on sqlite. This only
298 302 applies to sqlalchemy versions < 0.7.0
299 303
300 304 """
301 305 import sqlalchemy
302 306 from sqlalchemy import engine_from_config as efc
303 307 import logging
304 308
305 309 if int(sqlalchemy.__version__.split('.')[1]) < 7:
306 310
307 311 # This solution should work for sqlalchemy < 0.7.0, and should use
308 312 # proxy=TimerProxy() for execution time profiling
309 313
310 314 from sqlalchemy.pool import NullPool
311 315 url = configuration[prefix + 'url']
312 316
313 317 if url.startswith('sqlite'):
314 318 kwargs.update({'poolclass': NullPool})
315 319 return efc(configuration, prefix, **kwargs)
316 320 else:
317 321 import time
318 322 from sqlalchemy import event
319 323 from sqlalchemy.engine import Engine
320 324
321 325 log = logging.getLogger('sqlalchemy.engine')
322 326 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
323 327 engine = efc(configuration, prefix, **kwargs)
324 328
325 329 def color_sql(sql):
326 330 COLOR_SEQ = "\033[1;%dm"
327 331 COLOR_SQL = YELLOW
328 332 normal = '\x1b[0m'
329 333 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
330 334
331 335 if configuration['debug']:
332 336 #attach events only for debug configuration
333 337
334 338 def before_cursor_execute(conn, cursor, statement,
335 339 parameters, context, executemany):
336 340 context._query_start_time = time.time()
337 341 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
338 342
339 343 def after_cursor_execute(conn, cursor, statement,
340 344 parameters, context, executemany):
341 345 total = time.time() - context._query_start_time
342 346 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
343 347
344 348 event.listen(engine, "before_cursor_execute",
345 349 before_cursor_execute)
346 350 event.listen(engine, "after_cursor_execute",
347 351 after_cursor_execute)
348 352
349 353 return engine
350 354
351 355
352 356 def age(prevdate, show_short_version=False):
353 357 """
354 358 turns a datetime into an age string.
355 359 If show_short_version is True, then it will generate a not so accurate but shorter string,
356 360 example: 2days ago, instead of 2 days and 23 hours ago.
357 361
358 362 :param prevdate: datetime object
359 363 :param show_short_version: if it should aproximate the date and return a shorter string
360 364 :rtype: unicode
361 365 :returns: unicode words describing age
362 366 """
363 367 now = datetime.datetime.now()
364 368 now = now.replace(microsecond=0)
365 369 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
366 370 deltas = {}
367 371 future = False
368 372
369 373 if prevdate > now:
370 374 now, prevdate = prevdate, now
371 375 future = True
372 376
373 377 # Get date parts deltas
374 378 for part in order:
375 379 if future:
376 380 from dateutil import relativedelta
377 381 d = relativedelta.relativedelta(now, prevdate)
378 382 deltas[part] = getattr(d, part + 's')
379 383 else:
380 384 deltas[part] = getattr(now, part) - getattr(prevdate, part)
381 385
382 386 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
383 387 # not 1 hour, -59 minutes and -59 seconds)
384 388 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
385 389 part = order[num]
386 390 carry_part = order[num - 1]
387 391
388 392 if deltas[part] < 0:
389 393 deltas[part] += length
390 394 deltas[carry_part] -= 1
391 395
392 396 # Same thing for days except that the increment depends on the (variable)
393 397 # number of days in the month
394 398 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
395 399 if deltas['day'] < 0:
396 400 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
397 401 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
398 402 deltas['day'] += 29
399 403 else:
400 404 deltas['day'] += month_lengths[prevdate.month - 1]
401 405
402 406 deltas['month'] -= 1
403 407
404 408 if deltas['month'] < 0:
405 409 deltas['month'] += 12
406 410 deltas['year'] -= 1
407 411
408 412 # Format the result
409 413 fmt_funcs = {
410 414 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
411 415 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
412 416 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
413 417 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
414 418 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
415 419 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
416 420 }
417 421
418 422 for i, part in enumerate(order):
419 423 value = deltas[part]
420 424 if value == 0:
421 425 continue
422 426
423 427 if i < 5:
424 428 sub_part = order[i + 1]
425 429 sub_value = deltas[sub_part]
426 430 else:
427 431 sub_value = 0
428 432
429 433 if sub_value == 0 or show_short_version:
430 434 if future:
431 435 return _(u'in %s') % fmt_funcs[part](value)
432 436 else:
433 437 return _(u'%s ago') % fmt_funcs[part](value)
434 438 if future:
435 439 return _(u'in %s and %s') % (fmt_funcs[part](value),
436 440 fmt_funcs[sub_part](sub_value))
437 441 else:
438 442 return _(u'%s and %s ago') % (fmt_funcs[part](value),
439 443 fmt_funcs[sub_part](sub_value))
440 444
441 445 return _(u'just now')
442 446
443 447
444 448 def uri_filter(uri):
445 449 """
446 450 Removes user:password from given url string
447 451
448 452 :param uri:
449 453 :rtype: unicode
450 454 :returns: filtered list of strings
451 455 """
452 456 if not uri:
453 457 return ''
454 458
455 459 proto = ''
456 460
457 461 for pat in ('https://', 'http://'):
458 462 if uri.startswith(pat):
459 463 uri = uri[len(pat):]
460 464 proto = pat
461 465 break
462 466
463 467 # remove passwords and username
464 468 uri = uri[uri.find('@') + 1:]
465 469
466 470 # get the port
467 471 cred_pos = uri.find(':')
468 472 if cred_pos == -1:
469 473 host, port = uri, None
470 474 else:
471 475 host, port = uri[:cred_pos], uri[cred_pos + 1:]
472 476
473 477 return filter(None, [proto, host, port])
474 478
475 479
476 480 def credentials_filter(uri):
477 481 """
478 482 Returns a url with removed credentials
479 483
480 484 :param uri:
481 485 """
482 486
483 487 uri = uri_filter(uri)
484 488 #check if we have port
485 489 if len(uri) > 2 and uri[2]:
486 490 uri[2] = ':' + uri[2]
487 491
488 492 return ''.join(uri)
489 493
490 494
491 495 def get_changeset_safe(repo, rev):
492 496 """
493 497 Safe version of get_changeset if this changeset doesn't exists for a
494 498 repo it returns a Dummy one instead
495 499
496 500 :param repo:
497 501 :param rev:
498 502 """
499 503 from rhodecode.lib.vcs.backends.base import BaseRepository
500 504 from rhodecode.lib.vcs.exceptions import RepositoryError
501 505 from rhodecode.lib.vcs.backends.base import EmptyChangeset
502 506 if not isinstance(repo, BaseRepository):
503 507 raise Exception('You must pass an Repository '
504 508 'object as first argument got %s', type(repo))
505 509
506 510 try:
507 511 cs = repo.get_changeset(rev)
508 512 except RepositoryError:
509 513 cs = EmptyChangeset(requested_revision=rev)
510 514 return cs
511 515
512 516
513 517 def datetime_to_time(dt):
514 518 if dt:
515 519 return time.mktime(dt.timetuple())
516 520
517 521
518 522 def time_to_datetime(tm):
519 523 if tm:
520 524 if isinstance(tm, basestring):
521 525 try:
522 526 tm = float(tm)
523 527 except ValueError:
524 528 return
525 529 return datetime.datetime.fromtimestamp(tm)
526 530
527 531 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
528 532
529 533
530 534 def extract_mentioned_users(s):
531 535 """
532 536 Returns unique usernames from given string s that have @mention
533 537
534 538 :param s: string to get mentions
535 539 """
536 540 usrs = set()
537 541 for username in re.findall(MENTIONS_REGEX, s):
538 542 usrs.add(username)
539 543
540 544 return sorted(list(usrs), key=lambda k: k.lower())
541 545
542 546
543 547 class AttributeDict(dict):
544 548 def __getattr__(self, attr):
545 549 return self.get(attr, None)
546 550 __setattr__ = dict.__setitem__
547 551 __delattr__ = dict.__delitem__
548 552
549 553
550 554 def fix_PATH(os_=None):
551 555 """
552 556 Get current active python path, and append it to PATH variable to fix issues
553 557 of subprocess calls and different python versions
554 558 """
555 import sys
556 559 if os_ is None:
557 560 import os
558 561 else:
559 562 os = os_
560 563
561 564 cur_path = os.path.split(sys.executable)[0]
562 565 if not os.environ['PATH'].startswith(cur_path):
563 566 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
564 567
565 568
566 569 def obfuscate_url_pw(engine):
567 570 _url = engine or ''
568 571 from sqlalchemy.engine import url as sa_url
569 572 try:
570 573 _url = sa_url.make_url(engine)
571 574 if _url.password:
572 575 _url.password = 'XXXXX'
573 576 except:
574 577 pass
575 578 return str(_url)
576 579
577 580
578 581 def get_server_url(environ):
579 582 req = webob.Request(environ)
580 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 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.base
4 4 ~~~~~~~~~~~~~~~~~
5 5
6 6 Base for all available scm backends
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import datetime
13 13 from itertools import chain
14 14 from rhodecode.lib.vcs.utils import author_name, author_email
15 15 from rhodecode.lib.vcs.utils.lazy import LazyProperty
16 16 from rhodecode.lib.vcs.utils.helpers import get_dict_for_attrs
17 17 from rhodecode.lib.vcs.conf import settings
18 18
19 19 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
20 20 NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError, \
21 21 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, \
22 22 RepositoryError
23 23
24 24
25 25 class BaseRepository(object):
26 26 """
27 27 Base Repository for final backends
28 28
29 29 **Attributes**
30 30
31 31 ``DEFAULT_BRANCH_NAME``
32 32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33 33
34 34 ``scm``
35 35 alias of scm, i.e. *git* or *hg*
36 36
37 37 ``repo``
38 38 object from external api
39 39
40 40 ``revisions``
41 41 list of all available revisions' ids, in ascending order
42 42
43 43 ``changesets``
44 44 storage dict caching returned changesets
45 45
46 46 ``path``
47 47 absolute path to the repository
48 48
49 49 ``branches``
50 50 branches as list of changesets
51 51
52 52 ``tags``
53 53 tags as list of changesets
54 54 """
55 55 scm = None
56 56 DEFAULT_BRANCH_NAME = None
57 57 EMPTY_CHANGESET = '0' * 40
58 58
59 59 def __init__(self, repo_path, create=False, **kwargs):
60 60 """
61 61 Initializes repository. Raises RepositoryError if repository could
62 62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 63 exists and ``create`` is set to True.
64 64
65 65 :param repo_path: local path of the repository
66 66 :param create=False: if set to True, would try to craete repository.
67 67 :param src_url=None: if set, should be proper url from which repository
68 68 would be cloned; requires ``create`` parameter to be set to True -
69 69 raises RepositoryError if src_url is set and create evaluates to
70 70 False
71 71 """
72 72 raise NotImplementedError
73 73
74 74 def __str__(self):
75 75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76 76
77 77 def __repr__(self):
78 78 return self.__str__()
79 79
80 80 def __len__(self):
81 81 return self.count()
82 82
83 83 @LazyProperty
84 84 def alias(self):
85 85 for k, v in settings.BACKENDS.items():
86 86 if v.split('.')[-1] == str(self.__class__.__name__):
87 87 return k
88 88
89 89 @LazyProperty
90 90 def name(self):
91 91 raise NotImplementedError
92 92
93 93 @LazyProperty
94 94 def owner(self):
95 95 raise NotImplementedError
96 96
97 97 @LazyProperty
98 98 def description(self):
99 99 raise NotImplementedError
100 100
101 101 @LazyProperty
102 102 def size(self):
103 103 """
104 104 Returns combined size in bytes for all repository files
105 105 """
106 106
107 107 size = 0
108 108 try:
109 109 tip = self.get_changeset()
110 110 for topnode, dirs, files in tip.walk('/'):
111 111 for f in files:
112 112 size += tip.get_file_size(f.path)
113 113 for dir in dirs:
114 114 for f in files:
115 115 size += tip.get_file_size(f.path)
116 116
117 117 except RepositoryError, e:
118 118 pass
119 119 return size
120 120
121 121 def is_valid(self):
122 122 """
123 123 Validates repository.
124 124 """
125 125 raise NotImplementedError
126 126
127 127 def get_last_change(self):
128 128 self.get_changesets()
129 129
130 130 #==========================================================================
131 131 # CHANGESETS
132 132 #==========================================================================
133 133
134 134 def get_changeset(self, revision=None):
135 135 """
136 136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 137 recent changeset is returned.
138 138
139 139 :raises ``EmptyRepositoryError``: if there are no revisions
140 140 """
141 141 raise NotImplementedError
142 142
143 143 def __iter__(self):
144 144 """
145 145 Allows Repository objects to be iterated.
146 146
147 147 *Requires* implementation of ``__getitem__`` method.
148 148 """
149 149 for revision in self.revisions:
150 150 yield self.get_changeset(revision)
151 151
152 152 def get_changesets(self, start=None, end=None, start_date=None,
153 153 end_date=None, branch_name=None, reverse=False):
154 154 """
155 155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 156 not inclusive This should behave just like a list, ie. end is not
157 157 inclusive
158 158
159 159 :param start: None or str
160 160 :param end: None or str
161 161 :param start_date:
162 162 :param end_date:
163 163 :param branch_name:
164 164 :param reversed:
165 165 """
166 166 raise NotImplementedError
167 167
168 168 def __getslice__(self, i, j):
169 169 """
170 170 Returns a iterator of sliced repository
171 171 """
172 172 for rev in self.revisions[i:j]:
173 173 yield self.get_changeset(rev)
174 174
175 175 def __getitem__(self, key):
176 176 return self.get_changeset(key)
177 177
178 178 def count(self):
179 179 return len(self.revisions)
180 180
181 181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 182 """
183 183 Creates and returns a tag for the given ``revision``.
184 184
185 185 :param name: name for new tag
186 186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 187 :param revision: changeset id for which new tag would be created
188 188 :param message: message of the tag's commit
189 189 :param date: date of tag's commit
190 190
191 191 :raises TagAlreadyExistError: if tag with same name already exists
192 192 """
193 193 raise NotImplementedError
194 194
195 195 def remove_tag(self, name, user, message=None, date=None):
196 196 """
197 197 Removes tag with the given ``name``.
198 198
199 199 :param name: name of the tag to be removed
200 200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 201 :param message: message of the tag's removal commit
202 202 :param date: date of tag's removal commit
203 203
204 204 :raises TagDoesNotExistError: if tag with given name does not exists
205 205 """
206 206 raise NotImplementedError
207 207
208 208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 209 context=3):
210 210 """
211 211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 212 ``rev2`` since ``rev1``.
213 213
214 214 :param rev1: Entry point from which diff is shown. Can be
215 215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 216 the changes since empty state of the repository until ``rev2``
217 217 :param rev2: Until which revision changes should be shown.
218 218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 219 changes. Defaults to ``False``.
220 220 :param context: How many lines before/after changed lines should be
221 221 shown. Defaults to ``3``.
222 222 """
223 223 raise NotImplementedError
224 224
225 225 # ========== #
226 226 # COMMIT API #
227 227 # ========== #
228 228
229 229 @LazyProperty
230 230 def in_memory_changeset(self):
231 231 """
232 232 Returns ``InMemoryChangeset`` object for this repository.
233 233 """
234 234 raise NotImplementedError
235 235
236 236 def add(self, filenode, **kwargs):
237 237 """
238 238 Commit api function that will add given ``FileNode`` into this
239 239 repository.
240 240
241 241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 242 already in repository
243 243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 244 *added*
245 245 """
246 246 raise NotImplementedError
247 247
248 248 def remove(self, filenode, **kwargs):
249 249 """
250 250 Commit api function that will remove given ``FileNode`` into this
251 251 repository.
252 252
253 253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 255 """
256 256 raise NotImplementedError
257 257
258 258 def commit(self, message, **kwargs):
259 259 """
260 260 Persists current changes made on this repository and returns newly
261 261 created changeset.
262 262
263 263 :raises ``NothingChangedError``: if no changes has been made
264 264 """
265 265 raise NotImplementedError
266 266
267 267 def get_state(self):
268 268 """
269 269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 270 containing ``FileNode`` objects.
271 271 """
272 272 raise NotImplementedError
273 273
274 274 def get_config_value(self, section, name, config_file=None):
275 275 """
276 276 Returns configuration value for a given [``section``] and ``name``.
277 277
278 278 :param section: Section we want to retrieve value from
279 279 :param name: Name of configuration we want to retrieve
280 280 :param config_file: A path to file which should be used to retrieve
281 281 configuration from (might also be a list of file paths)
282 282 """
283 283 raise NotImplementedError
284 284
285 285 def get_user_name(self, config_file=None):
286 286 """
287 287 Returns user's name from global configuration file.
288 288
289 289 :param config_file: A path to file which should be used to retrieve
290 290 configuration from (might also be a list of file paths)
291 291 """
292 292 raise NotImplementedError
293 293
294 294 def get_user_email(self, config_file=None):
295 295 """
296 296 Returns user's email from global configuration file.
297 297
298 298 :param config_file: A path to file which should be used to retrieve
299 299 configuration from (might also be a list of file paths)
300 300 """
301 301 raise NotImplementedError
302 302
303 303 # =========== #
304 304 # WORKDIR API #
305 305 # =========== #
306 306
307 307 @LazyProperty
308 308 def workdir(self):
309 309 """
310 310 Returns ``Workdir`` instance for this repository.
311 311 """
312 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 315 class BaseChangeset(object):
337 316 """
338 317 Each backend should implement it's changeset representation.
339 318
340 319 **Attributes**
341 320
342 321 ``repository``
343 322 repository object within which changeset exists
344 323
345 324 ``id``
346 325 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
347 326
348 327 ``raw_id``
349 328 raw changeset representation (i.e. full 40 length sha for git
350 329 backend)
351 330
352 331 ``short_id``
353 332 shortened (if apply) version of ``raw_id``; it would be simple
354 333 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
355 334 as ``raw_id`` for subversion
356 335
357 336 ``revision``
358 337 revision number as integer
359 338
360 339 ``files``
361 340 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
362 341
363 342 ``dirs``
364 343 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
365 344
366 345 ``nodes``
367 346 combined list of ``Node`` objects
368 347
369 348 ``author``
370 349 author of the changeset, as unicode
371 350
372 351 ``message``
373 352 message of the changeset, as unicode
374 353
375 354 ``parents``
376 355 list of parent changesets
377 356
378 357 ``last``
379 358 ``True`` if this is last changeset in repository, ``False``
380 359 otherwise; trying to access this attribute while there is no
381 360 changesets would raise ``EmptyRepositoryError``
382 361 """
383 362 def __str__(self):
384 363 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
385 364 self.short_id)
386 365
387 366 def __repr__(self):
388 367 return self.__str__()
389 368
390 369 def __unicode__(self):
391 370 return u'%s:%s' % (self.revision, self.short_id)
392 371
393 372 def __eq__(self, other):
394 373 return self.raw_id == other.raw_id
395 374
396 375 def __json__(self):
397 376 return dict(
398 377 short_id=self.short_id,
399 378 raw_id=self.raw_id,
400 379 revision=self.revision,
401 380 message=self.message,
402 381 date=self.date,
403 382 author=self.author,
404 383 )
405 384
406 385 @LazyProperty
407 386 def last(self):
408 387 if self.repository is None:
409 388 raise ChangesetError("Cannot check if it's most recent revision")
410 389 return self.raw_id == self.repository.revisions[-1]
411 390
412 391 @LazyProperty
413 392 def parents(self):
414 393 """
415 394 Returns list of parents changesets.
416 395 """
417 396 raise NotImplementedError
418 397
419 398 @LazyProperty
420 399 def children(self):
421 400 """
422 401 Returns list of children changesets.
423 402 """
424 403 raise NotImplementedError
425 404
426 405 @LazyProperty
427 406 def id(self):
428 407 """
429 408 Returns string identifying this changeset.
430 409 """
431 410 raise NotImplementedError
432 411
433 412 @LazyProperty
434 413 def raw_id(self):
435 414 """
436 415 Returns raw string identifying this changeset.
437 416 """
438 417 raise NotImplementedError
439 418
440 419 @LazyProperty
441 420 def short_id(self):
442 421 """
443 422 Returns shortened version of ``raw_id`` attribute, as string,
444 423 identifying this changeset, useful for web representation.
445 424 """
446 425 raise NotImplementedError
447 426
448 427 @LazyProperty
449 428 def revision(self):
450 429 """
451 430 Returns integer identifying this changeset.
452 431
453 432 """
454 433 raise NotImplementedError
455 434
456 435 @LazyProperty
457 436 def committer(self):
458 437 """
459 438 Returns Committer for given commit
460 439 """
461 440
462 441 raise NotImplementedError
463 442
464 443 @LazyProperty
465 444 def committer_name(self):
466 445 """
467 446 Returns Author name for given commit
468 447 """
469 448
470 449 return author_name(self.committer)
471 450
472 451 @LazyProperty
473 452 def committer_email(self):
474 453 """
475 454 Returns Author email address for given commit
476 455 """
477 456
478 457 return author_email(self.committer)
479 458
480 459 @LazyProperty
481 460 def author(self):
482 461 """
483 462 Returns Author for given commit
484 463 """
485 464
486 465 raise NotImplementedError
487 466
488 467 @LazyProperty
489 468 def author_name(self):
490 469 """
491 470 Returns Author name for given commit
492 471 """
493 472
494 473 return author_name(self.author)
495 474
496 475 @LazyProperty
497 476 def author_email(self):
498 477 """
499 478 Returns Author email address for given commit
500 479 """
501 480
502 481 return author_email(self.author)
503 482
504 483 def get_file_mode(self, path):
505 484 """
506 485 Returns stat mode of the file at the given ``path``.
507 486 """
508 487 raise NotImplementedError
509 488
510 489 def get_file_content(self, path):
511 490 """
512 491 Returns content of the file at the given ``path``.
513 492 """
514 493 raise NotImplementedError
515 494
516 495 def get_file_size(self, path):
517 496 """
518 497 Returns size of the file at the given ``path``.
519 498 """
520 499 raise NotImplementedError
521 500
522 501 def get_file_changeset(self, path):
523 502 """
524 503 Returns last commit of the file at the given ``path``.
525 504 """
526 505 raise NotImplementedError
527 506
528 507 def get_file_history(self, path):
529 508 """
530 509 Returns history of file as reversed list of ``Changeset`` objects for
531 510 which file at given ``path`` has been modified.
532 511 """
533 512 raise NotImplementedError
534 513
535 514 def get_nodes(self, path):
536 515 """
537 516 Returns combined ``DirNode`` and ``FileNode`` objects list representing
538 517 state of changeset at the given ``path``.
539 518
540 519 :raises ``ChangesetError``: if node at the given ``path`` is not
541 520 instance of ``DirNode``
542 521 """
543 522 raise NotImplementedError
544 523
545 524 def get_node(self, path):
546 525 """
547 526 Returns ``Node`` object from the given ``path``.
548 527
549 528 :raises ``NodeDoesNotExistError``: if there is no node at the given
550 529 ``path``
551 530 """
552 531 raise NotImplementedError
553 532
554 533 def fill_archive(self, stream=None, kind='tgz', prefix=None):
555 534 """
556 535 Fills up given stream.
557 536
558 537 :param stream: file like object.
559 538 :param kind: one of following: ``zip``, ``tar``, ``tgz``
560 539 or ``tbz2``. Default: ``tgz``.
561 540 :param prefix: name of root directory in archive.
562 541 Default is repository name and changeset's raw_id joined with dash.
563 542
564 543 repo-tip.<kind>
565 544 """
566 545
567 546 raise NotImplementedError
568 547
569 548 def get_chunked_archive(self, **kwargs):
570 549 """
571 550 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
572 551
573 552 :param chunk_size: extra parameter which controls size of returned
574 553 chunks. Default:8k.
575 554 """
576 555
577 556 chunk_size = kwargs.pop('chunk_size', 8192)
578 557 stream = kwargs.get('stream')
579 558 self.fill_archive(**kwargs)
580 559 while True:
581 560 data = stream.read(chunk_size)
582 561 if not data:
583 562 break
584 563 yield data
585 564
586 565 @LazyProperty
587 566 def root(self):
588 567 """
589 568 Returns ``RootNode`` object for this changeset.
590 569 """
591 570 return self.get_node('')
592 571
593 572 def next(self, branch=None):
594 573 """
595 574 Returns next changeset from current, if branch is gives it will return
596 575 next changeset belonging to this branch
597 576
598 577 :param branch: show changesets within the given named branch
599 578 """
600 579 raise NotImplementedError
601 580
602 581 def prev(self, branch=None):
603 582 """
604 583 Returns previous changeset from current, if branch is gives it will
605 584 return previous changeset belonging to this branch
606 585
607 586 :param branch: show changesets within the given named branch
608 587 """
609 588 raise NotImplementedError
610 589
611 590 @LazyProperty
612 591 def added(self):
613 592 """
614 593 Returns list of added ``FileNode`` objects.
615 594 """
616 595 raise NotImplementedError
617 596
618 597 @LazyProperty
619 598 def changed(self):
620 599 """
621 600 Returns list of modified ``FileNode`` objects.
622 601 """
623 602 raise NotImplementedError
624 603
625 604 @LazyProperty
626 605 def removed(self):
627 606 """
628 607 Returns list of removed ``FileNode`` objects.
629 608 """
630 609 raise NotImplementedError
631 610
632 611 @LazyProperty
633 612 def size(self):
634 613 """
635 614 Returns total number of bytes from contents of all filenodes.
636 615 """
637 616 return sum((node.size for node in self.get_filenodes_generator()))
638 617
639 618 def walk(self, topurl=''):
640 619 """
641 620 Similar to os.walk method. Insted of filesystem it walks through
642 621 changeset starting at given ``topurl``. Returns generator of tuples
643 622 (topnode, dirnodes, filenodes).
644 623 """
645 624 topnode = self.get_node(topurl)
646 625 yield (topnode, topnode.dirs, topnode.files)
647 626 for dirnode in topnode.dirs:
648 627 for tup in self.walk(dirnode.path):
649 628 yield tup
650 629
651 630 def get_filenodes_generator(self):
652 631 """
653 632 Returns generator that yields *all* file nodes.
654 633 """
655 634 for topnode, dirs, files in self.walk():
656 635 for node in files:
657 636 yield node
658 637
659 638 def as_dict(self):
660 639 """
661 640 Returns dictionary with changeset's attributes and their values.
662 641 """
663 642 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
664 643 'revision', 'date', 'message'])
665 644 data['author'] = {'name': self.author_name, 'email': self.author_email}
666 645 data['added'] = [node.path for node in self.added]
667 646 data['changed'] = [node.path for node in self.changed]
668 647 data['removed'] = [node.path for node in self.removed]
669 648 return data
670 649
671 650
672 651 class BaseWorkdir(object):
673 652 """
674 653 Working directory representation of single repository.
675 654
676 655 :attribute: repository: repository object of working directory
677 656 """
678 657
679 658 def __init__(self, repository):
680 659 self.repository = repository
681 660
682 661 def get_branch(self):
683 662 """
684 663 Returns name of current branch.
685 664 """
686 665 raise NotImplementedError
687 666
688 667 def get_changeset(self):
689 668 """
690 669 Returns current changeset.
691 670 """
692 671 raise NotImplementedError
693 672
694 673 def get_added(self):
695 674 """
696 675 Returns list of ``FileNode`` objects marked as *new* in working
697 676 directory.
698 677 """
699 678 raise NotImplementedError
700 679
701 680 def get_changed(self):
702 681 """
703 682 Returns list of ``FileNode`` objects *changed* in working directory.
704 683 """
705 684 raise NotImplementedError
706 685
707 686 def get_removed(self):
708 687 """
709 688 Returns list of ``RemovedFileNode`` objects marked as *removed* in
710 689 working directory.
711 690 """
712 691 raise NotImplementedError
713 692
714 693 def get_untracked(self):
715 694 """
716 695 Returns list of ``FileNode`` objects which are present within working
717 696 directory however are not tracked by repository.
718 697 """
719 698 raise NotImplementedError
720 699
721 700 def get_status(self):
722 701 """
723 702 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
724 703 lists.
725 704 """
726 705 raise NotImplementedError
727 706
728 707 def commit(self, message, **kwargs):
729 708 """
730 709 Commits local (from working directory) changes and returns newly
731 710 created
732 711 ``Changeset``. Updates repository's ``revisions`` list.
733 712
734 713 :raises ``CommitError``: if any error occurs while committing
735 714 """
736 715 raise NotImplementedError
737 716
738 717 def update(self, revision=None):
739 718 """
740 719 Fetches content of the given revision and populates it within working
741 720 directory.
742 721 """
743 722 raise NotImplementedError
744 723
745 724 def checkout_branch(self, branch=None):
746 725 """
747 726 Checks out ``branch`` or the backend's default branch.
748 727
749 728 Raises ``BranchDoesNotExistError`` if the branch does not exist.
750 729 """
751 730 raise NotImplementedError
752 731
753 732
754 733 class BaseInMemoryChangeset(object):
755 734 """
756 735 Represents differences between repository's state (most recent head) and
757 736 changes made *in place*.
758 737
759 738 **Attributes**
760 739
761 740 ``repository``
762 741 repository object for this in-memory-changeset
763 742
764 743 ``added``
765 744 list of ``FileNode`` objects marked as *added*
766 745
767 746 ``changed``
768 747 list of ``FileNode`` objects marked as *changed*
769 748
770 749 ``removed``
771 750 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
772 751 *removed*
773 752
774 753 ``parents``
775 754 list of ``Changeset`` representing parents of in-memory changeset.
776 755 Should always be 2-element sequence.
777 756
778 757 """
779 758
780 759 def __init__(self, repository):
781 760 self.repository = repository
782 761 self.added = []
783 762 self.changed = []
784 763 self.removed = []
785 764 self.parents = []
786 765
787 766 def add(self, *filenodes):
788 767 """
789 768 Marks given ``FileNode`` objects as *to be committed*.
790 769
791 770 :raises ``NodeAlreadyExistsError``: if node with same path exists at
792 771 latest changeset
793 772 :raises ``NodeAlreadyAddedError``: if node with same path is already
794 773 marked as *added*
795 774 """
796 775 # Check if not already marked as *added* first
797 776 for node in filenodes:
798 777 if node.path in (n.path for n in self.added):
799 778 raise NodeAlreadyAddedError("Such FileNode %s is already "
800 779 "marked for addition" % node.path)
801 780 for node in filenodes:
802 781 self.added.append(node)
803 782
804 783 def change(self, *filenodes):
805 784 """
806 785 Marks given ``FileNode`` objects to be *changed* in next commit.
807 786
808 787 :raises ``EmptyRepositoryError``: if there are no changesets yet
809 788 :raises ``NodeAlreadyExistsError``: if node with same path is already
810 789 marked to be *changed*
811 790 :raises ``NodeAlreadyRemovedError``: if node with same path is already
812 791 marked to be *removed*
813 792 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
814 793 changeset
815 794 :raises ``NodeNotChangedError``: if node hasn't really be changed
816 795 """
817 796 for node in filenodes:
818 797 if node.path in (n.path for n in self.removed):
819 798 raise NodeAlreadyRemovedError("Node at %s is already marked "
820 799 "as removed" % node.path)
821 800 try:
822 801 self.repository.get_changeset()
823 802 except EmptyRepositoryError:
824 803 raise EmptyRepositoryError("Nothing to change - try to *add* new "
825 804 "nodes rather than changing them")
826 805 for node in filenodes:
827 806 if node.path in (n.path for n in self.changed):
828 807 raise NodeAlreadyChangedError("Node at '%s' is already "
829 808 "marked as changed" % node.path)
830 809 self.changed.append(node)
831 810
832 811 def remove(self, *filenodes):
833 812 """
834 813 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
835 814 *removed* in next commit.
836 815
837 816 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
838 817 be *removed*
839 818 :raises ``NodeAlreadyChangedError``: if node has been already marked to
840 819 be *changed*
841 820 """
842 821 for node in filenodes:
843 822 if node.path in (n.path for n in self.removed):
844 823 raise NodeAlreadyRemovedError("Node is already marked to "
845 824 "for removal at %s" % node.path)
846 825 if node.path in (n.path for n in self.changed):
847 826 raise NodeAlreadyChangedError("Node is already marked to "
848 827 "be changed at %s" % node.path)
849 828 # We only mark node as *removed* - real removal is done by
850 829 # commit method
851 830 self.removed.append(node)
852 831
853 832 def reset(self):
854 833 """
855 834 Resets this instance to initial state (cleans ``added``, ``changed``
856 835 and ``removed`` lists).
857 836 """
858 837 self.added = []
859 838 self.changed = []
860 839 self.removed = []
861 840 self.parents = []
862 841
863 842 def get_ipaths(self):
864 843 """
865 844 Returns generator of paths from nodes marked as added, changed or
866 845 removed.
867 846 """
868 847 for node in chain(self.added, self.changed, self.removed):
869 848 yield node.path
870 849
871 850 def get_paths(self):
872 851 """
873 852 Returns list of paths from nodes marked as added, changed or removed.
874 853 """
875 854 return list(self.get_ipaths())
876 855
877 856 def check_integrity(self, parents=None):
878 857 """
879 858 Checks in-memory changeset's integrity. Also, sets parents if not
880 859 already set.
881 860
882 861 :raises CommitError: if any error occurs (i.e.
883 862 ``NodeDoesNotExistError``).
884 863 """
885 864 if not self.parents:
886 865 parents = parents or []
887 866 if len(parents) == 0:
888 867 try:
889 868 parents = [self.repository.get_changeset(), None]
890 869 except EmptyRepositoryError:
891 870 parents = [None, None]
892 871 elif len(parents) == 1:
893 872 parents += [None]
894 873 self.parents = parents
895 874
896 875 # Local parents, only if not None
897 876 parents = [p for p in self.parents if p]
898 877
899 878 # Check nodes marked as added
900 879 for p in parents:
901 880 for node in self.added:
902 881 try:
903 882 p.get_node(node.path)
904 883 except NodeDoesNotExistError:
905 884 pass
906 885 else:
907 886 raise NodeAlreadyExistsError("Node at %s already exists "
908 887 "at %s" % (node.path, p))
909 888
910 889 # Check nodes marked as changed
911 890 missing = set(self.changed)
912 891 not_changed = set(self.changed)
913 892 if self.changed and not parents:
914 893 raise NodeDoesNotExistError(str(self.changed[0].path))
915 894 for p in parents:
916 895 for node in self.changed:
917 896 try:
918 897 old = p.get_node(node.path)
919 898 missing.remove(node)
920 899 if old.content != node.content:
921 900 not_changed.remove(node)
922 901 except NodeDoesNotExistError:
923 902 pass
924 903 if self.changed and missing:
925 904 raise NodeDoesNotExistError("Node at %s is missing "
926 905 "(parents: %s)" % (node.path, parents))
927 906
928 907 if self.changed and not_changed:
929 908 raise NodeNotChangedError("Node at %s wasn't actually changed "
930 909 "since parents' changesets: %s" % (not_changed.pop().path,
931 910 parents)
932 911 )
933 912
934 913 # Check nodes marked as removed
935 914 if self.removed and not parents:
936 915 raise NodeDoesNotExistError("Cannot remove node at %s as there "
937 916 "were no parents specified" % self.removed[0].path)
938 917 really_removed = set()
939 918 for p in parents:
940 919 for node in self.removed:
941 920 try:
942 921 p.get_node(node.path)
943 922 really_removed.add(node)
944 923 except ChangesetError:
945 924 pass
946 925 not_removed = set(self.removed) - really_removed
947 926 if not_removed:
948 927 raise NodeDoesNotExistError("Cannot remove node at %s from "
949 928 "following parents: %s" % (not_removed[0], parents))
950 929
951 930 def commit(self, message, author, parents=None, branch=None, date=None,
952 931 **kwargs):
953 932 """
954 933 Performs in-memory commit (doesn't check workdir in any way) and
955 934 returns newly created ``Changeset``. Updates repository's
956 935 ``revisions``.
957 936
958 937 .. note::
959 938 While overriding this method each backend's should call
960 939 ``self.check_integrity(parents)`` in the first place.
961 940
962 941 :param message: message of the commit
963 942 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
964 943 :param parents: single parent or sequence of parents from which commit
965 944 would be derieved
966 945 :param date: ``datetime.datetime`` instance. Defaults to
967 946 ``datetime.datetime.now()``.
968 947 :param branch: branch name, as string. If none given, default backend's
969 948 branch would be used.
970 949
971 950 :raises ``CommitError``: if any error occurs while committing
972 951 """
973 952 raise NotImplementedError
974 953
975 954
976 955 class EmptyChangeset(BaseChangeset):
977 956 """
978 957 An dummy empty changeset. It's possible to pass hash when creating
979 958 an EmptyChangeset
980 959 """
981 960
982 961 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
983 962 alias=None, revision=-1, message='', author='', date=None):
984 963 self._empty_cs = cs
985 964 self.revision = revision
986 965 self.message = message
987 966 self.author = author
988 967 self.date = date or datetime.datetime.fromtimestamp(0)
989 968 self.repository = repo
990 969 self.requested_revision = requested_revision
991 970 self.alias = alias
992 971
993 972 @LazyProperty
994 973 def raw_id(self):
995 974 """
996 975 Returns raw string identifying this changeset, useful for web
997 976 representation.
998 977 """
999 978
1000 979 return self._empty_cs
1001 980
1002 981 @LazyProperty
1003 982 def branch(self):
1004 983 from rhodecode.lib.vcs.backends import get_backend
1005 984 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1006 985
1007 986 @LazyProperty
1008 987 def short_id(self):
1009 988 return self.raw_id[:12]
1010 989
1011 990 def get_file_changeset(self, path):
1012 991 return self
1013 992
1014 993 def get_file_content(self, path):
1015 994 return u''
1016 995
1017 996 def get_file_size(self, path):
1018 997 return 0
@@ -1,698 +1,691 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git
4 4 ~~~~~~~~~~~~~~~~
5 5
6 6 Git backend implementation.
7 7
8 8 :created_on: Apr 8, 2010
9 9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 10 """
11 11
12 12 import os
13 13 import re
14 14 import time
15 15 import posixpath
16 16 import logging
17 17 import traceback
18 18 import urllib
19 19 import urllib2
20 20 from dulwich.repo import Repo, NotGitRepository
21 21 from dulwich.objects import Tag
22 22 from string import Template
23 23
24 24 import rhodecode
25 25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 35 from rhodecode.lib.vcs.utils.paths import abspath
36 36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 37 from .workdir import GitWorkdir
38 38 from .changeset import GitChangeset
39 39 from .inmemory import GitInMemoryChangeset
40 40 from .config import ConfigFile
41 41 from rhodecode.lib import subprocessio
42 42
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class GitRepository(BaseRepository):
48 48 """
49 49 Git repository backend.
50 50 """
51 51 DEFAULT_BRANCH_NAME = 'master'
52 52 scm = 'git'
53 53
54 54 def __init__(self, repo_path, create=False, src_url=None,
55 55 update_after_clone=False, bare=False):
56 56
57 57 self.path = abspath(repo_path)
58 58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 59 self.bare = repo.bare
60 60
61 61 self._config_files = [
62 62 bare and abspath(self.path, 'config')
63 63 or abspath(self.path, '.git', 'config'),
64 64 abspath(get_user_home(), '.gitconfig'),
65 65 ]
66 66
67 67 @ThreadLocalLazyProperty
68 68 def _repo(self):
69 repo = 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
69 return Repo(self.path)
77 70
78 71 @property
79 72 def head(self):
80 73 try:
81 74 return self._repo.head()
82 75 except KeyError:
83 76 return None
84 77
85 78 @LazyProperty
86 79 def revisions(self):
87 80 """
88 81 Returns list of revisions' ids, in ascending order. Being lazy
89 82 attribute allows external tools to inject shas from cache.
90 83 """
91 84 return self._get_all_revisions()
92 85
93 86 @classmethod
94 87 def _run_git_command(cls, cmd, **opts):
95 88 """
96 89 Runs given ``cmd`` as git command and returns tuple
97 90 (stdout, stderr).
98 91
99 92 :param cmd: git command to be executed
100 93 :param opts: env options to pass into Subprocess command
101 94 """
102 95
103 96 if '_bare' in opts:
104 97 _copts = []
105 98 del opts['_bare']
106 99 else:
107 100 _copts = ['-c', 'core.quotepath=false', ]
108 101 safe_call = False
109 102 if '_safe' in opts:
110 103 #no exc on failure
111 104 del opts['_safe']
112 105 safe_call = True
113 106
114 107 _str_cmd = False
115 108 if isinstance(cmd, basestring):
116 109 cmd = [cmd]
117 110 _str_cmd = True
118 111
119 112 gitenv = os.environ
120 113 # need to clean fix GIT_DIR !
121 114 if 'GIT_DIR' in gitenv:
122 115 del gitenv['GIT_DIR']
123 116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
124 117
125 118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
126 119 cmd = [_git_path] + _copts + cmd
127 120 if _str_cmd:
128 121 cmd = ' '.join(cmd)
129 122 try:
130 123 _opts = dict(
131 124 env=gitenv,
132 125 shell=False,
133 126 )
134 127 _opts.update(opts)
135 128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
136 129 except (EnvironmentError, OSError), err:
137 130 tb_err = ("Couldn't run git command (%s).\n"
138 131 "Original error was:%s\n" % (cmd, err))
139 132 log.error(tb_err)
140 133 if safe_call:
141 134 return '', err
142 135 else:
143 136 raise RepositoryError(tb_err)
144 137
145 138 return ''.join(p.output), ''.join(p.error)
146 139
147 140 def run_git_command(self, cmd):
148 141 opts = {}
149 142 if os.path.isdir(self.path):
150 143 opts['cwd'] = self.path
151 144 return self._run_git_command(cmd, **opts)
152 145
153 146 @classmethod
154 147 def _check_url(cls, url):
155 148 """
156 149 Functon will check given url and try to verify if it's a valid
157 150 link. Sometimes it may happened that mercurial will issue basic
158 151 auth request that can cause whole API to hang when used from python
159 152 or other external calls.
160 153
161 154 On failures it'll raise urllib2.HTTPError
162 155 """
163 156 from mercurial.util import url as Url
164 157
165 158 # those authnadlers are patched for python 2.6.5 bug an
166 159 # infinit looping when given invalid resources
167 160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
168 161
169 162 # check first if it's not an local url
170 163 if os.path.isdir(url) or url.startswith('file:'):
171 164 return True
172 165
173 166 if('+' in url[:url.find('://')]):
174 167 url = url[url.find('+') + 1:]
175 168
176 169 handlers = []
177 170 test_uri, authinfo = Url(url).authinfo()
178 171 if not test_uri.endswith('info/refs'):
179 172 test_uri = test_uri.rstrip('/') + '/info/refs'
180 173 if authinfo:
181 174 #create a password manager
182 175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
183 176 passmgr.add_password(*authinfo)
184 177
185 178 handlers.extend((httpbasicauthhandler(passmgr),
186 179 httpdigestauthhandler(passmgr)))
187 180
188 181 o = urllib2.build_opener(*handlers)
189 182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
190 183
191 184 q = {"service": 'git-upload-pack'}
192 185 qs = '?%s' % urllib.urlencode(q)
193 186 cu = "%s%s" % (test_uri, qs)
194 187 req = urllib2.Request(cu, None, {})
195 188
196 189 try:
197 190 resp = o.open(req)
198 191 return resp.code == 200
199 192 except Exception, e:
200 193 # means it cannot be cloned
201 194 raise urllib2.URLError("[%s] %s" % (url, e))
202 195
203 196 def _get_repo(self, create, src_url=None, update_after_clone=False,
204 197 bare=False):
205 198 if create and os.path.exists(self.path):
206 199 raise RepositoryError("Location already exist")
207 200 if src_url and not create:
208 201 raise RepositoryError("Create should be set to True if src_url is "
209 202 "given (clone operation creates repository)")
210 203 try:
211 204 if create and src_url:
212 205 GitRepository._check_url(src_url)
213 206 self.clone(src_url, update_after_clone, bare)
214 207 return Repo(self.path)
215 208 elif create:
216 209 os.mkdir(self.path)
217 210 if bare:
218 211 return Repo.init_bare(self.path)
219 212 else:
220 213 return Repo.init(self.path)
221 214 else:
222 215 return self._repo
223 216 except (NotGitRepository, OSError), err:
224 217 raise RepositoryError(err)
225 218
226 219 def _get_all_revisions(self):
227 220 # we must check if this repo is not empty, since later command
228 221 # fails if it is. And it's cheaper to ask than throw the subprocess
229 222 # errors
230 223 try:
231 224 self._repo.head()
232 225 except KeyError:
233 226 return []
234 227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
235 228 '--all').strip()
236 229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
237 230 try:
238 231 so, se = self.run_git_command(cmd)
239 232 except RepositoryError:
240 233 # Can be raised for empty repositories
241 234 return []
242 235 return so.splitlines()
243 236
244 237 def _get_all_revisions2(self):
245 238 #alternate implementation using dulwich
246 239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
247 240 if x[1][1] != 'T']
248 241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
249 242
250 243 def _get_revision(self, revision):
251 244 """
252 245 For git backend we always return integer here. This way we ensure
253 246 that changset's revision attribute would become integer.
254 247 """
255 248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
256 249 is_bstr = lambda o: isinstance(o, (str, unicode))
257 250 is_null = lambda o: len(o) == revision.count('0')
258 251
259 252 if len(self.revisions) == 0:
260 253 raise EmptyRepositoryError("There are no changesets yet")
261 254
262 255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
263 256 revision = self.revisions[-1]
264 257
265 258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
266 259 or isinstance(revision, int) or is_null(revision)):
267 260 try:
268 261 revision = self.revisions[int(revision)]
269 262 except:
270 263 raise ChangesetDoesNotExistError("Revision %s does not exist "
271 264 "for this repository" % (revision))
272 265
273 266 elif is_bstr(revision):
274 267 # get by branch/tag name
275 268 _ref_revision = self._parsed_refs.get(revision)
276 269 _tags_shas = self.tags.values()
277 270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
278 271 return _ref_revision[0]
279 272
280 273 # maybe it's a tag ? we don't have them in self.revisions
281 274 elif revision in _tags_shas:
282 275 return _tags_shas[_tags_shas.index(revision)]
283 276
284 277 elif not pattern.match(revision) or revision not in self.revisions:
285 278 raise ChangesetDoesNotExistError("Revision %s does not exist "
286 279 "for this repository" % (revision))
287 280
288 281 # Ensure we return full id
289 282 if not pattern.match(str(revision)):
290 283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
291 284 % revision)
292 285 return revision
293 286
294 287 def _get_archives(self, archive_name='tip'):
295 288
296 289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
297 290 yield {"type": i[0], "extension": i[1], "node": archive_name}
298 291
299 292 def _get_url(self, url):
300 293 """
301 294 Returns normalized url. If schema is not given, would fall to
302 295 filesystem (``file:///``) schema.
303 296 """
304 297 url = str(url)
305 298 if url != 'default' and not '://' in url:
306 299 url = ':///'.join(('file', url))
307 300 return url
308 301
309 302 def get_hook_location(self):
310 303 """
311 304 returns absolute path to location where hooks are stored
312 305 """
313 306 loc = os.path.join(self.path, 'hooks')
314 307 if not self.bare:
315 308 loc = os.path.join(self.path, '.git', 'hooks')
316 309 return loc
317 310
318 311 @LazyProperty
319 312 def name(self):
320 313 return os.path.basename(self.path)
321 314
322 315 @LazyProperty
323 316 def last_change(self):
324 317 """
325 318 Returns last change made on this repository as datetime object
326 319 """
327 320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
328 321
329 322 def _get_mtime(self):
330 323 try:
331 324 return time.mktime(self.get_changeset().date.timetuple())
332 325 except RepositoryError:
333 326 idx_loc = '' if self.bare else '.git'
334 327 # fallback to filesystem
335 328 in_path = os.path.join(self.path, idx_loc, "index")
336 329 he_path = os.path.join(self.path, idx_loc, "HEAD")
337 330 if os.path.exists(in_path):
338 331 return os.stat(in_path).st_mtime
339 332 else:
340 333 return os.stat(he_path).st_mtime
341 334
342 335 @LazyProperty
343 336 def description(self):
344 337 idx_loc = '' if self.bare else '.git'
345 338 undefined_description = u'unknown'
346 339 description_path = os.path.join(self.path, idx_loc, 'description')
347 340 if os.path.isfile(description_path):
348 341 return safe_unicode(open(description_path).read())
349 342 else:
350 343 return undefined_description
351 344
352 345 @LazyProperty
353 346 def contact(self):
354 347 undefined_contact = u'Unknown'
355 348 return undefined_contact
356 349
357 350 @property
358 351 def branches(self):
359 352 if not self.revisions:
360 353 return {}
361 354 sortkey = lambda ctx: ctx[0]
362 355 _branches = [(x[0], x[1][0])
363 356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
364 357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
365 358
366 359 @LazyProperty
367 360 def tags(self):
368 361 return self._get_tags()
369 362
370 363 def _get_tags(self):
371 364 if not self.revisions:
372 365 return {}
373 366
374 367 sortkey = lambda ctx: ctx[0]
375 368 _tags = [(x[0], x[1][0])
376 369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
377 370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
378 371
379 372 def tag(self, name, user, revision=None, message=None, date=None,
380 373 **kwargs):
381 374 """
382 375 Creates and returns a tag for the given ``revision``.
383 376
384 377 :param name: name for new tag
385 378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
386 379 :param revision: changeset id for which new tag would be created
387 380 :param message: message of the tag's commit
388 381 :param date: date of tag's commit
389 382
390 383 :raises TagAlreadyExistError: if tag with same name already exists
391 384 """
392 385 if name in self.tags:
393 386 raise TagAlreadyExistError("Tag %s already exists" % name)
394 387 changeset = self.get_changeset(revision)
395 388 message = message or "Added tag %s for commit %s" % (name,
396 389 changeset.raw_id)
397 390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
398 391
399 392 self._parsed_refs = self._get_parsed_refs()
400 393 self.tags = self._get_tags()
401 394 return changeset
402 395
403 396 def remove_tag(self, name, user, message=None, date=None):
404 397 """
405 398 Removes tag with the given ``name``.
406 399
407 400 :param name: name of the tag to be removed
408 401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
409 402 :param message: message of the tag's removal commit
410 403 :param date: date of tag's removal commit
411 404
412 405 :raises TagDoesNotExistError: if tag with given name does not exists
413 406 """
414 407 if name not in self.tags:
415 408 raise TagDoesNotExistError("Tag %s does not exist" % name)
416 409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
417 410 try:
418 411 os.remove(tagpath)
419 412 self._parsed_refs = self._get_parsed_refs()
420 413 self.tags = self._get_tags()
421 414 except OSError, e:
422 415 raise RepositoryError(e.strerror)
423 416
424 417 @LazyProperty
425 418 def _parsed_refs(self):
426 419 return self._get_parsed_refs()
427 420
428 421 def _get_parsed_refs(self):
429 422 refs = self._repo.get_refs()
430 423 keys = [('refs/heads/', 'H'),
431 424 ('refs/remotes/origin/', 'RH'),
432 425 ('refs/tags/', 'T')]
433 426 _refs = {}
434 427 for ref, sha in refs.iteritems():
435 428 for k, type_ in keys:
436 429 if ref.startswith(k):
437 430 _key = ref[len(k):]
438 431 if type_ == 'T':
439 432 obj = self._repo.get_object(sha)
440 433 if isinstance(obj, Tag):
441 434 sha = self._repo.get_object(sha).object[1]
442 435 _refs[_key] = [sha, type_]
443 436 break
444 437 return _refs
445 438
446 439 def _heads(self, reverse=False):
447 440 refs = self._repo.get_refs()
448 441 heads = {}
449 442
450 443 for key, val in refs.items():
451 444 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
452 445 if key.startswith(ref_key):
453 446 n = key[len(ref_key):]
454 447 if n not in ['HEAD']:
455 448 heads[n] = val
456 449
457 450 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
458 451
459 452 def get_changeset(self, revision=None):
460 453 """
461 454 Returns ``GitChangeset`` object representing commit from git repository
462 455 at the given revision or head (most recent commit) if None given.
463 456 """
464 457 if isinstance(revision, GitChangeset):
465 458 return revision
466 459 revision = self._get_revision(revision)
467 460 changeset = GitChangeset(repository=self, revision=revision)
468 461 return changeset
469 462
470 463 def get_changesets(self, start=None, end=None, start_date=None,
471 464 end_date=None, branch_name=None, reverse=False):
472 465 """
473 466 Returns iterator of ``GitChangeset`` objects from start to end (both
474 467 are inclusive), in ascending date order (unless ``reverse`` is set).
475 468
476 469 :param start: changeset ID, as str; first returned changeset
477 470 :param end: changeset ID, as str; last returned changeset
478 471 :param start_date: if specified, changesets with commit date less than
479 472 ``start_date`` would be filtered out from returned set
480 473 :param end_date: if specified, changesets with commit date greater than
481 474 ``end_date`` would be filtered out from returned set
482 475 :param branch_name: if specified, changesets not reachable from given
483 476 branch would be filtered out from returned set
484 477 :param reverse: if ``True``, returned generator would be reversed
485 478 (meaning that returned changesets would have descending date order)
486 479
487 480 :raise BranchDoesNotExistError: If given ``branch_name`` does not
488 481 exist.
489 482 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
490 483 ``end`` could not be found.
491 484
492 485 """
493 486 if branch_name and branch_name not in self.branches:
494 487 raise BranchDoesNotExistError("Branch '%s' not found" \
495 488 % branch_name)
496 489 # %H at format means (full) commit hash, initial hashes are retrieved
497 490 # in ascending date order
498 491 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
499 492 cmd_params = {}
500 493 if start_date:
501 494 cmd_template += ' --since "$since"'
502 495 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
503 496 if end_date:
504 497 cmd_template += ' --until "$until"'
505 498 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
506 499 if branch_name:
507 500 cmd_template += ' $branch_name'
508 501 cmd_params['branch_name'] = branch_name
509 502 else:
510 503 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
511 504 '--all').strip()
512 505 cmd_template += ' %s' % (rev_filter)
513 506
514 507 cmd = Template(cmd_template).safe_substitute(**cmd_params)
515 508 revs = self.run_git_command(cmd)[0].splitlines()
516 509 start_pos = 0
517 510 end_pos = len(revs)
518 511 if start:
519 512 _start = self._get_revision(start)
520 513 try:
521 514 start_pos = revs.index(_start)
522 515 except ValueError:
523 516 pass
524 517
525 518 if end is not None:
526 519 _end = self._get_revision(end)
527 520 try:
528 521 end_pos = revs.index(_end)
529 522 except ValueError:
530 523 pass
531 524
532 525 if None not in [start, end] and start_pos > end_pos:
533 526 raise RepositoryError('start cannot be after end')
534 527
535 528 if end_pos is not None:
536 529 end_pos += 1
537 530
538 531 revs = revs[start_pos:end_pos]
539 532 if reverse:
540 533 revs = reversed(revs)
541 534 for rev in revs:
542 535 yield self.get_changeset(rev)
543 536
544 537 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
545 538 context=3):
546 539 """
547 540 Returns (git like) *diff*, as plain text. Shows changes introduced by
548 541 ``rev2`` since ``rev1``.
549 542
550 543 :param rev1: Entry point from which diff is shown. Can be
551 544 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
552 545 the changes since empty state of the repository until ``rev2``
553 546 :param rev2: Until which revision changes should be shown.
554 547 :param ignore_whitespace: If set to ``True``, would not show whitespace
555 548 changes. Defaults to ``False``.
556 549 :param context: How many lines before/after changed lines should be
557 550 shown. Defaults to ``3``.
558 551 """
559 552 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
560 553 if ignore_whitespace:
561 554 flags.append('-w')
562 555
563 556 if hasattr(rev1, 'raw_id'):
564 557 rev1 = getattr(rev1, 'raw_id')
565 558
566 559 if hasattr(rev2, 'raw_id'):
567 560 rev2 = getattr(rev2, 'raw_id')
568 561
569 562 if rev1 == self.EMPTY_CHANGESET:
570 563 rev2 = self.get_changeset(rev2).raw_id
571 564 cmd = ' '.join(['show'] + flags + [rev2])
572 565 else:
573 566 rev1 = self.get_changeset(rev1).raw_id
574 567 rev2 = self.get_changeset(rev2).raw_id
575 568 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
576 569
577 570 if path:
578 571 cmd += ' -- "%s"' % path
579 572
580 573 stdout, stderr = self.run_git_command(cmd)
581 574 # If we used 'show' command, strip first few lines (until actual diff
582 575 # starts)
583 576 if rev1 == self.EMPTY_CHANGESET:
584 577 lines = stdout.splitlines()
585 578 x = 0
586 579 for line in lines:
587 580 if line.startswith('diff'):
588 581 break
589 582 x += 1
590 583 # Append new line just like 'diff' command do
591 584 stdout = '\n'.join(lines[x:]) + '\n'
592 585 return stdout
593 586
594 587 @LazyProperty
595 588 def in_memory_changeset(self):
596 589 """
597 590 Returns ``GitInMemoryChangeset`` object for this repository.
598 591 """
599 592 return GitInMemoryChangeset(self)
600 593
601 594 def clone(self, url, update_after_clone=True, bare=False):
602 595 """
603 596 Tries to clone changes from external location.
604 597
605 598 :param update_after_clone: If set to ``False``, git won't checkout
606 599 working directory
607 600 :param bare: If set to ``True``, repository would be cloned into
608 601 *bare* git repository (no working directory at all).
609 602 """
610 603 url = self._get_url(url)
611 604 cmd = ['clone']
612 605 if bare:
613 606 cmd.append('--bare')
614 607 elif not update_after_clone:
615 608 cmd.append('--no-checkout')
616 609 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
617 610 cmd = ' '.join(cmd)
618 611 # If error occurs run_git_command raises RepositoryError already
619 612 self.run_git_command(cmd)
620 613
621 614 def pull(self, url):
622 615 """
623 616 Tries to pull changes from external location.
624 617 """
625 618 url = self._get_url(url)
626 619 cmd = ['pull']
627 620 cmd.append("--ff-only")
628 621 cmd.append(url)
629 622 cmd = ' '.join(cmd)
630 623 # If error occurs run_git_command raises RepositoryError already
631 624 self.run_git_command(cmd)
632 625
633 626 def fetch(self, url):
634 627 """
635 628 Tries to pull changes from external location.
636 629 """
637 630 url = self._get_url(url)
638 631 so, se = self.run_git_command('ls-remote -h %s' % url)
639 632 refs = []
640 633 for line in (x for x in so.splitlines()):
641 634 sha, ref = line.split('\t')
642 635 refs.append(ref)
643 636 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
644 637 cmd = '''fetch %s -- %s''' % (url, refs)
645 638 self.run_git_command(cmd)
646 639
647 640 @LazyProperty
648 641 def workdir(self):
649 642 """
650 643 Returns ``Workdir`` instance for this repository.
651 644 """
652 645 return GitWorkdir(self)
653 646
654 647 def get_config_value(self, section, name, config_file=None):
655 648 """
656 649 Returns configuration value for a given [``section``] and ``name``.
657 650
658 651 :param section: Section we want to retrieve value from
659 652 :param name: Name of configuration we want to retrieve
660 653 :param config_file: A path to file which should be used to retrieve
661 654 configuration from (might also be a list of file paths)
662 655 """
663 656 if config_file is None:
664 657 config_file = []
665 658 elif isinstance(config_file, basestring):
666 659 config_file = [config_file]
667 660
668 661 def gen_configs():
669 662 for path in config_file + self._config_files:
670 663 try:
671 664 yield ConfigFile.from_path(path)
672 665 except (IOError, OSError, ValueError):
673 666 continue
674 667
675 668 for config in gen_configs():
676 669 try:
677 670 return config.get(section, name)
678 671 except KeyError:
679 672 continue
680 673 return None
681 674
682 675 def get_user_name(self, config_file=None):
683 676 """
684 677 Returns user's name from global configuration file.
685 678
686 679 :param config_file: A path to file which should be used to retrieve
687 680 configuration from (might also be a list of file paths)
688 681 """
689 682 return self.get_config_value('user', 'name', config_file)
690 683
691 684 def get_user_email(self, config_file=None):
692 685 """
693 686 Returns user's email from global configuration file.
694 687
695 688 :param config_file: A path to file which should be used to retrieve
696 689 configuration from (might also be a list of file paths)
697 690 """
698 691 return self.get_config_value('user', 'email', config_file)
@@ -1,2056 +1,2052 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48
49 49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime
50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime, _set_extras
51 51 from rhodecode.lib.compat import json
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model.meta import Base, Session
55 55
56 56 URL_SEP = '/'
57 57 log = logging.getLogger(__name__)
58 58
59 59 #==============================================================================
60 60 # BASE CLASSES
61 61 #==============================================================================
62 62
63 63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64 64
65 65
66 66 class BaseModel(object):
67 67 """
68 68 Base Model for all classess
69 69 """
70 70
71 71 @classmethod
72 72 def _get_keys(cls):
73 73 """return column names for this model """
74 74 return class_mapper(cls).c.keys()
75 75
76 76 def get_dict(self):
77 77 """
78 78 return dict with keys and values corresponding
79 79 to this model data """
80 80
81 81 d = {}
82 82 for k in self._get_keys():
83 83 d[k] = getattr(self, k)
84 84
85 85 # also use __json__() if present to get additional fields
86 86 _json_attr = getattr(self, '__json__', None)
87 87 if _json_attr:
88 88 # update with attributes from __json__
89 89 if callable(_json_attr):
90 90 _json_attr = _json_attr()
91 91 for k, val in _json_attr.iteritems():
92 92 d[k] = val
93 93 return d
94 94
95 95 def get_appstruct(self):
96 96 """return list with keys and values tupples corresponding
97 97 to this model data """
98 98
99 99 l = []
100 100 for k in self._get_keys():
101 101 l.append((k, getattr(self, k),))
102 102 return l
103 103
104 104 def populate_obj(self, populate_dict):
105 105 """populate model with data from given populate_dict"""
106 106
107 107 for k in self._get_keys():
108 108 if k in populate_dict:
109 109 setattr(self, k, populate_dict[k])
110 110
111 111 @classmethod
112 112 def query(cls):
113 113 return Session().query(cls)
114 114
115 115 @classmethod
116 116 def get(cls, id_):
117 117 if id_:
118 118 return cls.query().get(id_)
119 119
120 120 @classmethod
121 121 def get_or_404(cls, id_):
122 122 try:
123 123 id_ = int(id_)
124 124 except (TypeError, ValueError):
125 125 raise HTTPNotFound
126 126
127 127 res = cls.query().get(id_)
128 128 if not res:
129 129 raise HTTPNotFound
130 130 return res
131 131
132 132 @classmethod
133 133 def getAll(cls):
134 134 return cls.query().all()
135 135
136 136 @classmethod
137 137 def delete(cls, id_):
138 138 obj = cls.query().get(id_)
139 139 Session().delete(obj)
140 140
141 141 def __repr__(self):
142 142 if hasattr(self, '__unicode__'):
143 143 # python repr needs to return str
144 144 return safe_str(self.__unicode__())
145 145 return '<DB:%s>' % (self.__class__.__name__)
146 146
147 147
148 148 class RhodeCodeSetting(Base, BaseModel):
149 149 __tablename__ = 'rhodecode_settings'
150 150 __table_args__ = (
151 151 UniqueConstraint('app_settings_name'),
152 152 {'extend_existing': True, 'mysql_engine': 'InnoDB',
153 153 'mysql_charset': 'utf8'}
154 154 )
155 155 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
156 156 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 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 159 def __init__(self, k='', v=''):
160 160 self.app_settings_name = k
161 161 self.app_settings_value = v
162 162
163 163 @validates('_app_settings_value')
164 164 def validate_settings_value(self, key, val):
165 165 assert type(val) == unicode
166 166 return val
167 167
168 168 @hybrid_property
169 169 def app_settings_value(self):
170 170 v = self._app_settings_value
171 171 if self.app_settings_name in ["ldap_active",
172 172 "default_repo_enable_statistics",
173 173 "default_repo_enable_locking",
174 174 "default_repo_private",
175 175 "default_repo_enable_downloads"]:
176 176 v = str2bool(v)
177 177 return v
178 178
179 179 @app_settings_value.setter
180 180 def app_settings_value(self, val):
181 181 """
182 182 Setter that will always make sure we use unicode in app_settings_value
183 183
184 184 :param val:
185 185 """
186 186 self._app_settings_value = safe_unicode(val)
187 187
188 188 def __unicode__(self):
189 189 return u"<%s('%s:%s')>" % (
190 190 self.__class__.__name__,
191 191 self.app_settings_name, self.app_settings_value
192 192 )
193 193
194 194 @classmethod
195 195 def get_by_name(cls, key):
196 196 return cls.query()\
197 197 .filter(cls.app_settings_name == key).scalar()
198 198
199 199 @classmethod
200 200 def get_by_name_or_create(cls, key):
201 201 res = cls.get_by_name(key)
202 202 if not res:
203 203 res = cls(key)
204 204 return res
205 205
206 206 @classmethod
207 207 def get_app_settings(cls, cache=False):
208 208
209 209 ret = cls.query()
210 210
211 211 if cache:
212 212 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
213 213
214 214 if not ret:
215 215 raise Exception('Could not get application settings !')
216 216 settings = {}
217 217 for each in ret:
218 218 settings['rhodecode_' + each.app_settings_name] = \
219 219 each.app_settings_value
220 220
221 221 return settings
222 222
223 223 @classmethod
224 224 def get_ldap_settings(cls, cache=False):
225 225 ret = cls.query()\
226 226 .filter(cls.app_settings_name.startswith('ldap_')).all()
227 227 fd = {}
228 228 for row in ret:
229 229 fd.update({row.app_settings_name: row.app_settings_value})
230 230
231 231 return fd
232 232
233 233 @classmethod
234 234 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
235 235 ret = cls.query()\
236 236 .filter(cls.app_settings_name.startswith('default_')).all()
237 237 fd = {}
238 238 for row in ret:
239 239 key = row.app_settings_name
240 240 if strip_prefix:
241 241 key = remove_prefix(key, prefix='default_')
242 242 fd.update({key: row.app_settings_value})
243 243
244 244 return fd
245 245
246 246
247 247 class RhodeCodeUi(Base, BaseModel):
248 248 __tablename__ = 'rhodecode_ui'
249 249 __table_args__ = (
250 250 UniqueConstraint('ui_key'),
251 251 {'extend_existing': True, 'mysql_engine': 'InnoDB',
252 252 'mysql_charset': 'utf8'}
253 253 )
254 254
255 255 HOOK_UPDATE = 'changegroup.update'
256 256 HOOK_REPO_SIZE = 'changegroup.repo_size'
257 257 HOOK_PUSH = 'changegroup.push_logger'
258 258 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
259 259 HOOK_PULL = 'outgoing.pull_logger'
260 260 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
261 261
262 262 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
263 263 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
266 266 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
267 267
268 268 @classmethod
269 269 def get_by_key(cls, key):
270 270 return cls.query().filter(cls.ui_key == key).scalar()
271 271
272 272 @classmethod
273 273 def get_builtin_hooks(cls):
274 274 q = cls.query()
275 275 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
276 276 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
277 277 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
278 278 return q.all()
279 279
280 280 @classmethod
281 281 def get_custom_hooks(cls):
282 282 q = cls.query()
283 283 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
284 284 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
285 285 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
286 286 q = q.filter(cls.ui_section == 'hooks')
287 287 return q.all()
288 288
289 289 @classmethod
290 290 def get_repos_location(cls):
291 291 return cls.get_by_key('/').ui_value
292 292
293 293 @classmethod
294 294 def create_or_update_hook(cls, key, val):
295 295 new_ui = cls.get_by_key(key) or cls()
296 296 new_ui.ui_section = 'hooks'
297 297 new_ui.ui_active = True
298 298 new_ui.ui_key = key
299 299 new_ui.ui_value = val
300 300
301 301 Session().add(new_ui)
302 302
303 303 def __repr__(self):
304 304 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
305 305 self.ui_value)
306 306
307 307
308 308 class User(Base, BaseModel):
309 309 __tablename__ = 'users'
310 310 __table_args__ = (
311 311 UniqueConstraint('username'), UniqueConstraint('email'),
312 312 Index('u_username_idx', 'username'),
313 313 Index('u_email_idx', 'email'),
314 314 {'extend_existing': True, 'mysql_engine': 'InnoDB',
315 315 'mysql_charset': 'utf8'}
316 316 )
317 317 DEFAULT_USER = 'default'
318 318 DEFAULT_PERMISSIONS = [
319 319 'hg.register.manual_activate', 'hg.create.repository',
320 320 'hg.fork.repository', 'repository.read', 'group.read'
321 321 ]
322 322 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
323 323 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
324 324 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
325 325 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
326 326 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
327 327 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
328 328 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
329 329 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 330 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
331 331 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
333 333 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
334 334
335 335 user_log = relationship('UserLog')
336 336 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
337 337
338 338 repositories = relationship('Repository')
339 339 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
340 340 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
341 341
342 342 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
343 343 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
344 344
345 345 group_member = relationship('UserGroupMember', cascade='all')
346 346
347 347 notifications = relationship('UserNotification', cascade='all')
348 348 # notifications assigned to this user
349 349 user_created_notifications = relationship('Notification', cascade='all')
350 350 # comments created by this user
351 351 user_comments = relationship('ChangesetComment', cascade='all')
352 352 #extra emails for this user
353 353 user_emails = relationship('UserEmailMap', cascade='all')
354 354
355 355 @hybrid_property
356 356 def email(self):
357 357 return self._email
358 358
359 359 @email.setter
360 360 def email(self, val):
361 361 self._email = val.lower() if val else None
362 362
363 363 @property
364 364 def firstname(self):
365 365 # alias for future
366 366 return self.name
367 367
368 368 @property
369 369 def emails(self):
370 370 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
371 371 return [self.email] + [x.email for x in other]
372 372
373 373 @property
374 374 def ip_addresses(self):
375 375 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
376 376 return [x.ip_addr for x in ret]
377 377
378 378 @property
379 379 def username_and_name(self):
380 380 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
381 381
382 382 @property
383 383 def full_name(self):
384 384 return '%s %s' % (self.firstname, self.lastname)
385 385
386 386 @property
387 387 def full_name_or_username(self):
388 388 return ('%s %s' % (self.firstname, self.lastname)
389 389 if (self.firstname and self.lastname) else self.username)
390 390
391 391 @property
392 392 def full_contact(self):
393 393 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
394 394
395 395 @property
396 396 def short_contact(self):
397 397 return '%s %s' % (self.firstname, self.lastname)
398 398
399 399 @property
400 400 def is_admin(self):
401 401 return self.admin
402 402
403 403 @property
404 404 def AuthUser(self):
405 405 """
406 406 Returns instance of AuthUser for this user
407 407 """
408 408 from rhodecode.lib.auth import AuthUser
409 409 return AuthUser(user_id=self.user_id, api_key=self.api_key,
410 410 username=self.username)
411 411
412 412 def __unicode__(self):
413 413 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
414 414 self.user_id, self.username)
415 415
416 416 @classmethod
417 417 def get_by_username(cls, username, case_insensitive=False, cache=False):
418 418 if case_insensitive:
419 419 q = cls.query().filter(cls.username.ilike(username))
420 420 else:
421 421 q = cls.query().filter(cls.username == username)
422 422
423 423 if cache:
424 424 q = q.options(FromCache(
425 425 "sql_cache_short",
426 426 "get_user_%s" % _hash_key(username)
427 427 )
428 428 )
429 429 return q.scalar()
430 430
431 431 @classmethod
432 432 def get_by_api_key(cls, api_key, cache=False):
433 433 q = cls.query().filter(cls.api_key == api_key)
434 434
435 435 if cache:
436 436 q = q.options(FromCache("sql_cache_short",
437 437 "get_api_key_%s" % api_key))
438 438 return q.scalar()
439 439
440 440 @classmethod
441 441 def get_by_email(cls, email, case_insensitive=False, cache=False):
442 442 if case_insensitive:
443 443 q = cls.query().filter(cls.email.ilike(email))
444 444 else:
445 445 q = cls.query().filter(cls.email == email)
446 446
447 447 if cache:
448 448 q = q.options(FromCache("sql_cache_short",
449 449 "get_email_key_%s" % email))
450 450
451 451 ret = q.scalar()
452 452 if ret is None:
453 453 q = UserEmailMap.query()
454 454 # try fetching in alternate email map
455 455 if case_insensitive:
456 456 q = q.filter(UserEmailMap.email.ilike(email))
457 457 else:
458 458 q = q.filter(UserEmailMap.email == email)
459 459 q = q.options(joinedload(UserEmailMap.user))
460 460 if cache:
461 461 q = q.options(FromCache("sql_cache_short",
462 462 "get_email_map_key_%s" % email))
463 463 ret = getattr(q.scalar(), 'user', None)
464 464
465 465 return ret
466 466
467 467 @classmethod
468 468 def get_from_cs_author(cls, author):
469 469 """
470 470 Tries to get User objects out of commit author string
471 471
472 472 :param author:
473 473 """
474 474 from rhodecode.lib.helpers import email, author_name
475 475 # Valid email in the attribute passed, see if they're in the system
476 476 _email = email(author)
477 477 if _email:
478 478 user = cls.get_by_email(_email, case_insensitive=True)
479 479 if user:
480 480 return user
481 481 # Maybe we can match by username?
482 482 _author = author_name(author)
483 483 user = cls.get_by_username(_author, case_insensitive=True)
484 484 if user:
485 485 return user
486 486
487 487 def update_lastlogin(self):
488 488 """Update user lastlogin"""
489 489 self.last_login = datetime.datetime.now()
490 490 Session().add(self)
491 491 log.debug('updated user %s lastlogin' % self.username)
492 492
493 493 def get_api_data(self):
494 494 """
495 495 Common function for generating user related data for API
496 496 """
497 497 user = self
498 498 data = dict(
499 499 user_id=user.user_id,
500 500 username=user.username,
501 501 firstname=user.name,
502 502 lastname=user.lastname,
503 503 email=user.email,
504 504 emails=user.emails,
505 505 api_key=user.api_key,
506 506 active=user.active,
507 507 admin=user.admin,
508 508 ldap_dn=user.ldap_dn,
509 509 last_login=user.last_login,
510 510 ip_addresses=user.ip_addresses
511 511 )
512 512 return data
513 513
514 514 def __json__(self):
515 515 data = dict(
516 516 full_name=self.full_name,
517 517 full_name_or_username=self.full_name_or_username,
518 518 short_contact=self.short_contact,
519 519 full_contact=self.full_contact
520 520 )
521 521 data.update(self.get_api_data())
522 522 return data
523 523
524 524
525 525 class UserEmailMap(Base, BaseModel):
526 526 __tablename__ = 'user_email_map'
527 527 __table_args__ = (
528 528 Index('uem_email_idx', 'email'),
529 529 UniqueConstraint('email'),
530 530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
531 531 'mysql_charset': 'utf8'}
532 532 )
533 533 __mapper_args__ = {}
534 534
535 535 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
536 536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
537 537 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
538 538 user = relationship('User', lazy='joined')
539 539
540 540 @validates('_email')
541 541 def validate_email(self, key, email):
542 542 # check if this email is not main one
543 543 main_email = Session().query(User).filter(User.email == email).scalar()
544 544 if main_email is not None:
545 545 raise AttributeError('email %s is present is user table' % email)
546 546 return email
547 547
548 548 @hybrid_property
549 549 def email(self):
550 550 return self._email
551 551
552 552 @email.setter
553 553 def email(self, val):
554 554 self._email = val.lower() if val else None
555 555
556 556
557 557 class UserIpMap(Base, BaseModel):
558 558 __tablename__ = 'user_ip_map'
559 559 __table_args__ = (
560 560 UniqueConstraint('user_id', 'ip_addr'),
561 561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
562 562 'mysql_charset': 'utf8'}
563 563 )
564 564 __mapper_args__ = {}
565 565
566 566 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
567 567 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
568 568 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
569 569 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
570 570 user = relationship('User', lazy='joined')
571 571
572 572 @classmethod
573 573 def _get_ip_range(cls, ip_addr):
574 574 from rhodecode.lib import ipaddr
575 575 net = ipaddr.IPNetwork(address=ip_addr)
576 576 return [str(net.network), str(net.broadcast)]
577 577
578 578 def __json__(self):
579 579 return dict(
580 580 ip_addr=self.ip_addr,
581 581 ip_range=self._get_ip_range(self.ip_addr)
582 582 )
583 583
584 584
585 585 class UserLog(Base, BaseModel):
586 586 __tablename__ = 'user_logs'
587 587 __table_args__ = (
588 588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
589 589 'mysql_charset': 'utf8'},
590 590 )
591 591 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
592 592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
593 593 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
594 594 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
595 595 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
596 596 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
597 597 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
598 598 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 599
600 600 @property
601 601 def action_as_day(self):
602 602 return datetime.date(*self.action_date.timetuple()[:3])
603 603
604 604 user = relationship('User')
605 605 repository = relationship('Repository', cascade='')
606 606
607 607
608 608 class UserGroup(Base, BaseModel):
609 609 __tablename__ = 'users_groups'
610 610 __table_args__ = (
611 611 {'extend_existing': True, 'mysql_engine': 'InnoDB',
612 612 'mysql_charset': 'utf8'},
613 613 )
614 614
615 615 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
616 616 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
617 617 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
618 618 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
619 619
620 620 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
621 621 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
622 622 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
623 623
624 624 def __unicode__(self):
625 625 return u'<userGroup(%s)>' % (self.users_group_name)
626 626
627 627 @classmethod
628 628 def get_by_group_name(cls, group_name, cache=False,
629 629 case_insensitive=False):
630 630 if case_insensitive:
631 631 q = cls.query().filter(cls.users_group_name.ilike(group_name))
632 632 else:
633 633 q = cls.query().filter(cls.users_group_name == group_name)
634 634 if cache:
635 635 q = q.options(FromCache(
636 636 "sql_cache_short",
637 637 "get_user_%s" % _hash_key(group_name)
638 638 )
639 639 )
640 640 return q.scalar()
641 641
642 642 @classmethod
643 643 def get(cls, users_group_id, cache=False):
644 644 users_group = cls.query()
645 645 if cache:
646 646 users_group = users_group.options(FromCache("sql_cache_short",
647 647 "get_users_group_%s" % users_group_id))
648 648 return users_group.get(users_group_id)
649 649
650 650 def get_api_data(self):
651 651 users_group = self
652 652
653 653 data = dict(
654 654 users_group_id=users_group.users_group_id,
655 655 group_name=users_group.users_group_name,
656 656 active=users_group.users_group_active,
657 657 )
658 658
659 659 return data
660 660
661 661
662 662 class UserGroupMember(Base, BaseModel):
663 663 __tablename__ = 'users_groups_members'
664 664 __table_args__ = (
665 665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
666 666 'mysql_charset': 'utf8'},
667 667 )
668 668
669 669 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
670 670 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
671 671 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
672 672
673 673 user = relationship('User', lazy='joined')
674 674 users_group = relationship('UserGroup')
675 675
676 676 def __init__(self, gr_id='', u_id=''):
677 677 self.users_group_id = gr_id
678 678 self.user_id = u_id
679 679
680 680
681 681 class RepositoryField(Base, BaseModel):
682 682 __tablename__ = 'repositories_fields'
683 683 __table_args__ = (
684 684 UniqueConstraint('repository_id', 'field_key'), # no-multi field
685 685 {'extend_existing': True, 'mysql_engine': 'InnoDB',
686 686 'mysql_charset': 'utf8'},
687 687 )
688 688 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
689 689
690 690 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
691 691 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
692 692 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
693 693 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
694 694 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
695 695 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
696 696 field_type = Column("field_type", String(256), nullable=False, unique=None)
697 697 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
698 698
699 699 repository = relationship('Repository')
700 700
701 701 @property
702 702 def field_key_prefixed(self):
703 703 return 'ex_%s' % self.field_key
704 704
705 705 @classmethod
706 706 def un_prefix_key(cls, key):
707 707 if key.startswith(cls.PREFIX):
708 708 return key[len(cls.PREFIX):]
709 709 return key
710 710
711 711 @classmethod
712 712 def get_by_key_name(cls, key, repo):
713 713 row = cls.query()\
714 714 .filter(cls.repository == repo)\
715 715 .filter(cls.field_key == key).scalar()
716 716 return row
717 717
718 718
719 719 class Repository(Base, BaseModel):
720 720 __tablename__ = 'repositories'
721 721 __table_args__ = (
722 722 UniqueConstraint('repo_name'),
723 723 Index('r_repo_name_idx', 'repo_name'),
724 724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
725 725 'mysql_charset': 'utf8'},
726 726 )
727 727
728 728 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
729 729 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
730 730 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
731 731 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
732 732 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
733 733 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
734 734 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
735 735 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
736 736 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
737 737 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
738 738 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
739 739 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
740 740 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
741 741 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
742 742 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
743 743
744 744 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
745 745 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
746 746
747 747 user = relationship('User')
748 748 fork = relationship('Repository', remote_side=repo_id)
749 749 group = relationship('RepoGroup')
750 750 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
751 751 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
752 752 stats = relationship('Statistics', cascade='all', uselist=False)
753 753
754 754 followers = relationship('UserFollowing',
755 755 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
756 756 cascade='all')
757 757 extra_fields = relationship('RepositoryField',
758 758 cascade="all, delete, delete-orphan")
759 759
760 760 logs = relationship('UserLog')
761 761 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
762 762
763 763 pull_requests_org = relationship('PullRequest',
764 764 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
765 765 cascade="all, delete, delete-orphan")
766 766
767 767 pull_requests_other = relationship('PullRequest',
768 768 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
769 769 cascade="all, delete, delete-orphan")
770 770
771 771 def __unicode__(self):
772 772 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
773 773 self.repo_name)
774 774
775 775 @hybrid_property
776 776 def locked(self):
777 777 # always should return [user_id, timelocked]
778 778 if self._locked:
779 779 _lock_info = self._locked.split(':')
780 780 return int(_lock_info[0]), _lock_info[1]
781 781 return [None, None]
782 782
783 783 @locked.setter
784 784 def locked(self, val):
785 785 if val and isinstance(val, (list, tuple)):
786 786 self._locked = ':'.join(map(str, val))
787 787 else:
788 788 self._locked = None
789 789
790 790 @hybrid_property
791 791 def changeset_cache(self):
792 792 from rhodecode.lib.vcs.backends.base import EmptyChangeset
793 793 dummy = EmptyChangeset().__json__()
794 794 if not self._changeset_cache:
795 795 return dummy
796 796 try:
797 797 return json.loads(self._changeset_cache)
798 798 except TypeError:
799 799 return dummy
800 800
801 801 @changeset_cache.setter
802 802 def changeset_cache(self, val):
803 803 try:
804 804 self._changeset_cache = json.dumps(val)
805 805 except:
806 806 log.error(traceback.format_exc())
807 807
808 808 @classmethod
809 809 def url_sep(cls):
810 810 return URL_SEP
811 811
812 812 @classmethod
813 813 def normalize_repo_name(cls, repo_name):
814 814 """
815 815 Normalizes os specific repo_name to the format internally stored inside
816 816 dabatabase using URL_SEP
817 817
818 818 :param cls:
819 819 :param repo_name:
820 820 """
821 821 return cls.url_sep().join(repo_name.split(os.sep))
822 822
823 823 @classmethod
824 824 def get_by_repo_name(cls, repo_name):
825 825 q = Session().query(cls).filter(cls.repo_name == repo_name)
826 826 q = q.options(joinedload(Repository.fork))\
827 827 .options(joinedload(Repository.user))\
828 828 .options(joinedload(Repository.group))
829 829 return q.scalar()
830 830
831 831 @classmethod
832 832 def get_by_full_path(cls, repo_full_path):
833 833 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
834 834 repo_name = cls.normalize_repo_name(repo_name)
835 835 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
836 836
837 837 @classmethod
838 838 def get_repo_forks(cls, repo_id):
839 839 return cls.query().filter(Repository.fork_id == repo_id)
840 840
841 841 @classmethod
842 842 def base_path(cls):
843 843 """
844 844 Returns base path when all repos are stored
845 845
846 846 :param cls:
847 847 """
848 848 q = Session().query(RhodeCodeUi)\
849 849 .filter(RhodeCodeUi.ui_key == cls.url_sep())
850 850 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
851 851 return q.one().ui_value
852 852
853 853 @property
854 854 def forks(self):
855 855 """
856 856 Return forks of this repo
857 857 """
858 858 return Repository.get_repo_forks(self.repo_id)
859 859
860 860 @property
861 861 def parent(self):
862 862 """
863 863 Returns fork parent
864 864 """
865 865 return self.fork
866 866
867 867 @property
868 868 def just_name(self):
869 869 return self.repo_name.split(Repository.url_sep())[-1]
870 870
871 871 @property
872 872 def groups_with_parents(self):
873 873 groups = []
874 874 if self.group is None:
875 875 return groups
876 876
877 877 cur_gr = self.group
878 878 groups.insert(0, cur_gr)
879 879 while 1:
880 880 gr = getattr(cur_gr, 'parent_group', None)
881 881 cur_gr = cur_gr.parent_group
882 882 if gr is None:
883 883 break
884 884 groups.insert(0, gr)
885 885
886 886 return groups
887 887
888 888 @property
889 889 def groups_and_repo(self):
890 890 return self.groups_with_parents, self.just_name
891 891
892 892 @LazyProperty
893 893 def repo_path(self):
894 894 """
895 895 Returns base full path for that repository means where it actually
896 896 exists on a filesystem
897 897 """
898 898 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
899 899 Repository.url_sep())
900 900 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
901 901 return q.one().ui_value
902 902
903 903 @property
904 904 def repo_full_path(self):
905 905 p = [self.repo_path]
906 906 # we need to split the name by / since this is how we store the
907 907 # names in the database, but that eventually needs to be converted
908 908 # into a valid system path
909 909 p += self.repo_name.split(Repository.url_sep())
910 910 return os.path.join(*p)
911 911
912 912 @property
913 913 def cache_keys(self):
914 914 """
915 915 Returns associated cache keys for that repo
916 916 """
917 917 return CacheInvalidation.query()\
918 918 .filter(CacheInvalidation.cache_args == self.repo_name)\
919 919 .order_by(CacheInvalidation.cache_key)\
920 920 .all()
921 921
922 922 def get_new_name(self, repo_name):
923 923 """
924 924 returns new full repository name based on assigned group and new new
925 925
926 926 :param group_name:
927 927 """
928 928 path_prefix = self.group.full_path_splitted if self.group else []
929 929 return Repository.url_sep().join(path_prefix + [repo_name])
930 930
931 931 @property
932 932 def _ui(self):
933 933 """
934 934 Creates an db based ui object for this repository
935 935 """
936 936 from rhodecode.lib.utils import make_ui
937 937 return make_ui('db', clear_session=False)
938 938
939 939 @classmethod
940 def inject_ui(cls, repo, extras={}):
941 repo.inject_ui(extras)
942
943 @classmethod
944 940 def is_valid(cls, repo_name):
945 941 """
946 942 returns True if given repo name is a valid filesystem repository
947 943
948 944 :param cls:
949 945 :param repo_name:
950 946 """
951 947 from rhodecode.lib.utils import is_valid_repo
952 948
953 949 return is_valid_repo(repo_name, cls.base_path())
954 950
955 951 def get_api_data(self):
956 952 """
957 953 Common function for generating repo api data
958 954
959 955 """
960 956 repo = self
961 957 data = dict(
962 958 repo_id=repo.repo_id,
963 959 repo_name=repo.repo_name,
964 960 repo_type=repo.repo_type,
965 961 clone_uri=repo.clone_uri,
966 962 private=repo.private,
967 963 created_on=repo.created_on,
968 964 description=repo.description,
969 965 landing_rev=repo.landing_rev,
970 966 owner=repo.user.username,
971 967 fork_of=repo.fork.repo_name if repo.fork else None,
972 968 enable_statistics=repo.enable_statistics,
973 969 enable_locking=repo.enable_locking,
974 970 enable_downloads=repo.enable_downloads,
975 971 last_changeset=repo.changeset_cache,
976 972 locked_by=User.get(self.locked[0]).get_api_data() \
977 973 if self.locked[0] else None,
978 974 locked_date=time_to_datetime(self.locked[1]) \
979 975 if self.locked[1] else None
980 976 )
981 977 rc_config = RhodeCodeSetting.get_app_settings()
982 978 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
983 979 if repository_fields:
984 980 for f in self.extra_fields:
985 981 data[f.field_key_prefixed] = f.field_value
986 982
987 983 return data
988 984
989 985 @classmethod
990 986 def lock(cls, repo, user_id):
991 987 repo.locked = [user_id, time.time()]
992 988 Session().add(repo)
993 989 Session().commit()
994 990
995 991 @classmethod
996 992 def unlock(cls, repo):
997 993 repo.locked = None
998 994 Session().add(repo)
999 995 Session().commit()
1000 996
1001 997 @classmethod
1002 998 def getlock(cls, repo):
1003 999 return repo.locked
1004 1000
1005 1001 @property
1006 1002 def last_db_change(self):
1007 1003 return self.updated_on
1008 1004
1009 1005 def clone_url(self, **override):
1010 1006 from pylons import url
1011 1007 from urlparse import urlparse
1012 1008 import urllib
1013 1009 parsed_url = urlparse(url('home', qualified=True))
1014 1010 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1015 1011 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1016 1012 args = {
1017 1013 'user': '',
1018 1014 'pass': '',
1019 1015 'scheme': parsed_url.scheme,
1020 1016 'netloc': parsed_url.netloc,
1021 1017 'prefix': decoded_path,
1022 1018 'path': self.repo_name
1023 1019 }
1024 1020
1025 1021 args.update(override)
1026 1022 return default_clone_uri % args
1027 1023
1028 1024 #==========================================================================
1029 1025 # SCM PROPERTIES
1030 1026 #==========================================================================
1031 1027
1032 1028 def get_changeset(self, rev=None):
1033 1029 return get_changeset_safe(self.scm_instance, rev)
1034 1030
1035 1031 def get_landing_changeset(self):
1036 1032 """
1037 1033 Returns landing changeset, or if that doesn't exist returns the tip
1038 1034 """
1039 1035 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1040 1036 return cs
1041 1037
1042 1038 def update_changeset_cache(self, cs_cache=None):
1043 1039 """
1044 1040 Update cache of last changeset for repository, keys should be::
1045 1041
1046 1042 short_id
1047 1043 raw_id
1048 1044 revision
1049 1045 message
1050 1046 date
1051 1047 author
1052 1048
1053 1049 :param cs_cache:
1054 1050 """
1055 1051 from rhodecode.lib.vcs.backends.base import BaseChangeset
1056 1052 if cs_cache is None:
1057 1053 cs_cache = EmptyChangeset()
1058 1054 # use no-cache version here
1059 1055 scm_repo = self.scm_instance_no_cache()
1060 1056 if scm_repo:
1061 1057 cs_cache = scm_repo.get_changeset()
1062 1058
1063 1059 if isinstance(cs_cache, BaseChangeset):
1064 1060 cs_cache = cs_cache.__json__()
1065 1061
1066 1062 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1067 1063 _default = datetime.datetime.fromtimestamp(0)
1068 1064 last_change = cs_cache.get('date') or _default
1069 1065 log.debug('updated repo %s with new cs cache %s' % (self, cs_cache))
1070 1066 self.updated_on = last_change
1071 1067 self.changeset_cache = cs_cache
1072 1068 Session().add(self)
1073 1069 Session().commit()
1074 1070 else:
1075 1071 log.debug('Skipping repo:%s already with latest changes' % self)
1076 1072
1077 1073 @property
1078 1074 def tip(self):
1079 1075 return self.get_changeset('tip')
1080 1076
1081 1077 @property
1082 1078 def author(self):
1083 1079 return self.tip.author
1084 1080
1085 1081 @property
1086 1082 def last_change(self):
1087 1083 return self.scm_instance.last_change
1088 1084
1089 1085 def get_comments(self, revisions=None):
1090 1086 """
1091 1087 Returns comments for this repository grouped by revisions
1092 1088
1093 1089 :param revisions: filter query by revisions only
1094 1090 """
1095 1091 cmts = ChangesetComment.query()\
1096 1092 .filter(ChangesetComment.repo == self)
1097 1093 if revisions:
1098 1094 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1099 1095 grouped = defaultdict(list)
1100 1096 for cmt in cmts.all():
1101 1097 grouped[cmt.revision].append(cmt)
1102 1098 return grouped
1103 1099
1104 1100 def statuses(self, revisions=None):
1105 1101 """
1106 1102 Returns statuses for this repository
1107 1103
1108 1104 :param revisions: list of revisions to get statuses for
1109 1105 :type revisions: list
1110 1106 """
1111 1107
1112 1108 statuses = ChangesetStatus.query()\
1113 1109 .filter(ChangesetStatus.repo == self)\
1114 1110 .filter(ChangesetStatus.version == 0)
1115 1111 if revisions:
1116 1112 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1117 1113 grouped = {}
1118 1114
1119 1115 #maybe we have open new pullrequest without a status ?
1120 1116 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1121 1117 status_lbl = ChangesetStatus.get_status_lbl(stat)
1122 1118 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1123 1119 for rev in pr.revisions:
1124 1120 pr_id = pr.pull_request_id
1125 1121 pr_repo = pr.other_repo.repo_name
1126 1122 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1127 1123
1128 1124 for stat in statuses.all():
1129 1125 pr_id = pr_repo = None
1130 1126 if stat.pull_request:
1131 1127 pr_id = stat.pull_request.pull_request_id
1132 1128 pr_repo = stat.pull_request.other_repo.repo_name
1133 1129 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1134 1130 pr_id, pr_repo]
1135 1131 return grouped
1136 1132
1137 1133 def _repo_size(self):
1138 1134 from rhodecode.lib import helpers as h
1139 1135 log.debug('calculating repository size...')
1140 1136 return h.format_byte_size(self.scm_instance.size)
1141 1137
1142 1138 #==========================================================================
1143 1139 # SCM CACHE INSTANCE
1144 1140 #==========================================================================
1145 1141
1146 1142 @property
1147 1143 def invalidate(self):
1148 1144 return CacheInvalidation.invalidate(self.repo_name)
1149 1145
1150 1146 def set_invalidate(self):
1151 1147 """
1152 1148 set a cache for invalidation for this instance
1153 1149 """
1154 1150 CacheInvalidation.set_invalidate(repo_name=self.repo_name)
1155 1151
1156 1152 def scm_instance_no_cache(self):
1157 1153 return self.__get_instance()
1158 1154
1159 1155 @LazyProperty
1160 1156 def scm_instance(self):
1161 1157 import rhodecode
1162 1158 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1163 1159 if full_cache:
1164 1160 return self.scm_instance_cached()
1165 1161 return self.__get_instance()
1166 1162
1167 1163 def scm_instance_cached(self, cache_map=None):
1168 1164 @cache_region('long_term')
1169 1165 def _c(repo_name):
1170 1166 return self.__get_instance()
1171 1167 rn = self.repo_name
1172 1168 log.debug('Getting cached instance of repo')
1173 1169
1174 1170 if cache_map:
1175 1171 # get using prefilled cache_map
1176 1172 invalidate_repo = cache_map[self.repo_name]
1177 1173 if invalidate_repo:
1178 1174 invalidate_repo = (None if invalidate_repo.cache_active
1179 1175 else invalidate_repo)
1180 1176 else:
1181 1177 # get from invalidate
1182 1178 invalidate_repo = self.invalidate
1183 1179
1184 1180 if invalidate_repo is not None:
1185 1181 region_invalidate(_c, None, rn)
1186 1182 # update our cache
1187 1183 CacheInvalidation.set_valid(invalidate_repo.cache_key)
1188 1184 return _c(rn)
1189 1185
1190 1186 def __get_instance(self):
1191 1187 repo_full_path = self.repo_full_path
1192 1188 try:
1193 1189 alias = get_scm(repo_full_path)[0]
1194 1190 log.debug('Creating instance of %s repository from %s'
1195 1191 % (alias, repo_full_path))
1196 1192 backend = get_backend(alias)
1197 1193 except VCSError:
1198 1194 log.error(traceback.format_exc())
1199 1195 log.error('Perhaps this repository is in db and not in '
1200 1196 'filesystem run rescan repositories with '
1201 1197 '"destroy old data " option from admin panel')
1202 1198 return
1203 1199
1204 1200 if alias == 'hg':
1205 1201
1206 1202 repo = backend(safe_str(repo_full_path), create=False,
1207 1203 baseui=self._ui)
1208 1204 # skip hidden web repository
1209 1205 if repo._get_hidden():
1210 1206 return
1211 1207 else:
1212 1208 repo = backend(repo_full_path, create=False)
1213 1209
1214 1210 return repo
1215 1211
1216 1212
1217 1213 class RepoGroup(Base, BaseModel):
1218 1214 __tablename__ = 'groups'
1219 1215 __table_args__ = (
1220 1216 UniqueConstraint('group_name', 'group_parent_id'),
1221 1217 CheckConstraint('group_id != group_parent_id'),
1222 1218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1223 1219 'mysql_charset': 'utf8'},
1224 1220 )
1225 1221 __mapper_args__ = {'order_by': 'group_name'}
1226 1222
1227 1223 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1228 1224 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1229 1225 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1230 1226 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1231 1227 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1232 1228
1233 1229 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1234 1230 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1235 1231
1236 1232 parent_group = relationship('RepoGroup', remote_side=group_id)
1237 1233
1238 1234 def __init__(self, group_name='', parent_group=None):
1239 1235 self.group_name = group_name
1240 1236 self.parent_group = parent_group
1241 1237
1242 1238 def __unicode__(self):
1243 1239 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1244 1240 self.group_name)
1245 1241
1246 1242 @classmethod
1247 1243 def groups_choices(cls, groups=None, show_empty_group=True):
1248 1244 from webhelpers.html import literal as _literal
1249 1245 if not groups:
1250 1246 groups = cls.query().all()
1251 1247
1252 1248 repo_groups = []
1253 1249 if show_empty_group:
1254 1250 repo_groups = [('-1', '-- %s --' % _('top level'))]
1255 1251 sep = ' &raquo; '
1256 1252 _name = lambda k: _literal(sep.join(k))
1257 1253
1258 1254 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1259 1255 for x in groups])
1260 1256
1261 1257 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1262 1258 return repo_groups
1263 1259
1264 1260 @classmethod
1265 1261 def url_sep(cls):
1266 1262 return URL_SEP
1267 1263
1268 1264 @classmethod
1269 1265 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1270 1266 if case_insensitive:
1271 1267 gr = cls.query()\
1272 1268 .filter(cls.group_name.ilike(group_name))
1273 1269 else:
1274 1270 gr = cls.query()\
1275 1271 .filter(cls.group_name == group_name)
1276 1272 if cache:
1277 1273 gr = gr.options(FromCache(
1278 1274 "sql_cache_short",
1279 1275 "get_group_%s" % _hash_key(group_name)
1280 1276 )
1281 1277 )
1282 1278 return gr.scalar()
1283 1279
1284 1280 @property
1285 1281 def parents(self):
1286 1282 parents_recursion_limit = 5
1287 1283 groups = []
1288 1284 if self.parent_group is None:
1289 1285 return groups
1290 1286 cur_gr = self.parent_group
1291 1287 groups.insert(0, cur_gr)
1292 1288 cnt = 0
1293 1289 while 1:
1294 1290 cnt += 1
1295 1291 gr = getattr(cur_gr, 'parent_group', None)
1296 1292 cur_gr = cur_gr.parent_group
1297 1293 if gr is None:
1298 1294 break
1299 1295 if cnt == parents_recursion_limit:
1300 1296 # this will prevent accidental infinit loops
1301 1297 log.error('group nested more than %s' %
1302 1298 parents_recursion_limit)
1303 1299 break
1304 1300
1305 1301 groups.insert(0, gr)
1306 1302 return groups
1307 1303
1308 1304 @property
1309 1305 def children(self):
1310 1306 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1311 1307
1312 1308 @property
1313 1309 def name(self):
1314 1310 return self.group_name.split(RepoGroup.url_sep())[-1]
1315 1311
1316 1312 @property
1317 1313 def full_path(self):
1318 1314 return self.group_name
1319 1315
1320 1316 @property
1321 1317 def full_path_splitted(self):
1322 1318 return self.group_name.split(RepoGroup.url_sep())
1323 1319
1324 1320 @property
1325 1321 def repositories(self):
1326 1322 return Repository.query()\
1327 1323 .filter(Repository.group == self)\
1328 1324 .order_by(Repository.repo_name)
1329 1325
1330 1326 @property
1331 1327 def repositories_recursive_count(self):
1332 1328 cnt = self.repositories.count()
1333 1329
1334 1330 def children_count(group):
1335 1331 cnt = 0
1336 1332 for child in group.children:
1337 1333 cnt += child.repositories.count()
1338 1334 cnt += children_count(child)
1339 1335 return cnt
1340 1336
1341 1337 return cnt + children_count(self)
1342 1338
1343 1339 def _recursive_objects(self, include_repos=True):
1344 1340 all_ = []
1345 1341
1346 1342 def _get_members(root_gr):
1347 1343 if include_repos:
1348 1344 for r in root_gr.repositories:
1349 1345 all_.append(r)
1350 1346 childs = root_gr.children.all()
1351 1347 if childs:
1352 1348 for gr in childs:
1353 1349 all_.append(gr)
1354 1350 _get_members(gr)
1355 1351
1356 1352 _get_members(self)
1357 1353 return [self] + all_
1358 1354
1359 1355 def recursive_groups_and_repos(self):
1360 1356 """
1361 1357 Recursive return all groups, with repositories in those groups
1362 1358 """
1363 1359 return self._recursive_objects()
1364 1360
1365 1361 def recursive_groups(self):
1366 1362 """
1367 1363 Returns all children groups for this group including children of children
1368 1364 """
1369 1365 return self._recursive_objects(include_repos=False)
1370 1366
1371 1367 def get_new_name(self, group_name):
1372 1368 """
1373 1369 returns new full group name based on parent and new name
1374 1370
1375 1371 :param group_name:
1376 1372 """
1377 1373 path_prefix = (self.parent_group.full_path_splitted if
1378 1374 self.parent_group else [])
1379 1375 return RepoGroup.url_sep().join(path_prefix + [group_name])
1380 1376
1381 1377
1382 1378 class Permission(Base, BaseModel):
1383 1379 __tablename__ = 'permissions'
1384 1380 __table_args__ = (
1385 1381 Index('p_perm_name_idx', 'permission_name'),
1386 1382 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1387 1383 'mysql_charset': 'utf8'},
1388 1384 )
1389 1385 PERMS = [
1390 1386 ('repository.none', _('Repository no access')),
1391 1387 ('repository.read', _('Repository read access')),
1392 1388 ('repository.write', _('Repository write access')),
1393 1389 ('repository.admin', _('Repository admin access')),
1394 1390
1395 1391 ('group.none', _('Repository group no access')),
1396 1392 ('group.read', _('Repository group read access')),
1397 1393 ('group.write', _('Repository group write access')),
1398 1394 ('group.admin', _('Repository group admin access')),
1399 1395
1400 1396 ('hg.admin', _('RhodeCode Administrator')),
1401 1397 ('hg.create.none', _('Repository creation disabled')),
1402 1398 ('hg.create.repository', _('Repository creation enabled')),
1403 1399 ('hg.fork.none', _('Repository forking disabled')),
1404 1400 ('hg.fork.repository', _('Repository forking enabled')),
1405 1401 ('hg.register.none', _('Register disabled')),
1406 1402 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1407 1403 'with manual activation')),
1408 1404
1409 1405 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1410 1406 'with auto activation')),
1411 1407 ]
1412 1408
1413 1409 # defines which permissions are more important higher the more important
1414 1410 PERM_WEIGHTS = {
1415 1411 'repository.none': 0,
1416 1412 'repository.read': 1,
1417 1413 'repository.write': 3,
1418 1414 'repository.admin': 4,
1419 1415
1420 1416 'group.none': 0,
1421 1417 'group.read': 1,
1422 1418 'group.write': 3,
1423 1419 'group.admin': 4,
1424 1420
1425 1421 'hg.fork.none': 0,
1426 1422 'hg.fork.repository': 1,
1427 1423 'hg.create.none': 0,
1428 1424 'hg.create.repository':1
1429 1425 }
1430 1426
1431 1427 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1432 1428 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1433 1429 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1434 1430
1435 1431 def __unicode__(self):
1436 1432 return u"<%s('%s:%s')>" % (
1437 1433 self.__class__.__name__, self.permission_id, self.permission_name
1438 1434 )
1439 1435
1440 1436 @classmethod
1441 1437 def get_by_key(cls, key):
1442 1438 return cls.query().filter(cls.permission_name == key).scalar()
1443 1439
1444 1440 @classmethod
1445 1441 def get_default_perms(cls, default_user_id):
1446 1442 q = Session().query(UserRepoToPerm, Repository, cls)\
1447 1443 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1448 1444 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1449 1445 .filter(UserRepoToPerm.user_id == default_user_id)
1450 1446
1451 1447 return q.all()
1452 1448
1453 1449 @classmethod
1454 1450 def get_default_group_perms(cls, default_user_id):
1455 1451 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1456 1452 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1457 1453 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1458 1454 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1459 1455
1460 1456 return q.all()
1461 1457
1462 1458
1463 1459 class UserRepoToPerm(Base, BaseModel):
1464 1460 __tablename__ = 'repo_to_perm'
1465 1461 __table_args__ = (
1466 1462 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1467 1463 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1468 1464 'mysql_charset': 'utf8'}
1469 1465 )
1470 1466 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1471 1467 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1472 1468 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1473 1469 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1474 1470
1475 1471 user = relationship('User')
1476 1472 repository = relationship('Repository')
1477 1473 permission = relationship('Permission')
1478 1474
1479 1475 @classmethod
1480 1476 def create(cls, user, repository, permission):
1481 1477 n = cls()
1482 1478 n.user = user
1483 1479 n.repository = repository
1484 1480 n.permission = permission
1485 1481 Session().add(n)
1486 1482 return n
1487 1483
1488 1484 def __unicode__(self):
1489 1485 return u'<user:%s => %s >' % (self.user, self.repository)
1490 1486
1491 1487
1492 1488 class UserToPerm(Base, BaseModel):
1493 1489 __tablename__ = 'user_to_perm'
1494 1490 __table_args__ = (
1495 1491 UniqueConstraint('user_id', 'permission_id'),
1496 1492 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1497 1493 'mysql_charset': 'utf8'}
1498 1494 )
1499 1495 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1500 1496 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1501 1497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1502 1498
1503 1499 user = relationship('User')
1504 1500 permission = relationship('Permission', lazy='joined')
1505 1501
1506 1502
1507 1503 class UserGroupRepoToPerm(Base, BaseModel):
1508 1504 __tablename__ = 'users_group_repo_to_perm'
1509 1505 __table_args__ = (
1510 1506 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1511 1507 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1512 1508 'mysql_charset': 'utf8'}
1513 1509 )
1514 1510 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1515 1511 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1516 1512 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1517 1513 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1518 1514
1519 1515 users_group = relationship('UserGroup')
1520 1516 permission = relationship('Permission')
1521 1517 repository = relationship('Repository')
1522 1518
1523 1519 @classmethod
1524 1520 def create(cls, users_group, repository, permission):
1525 1521 n = cls()
1526 1522 n.users_group = users_group
1527 1523 n.repository = repository
1528 1524 n.permission = permission
1529 1525 Session().add(n)
1530 1526 return n
1531 1527
1532 1528 def __unicode__(self):
1533 1529 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1534 1530
1535 1531
1536 1532 class UserGroupToPerm(Base, BaseModel):
1537 1533 __tablename__ = 'users_group_to_perm'
1538 1534 __table_args__ = (
1539 1535 UniqueConstraint('users_group_id', 'permission_id',),
1540 1536 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1541 1537 'mysql_charset': 'utf8'}
1542 1538 )
1543 1539 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1544 1540 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1545 1541 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1546 1542
1547 1543 users_group = relationship('UserGroup')
1548 1544 permission = relationship('Permission')
1549 1545
1550 1546
1551 1547 class UserRepoGroupToPerm(Base, BaseModel):
1552 1548 __tablename__ = 'user_repo_group_to_perm'
1553 1549 __table_args__ = (
1554 1550 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1555 1551 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1556 1552 'mysql_charset': 'utf8'}
1557 1553 )
1558 1554
1559 1555 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1560 1556 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1561 1557 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1562 1558 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1563 1559
1564 1560 user = relationship('User')
1565 1561 group = relationship('RepoGroup')
1566 1562 permission = relationship('Permission')
1567 1563
1568 1564
1569 1565 class UserGroupRepoGroupToPerm(Base, BaseModel):
1570 1566 __tablename__ = 'users_group_repo_group_to_perm'
1571 1567 __table_args__ = (
1572 1568 UniqueConstraint('users_group_id', 'group_id'),
1573 1569 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1574 1570 'mysql_charset': 'utf8'}
1575 1571 )
1576 1572
1577 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 1574 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1579 1575 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1580 1576 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1581 1577
1582 1578 users_group = relationship('UserGroup')
1583 1579 permission = relationship('Permission')
1584 1580 group = relationship('RepoGroup')
1585 1581
1586 1582
1587 1583 class Statistics(Base, BaseModel):
1588 1584 __tablename__ = 'statistics'
1589 1585 __table_args__ = (
1590 1586 UniqueConstraint('repository_id'),
1591 1587 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1592 1588 'mysql_charset': 'utf8'}
1593 1589 )
1594 1590 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1595 1591 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1596 1592 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1597 1593 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1598 1594 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1599 1595 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1600 1596
1601 1597 repository = relationship('Repository', single_parent=True)
1602 1598
1603 1599
1604 1600 class UserFollowing(Base, BaseModel):
1605 1601 __tablename__ = 'user_followings'
1606 1602 __table_args__ = (
1607 1603 UniqueConstraint('user_id', 'follows_repository_id'),
1608 1604 UniqueConstraint('user_id', 'follows_user_id'),
1609 1605 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1610 1606 'mysql_charset': 'utf8'}
1611 1607 )
1612 1608
1613 1609 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1614 1610 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1615 1611 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1616 1612 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1617 1613 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1618 1614
1619 1615 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1620 1616
1621 1617 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1622 1618 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1623 1619
1624 1620 @classmethod
1625 1621 def get_repo_followers(cls, repo_id):
1626 1622 return cls.query().filter(cls.follows_repo_id == repo_id)
1627 1623
1628 1624
1629 1625 class CacheInvalidation(Base, BaseModel):
1630 1626 __tablename__ = 'cache_invalidation'
1631 1627 __table_args__ = (
1632 1628 UniqueConstraint('cache_key'),
1633 1629 Index('key_idx', 'cache_key'),
1634 1630 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1635 1631 'mysql_charset': 'utf8'},
1636 1632 )
1637 1633 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1638 1634 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1639 1635 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1640 1636 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1641 1637
1642 1638 def __init__(self, cache_key, cache_args=''):
1643 1639 self.cache_key = cache_key
1644 1640 self.cache_args = cache_args
1645 1641 self.cache_active = False
1646 1642
1647 1643 def __unicode__(self):
1648 1644 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1649 1645 self.cache_id, self.cache_key)
1650 1646
1651 1647 @property
1652 1648 def prefix(self):
1653 1649 _split = self.cache_key.split(self.cache_args, 1)
1654 1650 if _split and len(_split) == 2:
1655 1651 return _split[0]
1656 1652 return ''
1657 1653
1658 1654 @classmethod
1659 1655 def clear_cache(cls):
1660 1656 cls.query().delete()
1661 1657
1662 1658 @classmethod
1663 1659 def _get_key(cls, key):
1664 1660 """
1665 1661 Wrapper for generating a key, together with a prefix
1666 1662
1667 1663 :param key:
1668 1664 """
1669 1665 import rhodecode
1670 1666 prefix = ''
1671 1667 org_key = key
1672 1668 iid = rhodecode.CONFIG.get('instance_id')
1673 1669 if iid:
1674 1670 prefix = iid
1675 1671
1676 1672 return "%s%s" % (prefix, key), prefix, org_key
1677 1673
1678 1674 @classmethod
1679 1675 def get_by_key(cls, key):
1680 1676 return cls.query().filter(cls.cache_key == key).scalar()
1681 1677
1682 1678 @classmethod
1683 1679 def get_by_repo_name(cls, repo_name):
1684 1680 return cls.query().filter(cls.cache_args == repo_name).all()
1685 1681
1686 1682 @classmethod
1687 1683 def _get_or_create_key(cls, key, repo_name, commit=True):
1688 1684 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1689 1685 if not inv_obj:
1690 1686 try:
1691 1687 inv_obj = CacheInvalidation(key, repo_name)
1692 1688 Session().add(inv_obj)
1693 1689 if commit:
1694 1690 Session().commit()
1695 1691 except Exception:
1696 1692 log.error(traceback.format_exc())
1697 1693 Session().rollback()
1698 1694 return inv_obj
1699 1695
1700 1696 @classmethod
1701 1697 def invalidate(cls, key):
1702 1698 """
1703 1699 Returns Invalidation object if this given key should be invalidated
1704 1700 None otherwise. `cache_active = False` means that this cache
1705 1701 state is not valid and needs to be invalidated
1706 1702
1707 1703 :param key:
1708 1704 """
1709 1705 repo_name = key
1710 1706 repo_name = remove_suffix(repo_name, '_README')
1711 1707 repo_name = remove_suffix(repo_name, '_RSS')
1712 1708 repo_name = remove_suffix(repo_name, '_ATOM')
1713 1709
1714 1710 # adds instance prefix
1715 1711 key, _prefix, _org_key = cls._get_key(key)
1716 1712 inv = cls._get_or_create_key(key, repo_name)
1717 1713
1718 1714 if inv and inv.cache_active is False:
1719 1715 return inv
1720 1716
1721 1717 @classmethod
1722 1718 def set_invalidate(cls, key=None, repo_name=None):
1723 1719 """
1724 1720 Mark this Cache key for invalidation, either by key or whole
1725 1721 cache sets based on repo_name
1726 1722
1727 1723 :param key:
1728 1724 """
1729 1725 invalidated_keys = []
1730 1726 if key:
1731 1727 key, _prefix, _org_key = cls._get_key(key)
1732 1728 inv_objs = Session().query(cls).filter(cls.cache_key == key).all()
1733 1729 elif repo_name:
1734 1730 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1735 1731
1736 1732 try:
1737 1733 for inv_obj in inv_objs:
1738 1734 inv_obj.cache_active = False
1739 1735 log.debug('marking %s key for invalidation based on key=%s,repo_name=%s'
1740 1736 % (inv_obj, key, safe_str(repo_name)))
1741 1737 invalidated_keys.append(inv_obj.cache_key)
1742 1738 Session().add(inv_obj)
1743 1739 Session().commit()
1744 1740 except Exception:
1745 1741 log.error(traceback.format_exc())
1746 1742 Session().rollback()
1747 1743 return invalidated_keys
1748 1744
1749 1745 @classmethod
1750 1746 def set_valid(cls, key):
1751 1747 """
1752 1748 Mark this cache key as active and currently cached
1753 1749
1754 1750 :param key:
1755 1751 """
1756 1752 inv_obj = cls.get_by_key(key)
1757 1753 inv_obj.cache_active = True
1758 1754 Session().add(inv_obj)
1759 1755 Session().commit()
1760 1756
1761 1757 @classmethod
1762 1758 def get_cache_map(cls):
1763 1759
1764 1760 class cachemapdict(dict):
1765 1761
1766 1762 def __init__(self, *args, **kwargs):
1767 1763 fixkey = kwargs.get('fixkey')
1768 1764 if fixkey:
1769 1765 del kwargs['fixkey']
1770 1766 self.fixkey = fixkey
1771 1767 super(cachemapdict, self).__init__(*args, **kwargs)
1772 1768
1773 1769 def __getattr__(self, name):
1774 1770 key = name
1775 1771 if self.fixkey:
1776 1772 key, _prefix, _org_key = cls._get_key(key)
1777 1773 if key in self.__dict__:
1778 1774 return self.__dict__[key]
1779 1775 else:
1780 1776 return self[key]
1781 1777
1782 1778 def __getitem__(self, key):
1783 1779 if self.fixkey:
1784 1780 key, _prefix, _org_key = cls._get_key(key)
1785 1781 try:
1786 1782 return super(cachemapdict, self).__getitem__(key)
1787 1783 except KeyError:
1788 1784 return
1789 1785
1790 1786 cache_map = cachemapdict(fixkey=True)
1791 1787 for obj in cls.query().all():
1792 1788 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1793 1789 return cache_map
1794 1790
1795 1791
1796 1792 class ChangesetComment(Base, BaseModel):
1797 1793 __tablename__ = 'changeset_comments'
1798 1794 __table_args__ = (
1799 1795 Index('cc_revision_idx', 'revision'),
1800 1796 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1801 1797 'mysql_charset': 'utf8'},
1802 1798 )
1803 1799 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1804 1800 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1805 1801 revision = Column('revision', String(40), nullable=True)
1806 1802 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1807 1803 line_no = Column('line_no', Unicode(10), nullable=True)
1808 1804 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1809 1805 f_path = Column('f_path', Unicode(1000), nullable=True)
1810 1806 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1811 1807 text = Column('text', UnicodeText(25000), nullable=False)
1812 1808 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1813 1809 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1814 1810
1815 1811 author = relationship('User', lazy='joined')
1816 1812 repo = relationship('Repository')
1817 1813 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1818 1814 pull_request = relationship('PullRequest', lazy='joined')
1819 1815
1820 1816 @classmethod
1821 1817 def get_users(cls, revision=None, pull_request_id=None):
1822 1818 """
1823 1819 Returns user associated with this ChangesetComment. ie those
1824 1820 who actually commented
1825 1821
1826 1822 :param cls:
1827 1823 :param revision:
1828 1824 """
1829 1825 q = Session().query(User)\
1830 1826 .join(ChangesetComment.author)
1831 1827 if revision:
1832 1828 q = q.filter(cls.revision == revision)
1833 1829 elif pull_request_id:
1834 1830 q = q.filter(cls.pull_request_id == pull_request_id)
1835 1831 return q.all()
1836 1832
1837 1833
1838 1834 class ChangesetStatus(Base, BaseModel):
1839 1835 __tablename__ = 'changeset_statuses'
1840 1836 __table_args__ = (
1841 1837 Index('cs_revision_idx', 'revision'),
1842 1838 Index('cs_version_idx', 'version'),
1843 1839 UniqueConstraint('repo_id', 'revision', 'version'),
1844 1840 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1845 1841 'mysql_charset': 'utf8'}
1846 1842 )
1847 1843 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1848 1844 STATUS_APPROVED = 'approved'
1849 1845 STATUS_REJECTED = 'rejected'
1850 1846 STATUS_UNDER_REVIEW = 'under_review'
1851 1847
1852 1848 STATUSES = [
1853 1849 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1854 1850 (STATUS_APPROVED, _("Approved")),
1855 1851 (STATUS_REJECTED, _("Rejected")),
1856 1852 (STATUS_UNDER_REVIEW, _("Under Review")),
1857 1853 ]
1858 1854
1859 1855 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1860 1856 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1861 1857 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1862 1858 revision = Column('revision', String(40), nullable=False)
1863 1859 status = Column('status', String(128), nullable=False, default=DEFAULT)
1864 1860 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1865 1861 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1866 1862 version = Column('version', Integer(), nullable=False, default=0)
1867 1863 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1868 1864
1869 1865 author = relationship('User', lazy='joined')
1870 1866 repo = relationship('Repository')
1871 1867 comment = relationship('ChangesetComment', lazy='joined')
1872 1868 pull_request = relationship('PullRequest', lazy='joined')
1873 1869
1874 1870 def __unicode__(self):
1875 1871 return u"<%s('%s:%s')>" % (
1876 1872 self.__class__.__name__,
1877 1873 self.status, self.author
1878 1874 )
1879 1875
1880 1876 @classmethod
1881 1877 def get_status_lbl(cls, value):
1882 1878 return dict(cls.STATUSES).get(value)
1883 1879
1884 1880 @property
1885 1881 def status_lbl(self):
1886 1882 return ChangesetStatus.get_status_lbl(self.status)
1887 1883
1888 1884
1889 1885 class PullRequest(Base, BaseModel):
1890 1886 __tablename__ = 'pull_requests'
1891 1887 __table_args__ = (
1892 1888 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1893 1889 'mysql_charset': 'utf8'},
1894 1890 )
1895 1891
1896 1892 STATUS_NEW = u'new'
1897 1893 STATUS_OPEN = u'open'
1898 1894 STATUS_CLOSED = u'closed'
1899 1895
1900 1896 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1901 1897 title = Column('title', Unicode(256), nullable=True)
1902 1898 description = Column('description', UnicodeText(10240), nullable=True)
1903 1899 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1904 1900 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1905 1901 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1906 1902 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1907 1903 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1908 1904 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1909 1905 org_ref = Column('org_ref', Unicode(256), nullable=False)
1910 1906 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1911 1907 other_ref = Column('other_ref', Unicode(256), nullable=False)
1912 1908
1913 1909 @hybrid_property
1914 1910 def revisions(self):
1915 1911 return self._revisions.split(':')
1916 1912
1917 1913 @revisions.setter
1918 1914 def revisions(self, val):
1919 1915 self._revisions = ':'.join(val)
1920 1916
1921 1917 @property
1922 1918 def org_ref_parts(self):
1923 1919 return self.org_ref.split(':')
1924 1920
1925 1921 @property
1926 1922 def other_ref_parts(self):
1927 1923 return self.other_ref.split(':')
1928 1924
1929 1925 author = relationship('User', lazy='joined')
1930 1926 reviewers = relationship('PullRequestReviewers',
1931 1927 cascade="all, delete, delete-orphan")
1932 1928 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1933 1929 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1934 1930 statuses = relationship('ChangesetStatus')
1935 1931 comments = relationship('ChangesetComment',
1936 1932 cascade="all, delete, delete-orphan")
1937 1933
1938 1934 def is_closed(self):
1939 1935 return self.status == self.STATUS_CLOSED
1940 1936
1941 1937 @property
1942 1938 def last_review_status(self):
1943 1939 return self.statuses[-1].status if self.statuses else ''
1944 1940
1945 1941 def __json__(self):
1946 1942 return dict(
1947 1943 revisions=self.revisions
1948 1944 )
1949 1945
1950 1946
1951 1947 class PullRequestReviewers(Base, BaseModel):
1952 1948 __tablename__ = 'pull_request_reviewers'
1953 1949 __table_args__ = (
1954 1950 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1955 1951 'mysql_charset': 'utf8'},
1956 1952 )
1957 1953
1958 1954 def __init__(self, user=None, pull_request=None):
1959 1955 self.user = user
1960 1956 self.pull_request = pull_request
1961 1957
1962 1958 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1963 1959 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1964 1960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1965 1961
1966 1962 user = relationship('User')
1967 1963 pull_request = relationship('PullRequest')
1968 1964
1969 1965
1970 1966 class Notification(Base, BaseModel):
1971 1967 __tablename__ = 'notifications'
1972 1968 __table_args__ = (
1973 1969 Index('notification_type_idx', 'type'),
1974 1970 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1975 1971 'mysql_charset': 'utf8'},
1976 1972 )
1977 1973
1978 1974 TYPE_CHANGESET_COMMENT = u'cs_comment'
1979 1975 TYPE_MESSAGE = u'message'
1980 1976 TYPE_MENTION = u'mention'
1981 1977 TYPE_REGISTRATION = u'registration'
1982 1978 TYPE_PULL_REQUEST = u'pull_request'
1983 1979 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1984 1980
1985 1981 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1986 1982 subject = Column('subject', Unicode(512), nullable=True)
1987 1983 body = Column('body', UnicodeText(50000), nullable=True)
1988 1984 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1989 1985 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1990 1986 type_ = Column('type', Unicode(256))
1991 1987
1992 1988 created_by_user = relationship('User')
1993 1989 notifications_to_users = relationship('UserNotification', lazy='joined',
1994 1990 cascade="all, delete, delete-orphan")
1995 1991
1996 1992 @property
1997 1993 def recipients(self):
1998 1994 return [x.user for x in UserNotification.query()\
1999 1995 .filter(UserNotification.notification == self)\
2000 1996 .order_by(UserNotification.user_id.asc()).all()]
2001 1997
2002 1998 @classmethod
2003 1999 def create(cls, created_by, subject, body, recipients, type_=None):
2004 2000 if type_ is None:
2005 2001 type_ = Notification.TYPE_MESSAGE
2006 2002
2007 2003 notification = cls()
2008 2004 notification.created_by_user = created_by
2009 2005 notification.subject = subject
2010 2006 notification.body = body
2011 2007 notification.type_ = type_
2012 2008 notification.created_on = datetime.datetime.now()
2013 2009
2014 2010 for u in recipients:
2015 2011 assoc = UserNotification()
2016 2012 assoc.notification = notification
2017 2013 u.notifications.append(assoc)
2018 2014 Session().add(notification)
2019 2015 return notification
2020 2016
2021 2017 @property
2022 2018 def description(self):
2023 2019 from rhodecode.model.notification import NotificationModel
2024 2020 return NotificationModel().make_description(self)
2025 2021
2026 2022
2027 2023 class UserNotification(Base, BaseModel):
2028 2024 __tablename__ = 'user_to_notification'
2029 2025 __table_args__ = (
2030 2026 UniqueConstraint('user_id', 'notification_id'),
2031 2027 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2032 2028 'mysql_charset': 'utf8'}
2033 2029 )
2034 2030 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2035 2031 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2036 2032 read = Column('read', Boolean, default=False)
2037 2033 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2038 2034
2039 2035 user = relationship('User', lazy="joined")
2040 2036 notification = relationship('Notification', lazy="joined",
2041 2037 order_by=lambda: Notification.created_on.desc(),)
2042 2038
2043 2039 def mark_as_read(self):
2044 2040 self.read = True
2045 2041 Session().add(self)
2046 2042
2047 2043
2048 2044 class DbMigrateVersion(Base, BaseModel):
2049 2045 __tablename__ = 'db_migrate_version'
2050 2046 __table_args__ = (
2051 2047 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2052 2048 'mysql_charset': 'utf8'},
2053 2049 )
2054 2050 repository_id = Column('repository_id', String(250), primary_key=True)
2055 2051 repository_path = Column('repository_path', Text)
2056 2052 version = Column('version', Integer)
@@ -1,672 +1,673 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.scm
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Scm model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import re
28 28 import time
29 29 import traceback
30 30 import logging
31 31 import cStringIO
32 32 import pkg_resources
33 33 from os.path import dirname as dn, join as jn
34 34
35 35 from sqlalchemy import func
36 36 from pylons.i18n.translation import _
37 37
38 38 import rhodecode
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.exceptions import RepositoryError
41 41 from rhodecode.lib.vcs.utils.lazy import LazyProperty
42 42 from rhodecode.lib.vcs.nodes import FileNode
43 43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
44 44
45 45 from rhodecode import BACKENDS
46 46 from rhodecode.lib import helpers as h
47 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 49 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
49 50 from rhodecode.lib.utils import get_filesystem_repos, make_ui, \
50 51 action_logger, REMOVED_REPO_PAT
51 52 from rhodecode.model import BaseModel
52 53 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
53 54 UserFollowing, UserLog, User, RepoGroup, PullRequest
54 55 from rhodecode.lib.hooks import log_push_action
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 class UserTemp(object):
60 61 def __init__(self, user_id):
61 62 self.user_id = user_id
62 63
63 64 def __repr__(self):
64 65 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
65 66
66 67
67 68 class RepoTemp(object):
68 69 def __init__(self, repo_id):
69 70 self.repo_id = repo_id
70 71
71 72 def __repr__(self):
72 73 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
73 74
74 75
75 76 class CachedRepoList(object):
76 77 """
77 78 Cached repo list, uses in-memory cache after initialization, that is
78 79 super fast
79 80 """
80 81
81 82 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
82 83 self.db_repo_list = db_repo_list
83 84 self.repos_path = repos_path
84 85 self.order_by = order_by
85 86 self.reversed = (order_by or '').startswith('-')
86 87 if not perm_set:
87 88 perm_set = ['repository.read', 'repository.write',
88 89 'repository.admin']
89 90 self.perm_set = perm_set
90 91
91 92 def __len__(self):
92 93 return len(self.db_repo_list)
93 94
94 95 def __repr__(self):
95 96 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
96 97
97 98 def __iter__(self):
98 99 # pre-propagated cache_map to save executing select statements
99 100 # for each repo
100 101 cache_map = CacheInvalidation.get_cache_map()
101 102
102 103 for dbr in self.db_repo_list:
103 104 scmr = dbr.scm_instance_cached(cache_map)
104 105 # check permission at this level
105 106 if not HasRepoPermissionAny(
106 107 *self.perm_set
107 108 )(dbr.repo_name, 'get repo check'):
108 109 continue
109 110
110 111 try:
111 112 last_change = scmr.last_change
112 113 tip = h.get_changeset_safe(scmr, 'tip')
113 114 except Exception:
114 115 log.error(
115 116 '%s this repository is present in database but it '
116 117 'cannot be created as an scm instance, org_exc:%s'
117 118 % (dbr.repo_name, traceback.format_exc())
118 119 )
119 120 continue
120 121
121 122 tmp_d = {}
122 123 tmp_d['name'] = dbr.repo_name
123 124 tmp_d['name_sort'] = tmp_d['name'].lower()
124 125 tmp_d['raw_name'] = tmp_d['name'].lower()
125 126 tmp_d['description'] = dbr.description
126 127 tmp_d['description_sort'] = tmp_d['description'].lower()
127 128 tmp_d['last_change'] = last_change
128 129 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
129 130 tmp_d['tip'] = tip.raw_id
130 131 tmp_d['tip_sort'] = tip.revision
131 132 tmp_d['rev'] = tip.revision
132 133 tmp_d['contact'] = dbr.user.full_contact
133 134 tmp_d['contact_sort'] = tmp_d['contact']
134 135 tmp_d['owner_sort'] = tmp_d['contact']
135 136 tmp_d['repo_archives'] = list(scmr._get_archives())
136 137 tmp_d['last_msg'] = tip.message
137 138 tmp_d['author'] = tip.author
138 139 tmp_d['dbrepo'] = dbr.get_dict()
139 140 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
140 141 yield tmp_d
141 142
142 143
143 144 class SimpleCachedRepoList(CachedRepoList):
144 145 """
145 146 Lighter version of CachedRepoList without the scm initialisation
146 147 """
147 148
148 149 def __iter__(self):
149 150 for dbr in self.db_repo_list:
150 151 # check permission at this level
151 152 if not HasRepoPermissionAny(
152 153 *self.perm_set
153 154 )(dbr.repo_name, 'get repo check'):
154 155 continue
155 156
156 157 tmp_d = {}
157 158 tmp_d['name'] = dbr.repo_name
158 159 tmp_d['name_sort'] = tmp_d['name'].lower()
159 160 tmp_d['raw_name'] = tmp_d['name'].lower()
160 161 tmp_d['description'] = dbr.description
161 162 tmp_d['description_sort'] = tmp_d['description'].lower()
162 163 tmp_d['dbrepo'] = dbr.get_dict()
163 164 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
164 165 yield tmp_d
165 166
166 167
167 168 class GroupList(object):
168 169
169 170 def __init__(self, db_repo_group_list, perm_set=None):
170 171 """
171 172 Creates iterator from given list of group objects, additionally
172 173 checking permission for them from perm_set var
173 174
174 175 :param db_repo_group_list:
175 176 :param perm_set: list of permissons to check
176 177 """
177 178 self.db_repo_group_list = db_repo_group_list
178 179 if not perm_set:
179 180 perm_set = ['group.read', 'group.write', 'group.admin']
180 181 self.perm_set = perm_set
181 182
182 183 def __len__(self):
183 184 return len(self.db_repo_group_list)
184 185
185 186 def __repr__(self):
186 187 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
187 188
188 189 def __iter__(self):
189 190 for dbgr in self.db_repo_group_list:
190 191 # check permission at this level
191 192 if not HasReposGroupPermissionAny(
192 193 *self.perm_set
193 194 )(dbgr.group_name, 'get group repo check'):
194 195 continue
195 196
196 197 yield dbgr
197 198
198 199
199 200 class ScmModel(BaseModel):
200 201 """
201 202 Generic Scm Model
202 203 """
203 204
204 205 def __get_repo(self, instance):
205 206 cls = Repository
206 207 if isinstance(instance, cls):
207 208 return instance
208 209 elif isinstance(instance, int) or safe_str(instance).isdigit():
209 210 return cls.get(instance)
210 211 elif isinstance(instance, basestring):
211 212 return cls.get_by_repo_name(instance)
212 213 elif instance:
213 214 raise Exception('given object must be int, basestr or Instance'
214 215 ' of %s got %s' % (type(cls), type(instance)))
215 216
216 217 @LazyProperty
217 218 def repos_path(self):
218 219 """
219 220 Get's the repositories root path from database
220 221 """
221 222
222 223 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
223 224
224 225 return q.ui_value
225 226
226 227 def repo_scan(self, repos_path=None):
227 228 """
228 229 Listing of repositories in given path. This path should not be a
229 230 repository itself. Return a dictionary of repository objects
230 231
231 232 :param repos_path: path to directory containing repositories
232 233 """
233 234
234 235 if repos_path is None:
235 236 repos_path = self.repos_path
236 237
237 238 log.info('scanning for repositories in %s' % repos_path)
238 239
239 240 baseui = make_ui('db')
240 241 repos = {}
241 242
242 243 for name, path in get_filesystem_repos(repos_path, recursive=True):
243 244 # name need to be decomposed and put back together using the /
244 245 # since this is internal storage separator for rhodecode
245 246 name = Repository.normalize_repo_name(name)
246 247
247 248 try:
248 249 if name in repos:
249 250 raise RepositoryError('Duplicate repository name %s '
250 251 'found in %s' % (name, path))
251 252 else:
252 253
253 254 klass = get_backend(path[0])
254 255
255 256 if path[0] == 'hg' and path[0] in BACKENDS.keys():
256 257 repos[name] = klass(safe_str(path[1]), baseui=baseui)
257 258
258 259 if path[0] == 'git' and path[0] in BACKENDS.keys():
259 260 repos[name] = klass(path[1])
260 261 except OSError:
261 262 continue
262 263 log.debug('found %s paths with repositories' % (len(repos)))
263 264 return repos
264 265
265 266 def get_repos(self, all_repos=None, sort_key=None, simple=False):
266 267 """
267 268 Get all repos from db and for each repo create it's
268 269 backend instance and fill that backed with information from database
269 270
270 271 :param all_repos: list of repository names as strings
271 272 give specific repositories list, good for filtering
272 273
273 274 :param sort_key: initial sorting of repos
274 275 :param simple: use SimpleCachedList - one without the SCM info
275 276 """
276 277 if all_repos is None:
277 278 all_repos = self.sa.query(Repository)\
278 279 .filter(Repository.group_id == None)\
279 280 .order_by(func.lower(Repository.repo_name)).all()
280 281 if simple:
281 282 repo_iter = SimpleCachedRepoList(all_repos,
282 283 repos_path=self.repos_path,
283 284 order_by=sort_key)
284 285 else:
285 286 repo_iter = CachedRepoList(all_repos,
286 287 repos_path=self.repos_path,
287 288 order_by=sort_key)
288 289
289 290 return repo_iter
290 291
291 292 def get_repos_groups(self, all_groups=None):
292 293 if all_groups is None:
293 294 all_groups = RepoGroup.query()\
294 295 .filter(RepoGroup.group_parent_id == None).all()
295 296 return [x for x in GroupList(all_groups)]
296 297
297 298 def mark_for_invalidation(self, repo_name):
298 299 """
299 300 Puts cache invalidation task into db for
300 301 further global cache invalidation
301 302
302 303 :param repo_name: this repo that should invalidation take place
303 304 """
304 305 invalidated_keys = CacheInvalidation.set_invalidate(repo_name=repo_name)
305 306 repo = Repository.get_by_repo_name(repo_name)
306 307 if repo:
307 308 repo.update_changeset_cache()
308 309 return invalidated_keys
309 310
310 311 def toggle_following_repo(self, follow_repo_id, user_id):
311 312
312 313 f = self.sa.query(UserFollowing)\
313 314 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
314 315 .filter(UserFollowing.user_id == user_id).scalar()
315 316
316 317 if f is not None:
317 318 try:
318 319 self.sa.delete(f)
319 320 action_logger(UserTemp(user_id),
320 321 'stopped_following_repo',
321 322 RepoTemp(follow_repo_id))
322 323 return
323 324 except:
324 325 log.error(traceback.format_exc())
325 326 raise
326 327
327 328 try:
328 329 f = UserFollowing()
329 330 f.user_id = user_id
330 331 f.follows_repo_id = follow_repo_id
331 332 self.sa.add(f)
332 333
333 334 action_logger(UserTemp(user_id),
334 335 'started_following_repo',
335 336 RepoTemp(follow_repo_id))
336 337 except:
337 338 log.error(traceback.format_exc())
338 339 raise
339 340
340 341 def toggle_following_user(self, follow_user_id, user_id):
341 342 f = self.sa.query(UserFollowing)\
342 343 .filter(UserFollowing.follows_user_id == follow_user_id)\
343 344 .filter(UserFollowing.user_id == user_id).scalar()
344 345
345 346 if f is not None:
346 347 try:
347 348 self.sa.delete(f)
348 349 return
349 350 except:
350 351 log.error(traceback.format_exc())
351 352 raise
352 353
353 354 try:
354 355 f = UserFollowing()
355 356 f.user_id = user_id
356 357 f.follows_user_id = follow_user_id
357 358 self.sa.add(f)
358 359 except:
359 360 log.error(traceback.format_exc())
360 361 raise
361 362
362 363 def is_following_repo(self, repo_name, user_id, cache=False):
363 364 r = self.sa.query(Repository)\
364 365 .filter(Repository.repo_name == repo_name).scalar()
365 366
366 367 f = self.sa.query(UserFollowing)\
367 368 .filter(UserFollowing.follows_repository == r)\
368 369 .filter(UserFollowing.user_id == user_id).scalar()
369 370
370 371 return f is not None
371 372
372 373 def is_following_user(self, username, user_id, cache=False):
373 374 u = User.get_by_username(username)
374 375
375 376 f = self.sa.query(UserFollowing)\
376 377 .filter(UserFollowing.follows_user == u)\
377 378 .filter(UserFollowing.user_id == user_id).scalar()
378 379
379 380 return f is not None
380 381
381 382 def get_followers(self, repo):
382 383 repo = self._get_repo(repo)
383 384
384 385 return self.sa.query(UserFollowing)\
385 386 .filter(UserFollowing.follows_repository == repo).count()
386 387
387 388 def get_forks(self, repo):
388 389 repo = self._get_repo(repo)
389 390 return self.sa.query(Repository)\
390 391 .filter(Repository.fork == repo).count()
391 392
392 393 def get_pull_requests(self, repo):
393 394 repo = self._get_repo(repo)
394 395 return self.sa.query(PullRequest)\
395 396 .filter(PullRequest.other_repo == repo).count()
396 397
397 398 def mark_as_fork(self, repo, fork, user):
398 399 repo = self.__get_repo(repo)
399 400 fork = self.__get_repo(fork)
400 401 if fork and repo.repo_id == fork.repo_id:
401 402 raise Exception("Cannot set repository as fork of itself")
402 403 repo.fork = fork
403 404 self.sa.add(repo)
404 405 return repo
405 406
406 407 def _handle_push(self, repo, username, action, repo_name, revisions):
407 408 """
408 409 Triggers push action hooks
409 410
410 411 :param repo: SCM repo
411 412 :param username: username who pushes
412 413 :param action: push/push_loca/push_remote
413 414 :param repo_name: name of repo
414 415 :param revisions: list of revisions that we pushed
415 416 """
416 417 from rhodecode import CONFIG
417 418 from rhodecode.lib.base import _get_ip_addr
418 419 try:
419 420 from pylons import request
420 421 environ = request.environ
421 422 except TypeError:
422 423 # we might use this outside of request context, let's fake the
423 424 # environ data
424 425 from webob import Request
425 426 environ = Request.blank('').environ
426 427
427 428 #trigger push hook
428 429 extras = {
429 430 'ip': _get_ip_addr(environ),
430 431 'username': username,
431 432 'action': 'push_local',
432 433 'repository': repo_name,
433 434 'scm': repo.alias,
434 435 'config': CONFIG['__file__'],
435 436 'server_url': get_server_url(environ),
436 437 'make_lock': None,
437 438 'locked_by': [None, None]
438 439 }
439 440 _scm_repo = repo._repo
440 repo.inject_ui(**extras)
441 _set_extras(extras)
441 442 if repo.alias == 'hg':
442 443 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
443 444 elif repo.alias == 'git':
444 445 log_push_action(_scm_repo.ui, _scm_repo, _git_revs=revisions)
445 446
446 447 def _get_IMC_module(self, scm_type):
447 448 """
448 449 Returns InMemoryCommit class based on scm_type
449 450
450 451 :param scm_type:
451 452 """
452 453 if scm_type == 'hg':
453 454 from rhodecode.lib.vcs.backends.hg import \
454 455 MercurialInMemoryChangeset as IMC
455 456 elif scm_type == 'git':
456 457 from rhodecode.lib.vcs.backends.git import \
457 458 GitInMemoryChangeset as IMC
458 459 return IMC
459 460
460 461 def pull_changes(self, repo, username):
461 462 dbrepo = self.__get_repo(repo)
462 463 clone_uri = dbrepo.clone_uri
463 464 if not clone_uri:
464 465 raise Exception("This repository doesn't have a clone uri")
465 466
466 467 repo = dbrepo.scm_instance
467 468 repo_name = dbrepo.repo_name
468 469 try:
469 470 if repo.alias == 'git':
470 471 repo.fetch(clone_uri)
471 472 else:
472 473 repo.pull(clone_uri)
473 474 self.mark_for_invalidation(repo_name)
474 475 except:
475 476 log.error(traceback.format_exc())
476 477 raise
477 478
478 479 def commit_change(self, repo, repo_name, cs, user, author, message,
479 480 content, f_path):
480 481 """
481 482 Commits changes
482 483
483 484 :param repo: SCM instance
484 485
485 486 """
486 487 user = self._get_user(user)
487 488 IMC = self._get_IMC_module(repo.alias)
488 489
489 490 # decoding here will force that we have proper encoded values
490 491 # in any other case this will throw exceptions and deny commit
491 492 content = safe_str(content)
492 493 path = safe_str(f_path)
493 494 # message and author needs to be unicode
494 495 # proper backend should then translate that into required type
495 496 message = safe_unicode(message)
496 497 author = safe_unicode(author)
497 498 m = IMC(repo)
498 499 m.change(FileNode(path, content))
499 500 tip = m.commit(message=message,
500 501 author=author,
501 502 parents=[cs], branch=cs.branch)
502 503
503 504 self.mark_for_invalidation(repo_name)
504 505 self._handle_push(repo,
505 506 username=user.username,
506 507 action='push_local',
507 508 repo_name=repo_name,
508 509 revisions=[tip.raw_id])
509 510 return tip
510 511
511 512 def create_node(self, repo, repo_name, cs, user, author, message, content,
512 513 f_path):
513 514 user = self._get_user(user)
514 515 IMC = self._get_IMC_module(repo.alias)
515 516
516 517 # decoding here will force that we have proper encoded values
517 518 # in any other case this will throw exceptions and deny commit
518 519 if isinstance(content, (basestring,)):
519 520 content = safe_str(content)
520 521 elif isinstance(content, (file, cStringIO.OutputType,)):
521 522 content = content.read()
522 523 else:
523 524 raise Exception('Content is of unrecognized type %s' % (
524 525 type(content)
525 526 ))
526 527
527 528 message = safe_unicode(message)
528 529 author = safe_unicode(author)
529 530 path = safe_str(f_path)
530 531 m = IMC(repo)
531 532
532 533 if isinstance(cs, EmptyChangeset):
533 534 # EmptyChangeset means we we're editing empty repository
534 535 parents = None
535 536 else:
536 537 parents = [cs]
537 538
538 539 m.add(FileNode(path, content=content))
539 540 tip = m.commit(message=message,
540 541 author=author,
541 542 parents=parents, branch=cs.branch)
542 543
543 544 self.mark_for_invalidation(repo_name)
544 545 self._handle_push(repo,
545 546 username=user.username,
546 547 action='push_local',
547 548 repo_name=repo_name,
548 549 revisions=[tip.raw_id])
549 550 return tip
550 551
551 552 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
552 553 """
553 554 recursive walk in root dir and return a set of all path in that dir
554 555 based on repository walk function
555 556
556 557 :param repo_name: name of repository
557 558 :param revision: revision for which to list nodes
558 559 :param root_path: root path to list
559 560 :param flat: return as a list, if False returns a dict with decription
560 561
561 562 """
562 563 _files = list()
563 564 _dirs = list()
564 565 try:
565 566 _repo = self.__get_repo(repo_name)
566 567 changeset = _repo.scm_instance.get_changeset(revision)
567 568 root_path = root_path.lstrip('/')
568 569 for topnode, dirs, files in changeset.walk(root_path):
569 570 for f in files:
570 571 _files.append(f.path if flat else {"name": f.path,
571 572 "type": "file"})
572 573 for d in dirs:
573 574 _dirs.append(d.path if flat else {"name": d.path,
574 575 "type": "dir"})
575 576 except RepositoryError:
576 577 log.debug(traceback.format_exc())
577 578 raise
578 579
579 580 return _dirs, _files
580 581
581 582 def get_unread_journal(self):
582 583 return self.sa.query(UserLog).count()
583 584
584 585 def get_repo_landing_revs(self, repo=None):
585 586 """
586 587 Generates select option with tags branches and bookmarks (for hg only)
587 588 grouped by type
588 589
589 590 :param repo:
590 591 :type repo:
591 592 """
592 593
593 594 hist_l = []
594 595 choices = []
595 596 repo = self.__get_repo(repo)
596 597 hist_l.append(['tip', _('latest tip')])
597 598 choices.append('tip')
598 599 if not repo:
599 600 return choices, hist_l
600 601
601 602 repo = repo.scm_instance
602 603
603 604 branches_group = ([(k, k) for k, v in
604 605 repo.branches.iteritems()], _("Branches"))
605 606 hist_l.append(branches_group)
606 607 choices.extend([x[0] for x in branches_group[0]])
607 608
608 609 if repo.alias == 'hg':
609 610 bookmarks_group = ([(k, k) for k, v in
610 611 repo.bookmarks.iteritems()], _("Bookmarks"))
611 612 hist_l.append(bookmarks_group)
612 613 choices.extend([x[0] for x in bookmarks_group[0]])
613 614
614 615 tags_group = ([(k, k) for k, v in
615 616 repo.tags.iteritems()], _("Tags"))
616 617 hist_l.append(tags_group)
617 618 choices.extend([x[0] for x in tags_group[0]])
618 619
619 620 return choices, hist_l
620 621
621 622 def install_git_hook(self, repo, force_create=False):
622 623 """
623 624 Creates a rhodecode hook inside a git repository
624 625
625 626 :param repo: Instance of VCS repo
626 627 :param force_create: Create even if same name hook exists
627 628 """
628 629
629 630 loc = jn(repo.path, 'hooks')
630 631 if not repo.bare:
631 632 loc = jn(repo.path, '.git', 'hooks')
632 633 if not os.path.isdir(loc):
633 634 os.makedirs(loc)
634 635
635 636 tmpl_post = pkg_resources.resource_string(
636 637 'rhodecode', jn('config', 'post_receive_tmpl.py')
637 638 )
638 639 tmpl_pre = pkg_resources.resource_string(
639 640 'rhodecode', jn('config', 'pre_receive_tmpl.py')
640 641 )
641 642
642 643 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
643 644 _hook_file = jn(loc, '%s-receive' % h_type)
644 645 _rhodecode_hook = False
645 646 log.debug('Installing git hook in repo %s' % repo)
646 647 if os.path.exists(_hook_file):
647 648 # let's take a look at this hook, maybe it's rhodecode ?
648 649 log.debug('hook exists, checking if it is from rhodecode')
649 650 _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER')
650 651 with open(_hook_file, 'rb') as f:
651 652 data = f.read()
652 653 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
653 654 % 'RC_HOOK_VER').search(data)
654 655 if matches:
655 656 try:
656 657 ver = matches.groups()[0]
657 658 log.debug('got %s it is rhodecode' % (ver))
658 659 _rhodecode_hook = True
659 660 except:
660 661 log.error(traceback.format_exc())
661 662 else:
662 663 # there is no hook in this dir, so we want to create one
663 664 _rhodecode_hook = True
664 665
665 666 if _rhodecode_hook or force_create:
666 667 log.debug('writing %s hook file !' % h_type)
667 668 with open(_hook_file, 'wb') as f:
668 669 tmpl = tmpl.replace('_TMPL_', rhodecode.__version__)
669 670 f.write(tmpl)
670 671 os.chmod(_hook_file, 0755)
671 672 else:
672 673 log.debug('skipping writing hook file')
General Comments 0
You need to be logged in to leave comments. Login now