##// END OF EJS Templates
extensions: preparatory refactoring...
Thomas De Schampheleire -
r8420:2228102b default
parent child Browse files
Show More
@@ -1,596 +1,597 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.utils
16 16 ~~~~~~~~~~~~~~~~~~~
17 17
18 18 Utilities library for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 18, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import datetime
29 29 import logging
30 30 import os
31 31 import re
32 32 import sys
33 33 import traceback
34 34 import urllib.error
35 35 from distutils.version import StrictVersion
36 36
37 37 import mercurial.config
38 38 import mercurial.error
39 39 import mercurial.ui
40 40
41 41 import kallithea.config.conf
42 42 from kallithea.lib.exceptions import InvalidCloneUriException
43 43 from kallithea.lib.utils2 import ascii_bytes, aslist, get_current_authuser, safe_bytes, safe_str
44 44 from kallithea.lib.vcs.backends.git.repository import GitRepository
45 45 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
46 46 from kallithea.lib.vcs.conf import settings
47 47 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError
48 48 from kallithea.lib.vcs.utils.fakemod import create_module
49 49 from kallithea.lib.vcs.utils.helpers import get_scm
50 50 from kallithea.model import db, meta
51 51 from kallithea.model.db import RepoGroup, Repository, Setting, Ui, User, UserGroup, UserLog
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
57 57
58 58
59 59 #==============================================================================
60 60 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
61 61 #==============================================================================
62 62 def get_repo_slug(request):
63 63 _repo = request.environ['pylons.routes_dict'].get('repo_name')
64 64 if _repo:
65 65 _repo = _repo.rstrip('/')
66 66 return _repo
67 67
68 68
69 69 def get_repo_group_slug(request):
70 70 _group = request.environ['pylons.routes_dict'].get('group_name')
71 71 if _group:
72 72 _group = _group.rstrip('/')
73 73 return _group
74 74
75 75
76 76 def get_user_group_slug(request):
77 77 _group = request.environ['pylons.routes_dict'].get('id')
78 78 _group = UserGroup.get(_group)
79 79 if _group:
80 80 return _group.users_group_name
81 81 return None
82 82
83 83
84 84 def _get_permanent_id(s):
85 85 """Helper for decoding stable URLs with repo ID. For a string like '_123'
86 86 return 123.
87 87 """
88 88 by_id_match = re.match(r'^_(\d+)$', s)
89 89 if by_id_match is None:
90 90 return None
91 91 return int(by_id_match.group(1))
92 92
93 93
94 94 def fix_repo_id_name(path):
95 95 """
96 96 Rewrite repo_name for _<ID> permanent URLs.
97 97
98 98 Given a path, if the first path element is like _<ID>, return the path with
99 99 this part expanded to the corresponding full repo name, else return the
100 100 provided path.
101 101 """
102 102 first, rest = path, ''
103 103 if '/' in path:
104 104 first, rest_ = path.split('/', 1)
105 105 rest = '/' + rest_
106 106 repo_id = _get_permanent_id(first)
107 107 if repo_id is not None:
108 108 repo = Repository.get(repo_id)
109 109 if repo is not None:
110 110 return repo.repo_name + rest
111 111 return path
112 112
113 113
114 114 def action_logger(user, action, repo, ipaddr='', commit=False):
115 115 """
116 116 Action logger for various actions made by users
117 117
118 118 :param user: user that made this action, can be a unique username string or
119 119 object containing user_id attribute
120 120 :param action: action to log, should be on of predefined unique actions for
121 121 easy translations
122 122 :param repo: string name of repository or object containing repo_id,
123 123 that action was made on
124 124 :param ipaddr: optional IP address from what the action was made
125 125
126 126 """
127 127
128 128 # if we don't get explicit IP address try to get one from registered user
129 129 # in tmpl context var
130 130 if not ipaddr:
131 131 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
132 132
133 133 if getattr(user, 'user_id', None):
134 134 user_obj = User.get(user.user_id)
135 135 elif isinstance(user, str):
136 136 user_obj = User.get_by_username(user)
137 137 else:
138 138 raise Exception('You have to provide a user object or a username')
139 139
140 140 if getattr(repo, 'repo_id', None):
141 141 repo_obj = Repository.get(repo.repo_id)
142 142 repo_name = repo_obj.repo_name
143 143 elif isinstance(repo, str):
144 144 repo_name = repo.lstrip('/')
145 145 repo_obj = Repository.get_by_repo_name(repo_name)
146 146 else:
147 147 repo_obj = None
148 148 repo_name = ''
149 149
150 150 user_log = UserLog()
151 151 user_log.user_id = user_obj.user_id
152 152 user_log.username = user_obj.username
153 153 user_log.action = action
154 154
155 155 user_log.repository = repo_obj
156 156 user_log.repository_name = repo_name
157 157
158 158 user_log.action_date = datetime.datetime.now()
159 159 user_log.user_ip = ipaddr
160 160 meta.Session().add(user_log)
161 161
162 162 log.info('Logging action:%s on %s by user:%s ip:%s',
163 163 action, repo, user_obj, ipaddr)
164 164 if commit:
165 165 meta.Session().commit()
166 166
167 167
168 168 def get_filesystem_repos(path):
169 169 """
170 170 Scans given path for repos and return (name,(type,path)) tuple
171 171
172 172 :param path: path to scan for repositories
173 173 :param recursive: recursive search and return names with subdirs in front
174 174 """
175 175
176 176 # remove ending slash for better results
177 177 path = path.rstrip(os.sep)
178 178 log.debug('now scanning in %s', path)
179 179
180 180 def isdir(*n):
181 181 return os.path.isdir(os.path.join(*n))
182 182
183 183 for root, dirs, _files in os.walk(path):
184 184 recurse_dirs = []
185 185 for subdir in dirs:
186 186 # skip removed repos
187 187 if REMOVED_REPO_PAT.match(subdir):
188 188 continue
189 189
190 190 # skip .<something> dirs TODO: rly? then we should prevent creating them ...
191 191 if subdir.startswith('.'):
192 192 continue
193 193
194 194 cur_path = os.path.join(root, subdir)
195 195 if isdir(cur_path, '.git'):
196 196 log.warning('ignoring non-bare Git repo: %s', cur_path)
197 197 continue
198 198
199 199 if (isdir(cur_path, '.hg') or
200 200 isdir(cur_path, '.svn') or
201 201 isdir(cur_path, 'objects') and (isdir(cur_path, 'refs') or
202 202 os.path.isfile(os.path.join(cur_path, 'packed-refs')))):
203 203
204 204 if not os.access(cur_path, os.R_OK) or not os.access(cur_path, os.X_OK):
205 205 log.warning('ignoring repo path without access: %s', cur_path)
206 206 continue
207 207
208 208 if not os.access(cur_path, os.W_OK):
209 209 log.warning('repo path without write access: %s', cur_path)
210 210
211 211 try:
212 212 scm_info = get_scm(cur_path)
213 213 assert cur_path.startswith(path)
214 214 repo_path = cur_path[len(path) + 1:]
215 215 yield repo_path, scm_info
216 216 continue # no recursion
217 217 except VCSError:
218 218 # We should perhaps ignore such broken repos, but especially
219 219 # the bare git detection is unreliable so we dive into it
220 220 pass
221 221
222 222 recurse_dirs.append(subdir)
223 223
224 224 dirs[:] = recurse_dirs
225 225
226 226
227 227 def is_valid_repo_uri(repo_type, url, ui):
228 228 """Check if the url seems like a valid remote repo location
229 229 Raise InvalidCloneUriException if any problems"""
230 230 if repo_type == 'hg':
231 231 if url.startswith('http') or url.startswith('ssh'):
232 232 # initially check if it's at least the proper URL
233 233 # or does it pass basic auth
234 234 try:
235 235 MercurialRepository._check_url(url, ui)
236 236 except urllib.error.URLError as e:
237 237 raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
238 238 except mercurial.error.RepoError as e:
239 239 raise InvalidCloneUriException('Mercurial %s: %s' % (type(e).__name__, safe_str(bytes(e))))
240 240 elif url.startswith('svn+http'):
241 241 try:
242 242 from hgsubversion.svnrepo import svnremoterepo
243 243 except ImportError:
244 244 raise InvalidCloneUriException('URI type %s not supported - hgsubversion is not available' % (url,))
245 245 svnremoterepo(ui, url).svn.uuid
246 246 elif url.startswith('git+http'):
247 247 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
248 248 else:
249 249 raise InvalidCloneUriException('URI %s not allowed' % (url,))
250 250
251 251 elif repo_type == 'git':
252 252 if url.startswith('http') or url.startswith('git'):
253 253 # initially check if it's at least the proper URL
254 254 # or does it pass basic auth
255 255 try:
256 256 GitRepository._check_url(url)
257 257 except urllib.error.URLError as e:
258 258 raise InvalidCloneUriException('URI %s URLError: %s' % (url, e))
259 259 elif url.startswith('svn+http'):
260 260 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
261 261 elif url.startswith('hg+http'):
262 262 raise InvalidCloneUriException('URI type %s not implemented' % (url,))
263 263 else:
264 264 raise InvalidCloneUriException('URI %s not allowed' % (url))
265 265
266 266
267 267 def is_valid_repo(repo_name, base_path, scm=None):
268 268 """
269 269 Returns True if given path is a valid repository False otherwise.
270 270 If scm param is given also compare if given scm is the same as expected
271 271 from scm parameter
272 272
273 273 :param repo_name:
274 274 :param base_path:
275 275 :param scm:
276 276
277 277 :return True: if given path is a valid repository
278 278 """
279 279 # TODO: paranoid security checks?
280 280 full_path = os.path.join(base_path, repo_name)
281 281
282 282 try:
283 283 scm_ = get_scm(full_path)
284 284 if scm:
285 285 return scm_[0] == scm
286 286 return True
287 287 except VCSError:
288 288 return False
289 289
290 290
291 291 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
292 292 """
293 293 Returns True if given path is a repository group False otherwise
294 294
295 295 :param repo_name:
296 296 :param base_path:
297 297 """
298 298 full_path = os.path.join(base_path, repo_group_name)
299 299
300 300 # check if it's not a repo
301 301 if is_valid_repo(repo_group_name, base_path):
302 302 return False
303 303
304 304 try:
305 305 # we need to check bare git repos at higher level
306 306 # since we might match branches/hooks/info/objects or possible
307 307 # other things inside bare git repo
308 308 get_scm(os.path.dirname(full_path))
309 309 return False
310 310 except VCSError:
311 311 pass
312 312
313 313 # check if it's a valid path
314 314 if skip_path_check or os.path.isdir(full_path):
315 315 return True
316 316
317 317 return False
318 318
319 319
320 320 def make_ui(repo_path=None):
321 321 """
322 322 Create an Mercurial 'ui' object based on database Ui settings, possibly
323 323 augmenting with content from a hgrc file.
324 324 """
325 325 baseui = mercurial.ui.ui()
326 326
327 327 # clean the baseui object
328 328 baseui._ocfg = mercurial.config.config()
329 329 baseui._ucfg = mercurial.config.config()
330 330 baseui._tcfg = mercurial.config.config()
331 331
332 332 sa = meta.Session()
333 333 for ui_ in sa.query(Ui).order_by(Ui.ui_section, Ui.ui_key):
334 334 if ui_.ui_active:
335 335 log.debug('config from db: [%s] %s=%r', ui_.ui_section,
336 336 ui_.ui_key, ui_.ui_value)
337 337 baseui.setconfig(ascii_bytes(ui_.ui_section), ascii_bytes(ui_.ui_key),
338 338 b'' if ui_.ui_value is None else safe_bytes(ui_.ui_value))
339 339
340 340 # force set push_ssl requirement to False, Kallithea handles that
341 341 baseui.setconfig(b'web', b'push_ssl', False)
342 342 baseui.setconfig(b'web', b'allow_push', b'*')
343 343 # prevent interactive questions for ssh password / passphrase
344 344 ssh = baseui.config(b'ui', b'ssh', default=b'ssh')
345 345 baseui.setconfig(b'ui', b'ssh', b'%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
346 346 # push / pull hooks
347 347 baseui.setconfig(b'hooks', b'changegroup.kallithea_log_push_action', b'python:kallithea.lib.hooks.log_push_action')
348 348 baseui.setconfig(b'hooks', b'outgoing.kallithea_log_pull_action', b'python:kallithea.lib.hooks.log_pull_action')
349 349
350 350 if repo_path is not None:
351 351 # Note: MercurialRepository / mercurial.localrepo.instance will do this too, so it will always be possible to override db settings or what is hardcoded above
352 352 baseui.readconfig(repo_path)
353 353
354 354 assert baseui.plain() # set by hgcompat.monkey_do (invoked from import of vcs.backends.hg) to minimize potential impact of loading config files
355 355 return baseui
356 356
357 357
358 358 def set_app_settings(config):
359 359 """
360 360 Updates app config with new settings from database
361 361
362 362 :param config:
363 363 """
364 364 hgsettings = Setting.get_app_settings()
365 365 for k, v in hgsettings.items():
366 366 config[k] = v
367 367 config['base_path'] = Ui.get_repos_location()
368 368
369 369
370 370 def set_vcs_config(config):
371 371 """
372 372 Patch VCS config with some Kallithea specific stuff
373 373
374 374 :param config: kallithea.CONFIG
375 375 """
376 376 settings.BACKENDS = {
377 377 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
378 378 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
379 379 }
380 380
381 381 settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
382 382 settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
383 383 settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
384 384 'utf-8'), sep=',')
385 385
386 386
387 387 def set_indexer_config(config):
388 388 """
389 389 Update Whoosh index mapping
390 390
391 391 :param config: kallithea.CONFIG
392 392 """
393 393 log.debug('adding extra into INDEX_EXTENSIONS')
394 394 kallithea.config.conf.INDEX_EXTENSIONS.extend(re.split(r'\s+', config.get('index.extensions', '')))
395 395
396 396 log.debug('adding extra into INDEX_FILENAMES')
397 397 kallithea.config.conf.INDEX_FILENAMES.extend(re.split(r'\s+', config.get('index.filenames', '')))
398 398
399 399
400 400 def map_groups(path):
401 401 """
402 402 Given a full path to a repository, create all nested groups that this
403 403 repo is inside. This function creates parent-child relationships between
404 404 groups and creates default perms for all new groups.
405 405
406 406 :param paths: full path to repository
407 407 """
408 408 from kallithea.model.repo_group import RepoGroupModel
409 409 sa = meta.Session()
410 410 groups = path.split(db.URL_SEP)
411 411 parent = None
412 412 group = None
413 413
414 414 # last element is repo in nested groups structure
415 415 groups = groups[:-1]
416 416 rgm = RepoGroupModel()
417 417 owner = User.get_first_admin()
418 418 for lvl, group_name in enumerate(groups):
419 419 group_name = '/'.join(groups[:lvl] + [group_name])
420 420 group = RepoGroup.get_by_group_name(group_name)
421 421 desc = '%s group' % group_name
422 422
423 423 # skip folders that are now removed repos
424 424 if REMOVED_REPO_PAT.match(group_name):
425 425 break
426 426
427 427 if group is None:
428 428 log.debug('creating group level: %s group_name: %s',
429 429 lvl, group_name)
430 430 group = RepoGroup(group_name, parent)
431 431 group.group_description = desc
432 432 group.owner = owner
433 433 sa.add(group)
434 434 rgm._create_default_perms(group)
435 435 sa.flush()
436 436
437 437 parent = group
438 438 return group
439 439
440 440
441 441 def repo2db_mapper(initial_repo_dict, remove_obsolete=False,
442 442 install_git_hooks=False, user=None, overwrite_git_hooks=False):
443 443 """
444 444 maps all repos given in initial_repo_dict, non existing repositories
445 445 are created, if remove_obsolete is True it also check for db entries
446 446 that are not in initial_repo_dict and removes them.
447 447
448 448 :param initial_repo_dict: mapping with repositories found by scanning methods
449 449 :param remove_obsolete: check for obsolete entries in database
450 450 :param install_git_hooks: if this is True, also check and install git hook
451 451 for a repo if missing
452 452 :param overwrite_git_hooks: if this is True, overwrite any existing git hooks
453 453 that may be encountered (even if user-deployed)
454 454 """
455 455 from kallithea.model.repo import RepoModel
456 456 from kallithea.model.scm import ScmModel
457 457 sa = meta.Session()
458 458 repo_model = RepoModel()
459 459 if user is None:
460 460 user = User.get_first_admin()
461 461 added = []
462 462
463 463 # creation defaults
464 464 defs = Setting.get_default_repo_settings(strip_prefix=True)
465 465 enable_statistics = defs.get('repo_enable_statistics')
466 466 enable_downloads = defs.get('repo_enable_downloads')
467 467 private = defs.get('repo_private')
468 468
469 469 for name, repo in sorted(initial_repo_dict.items()):
470 470 group = map_groups(name)
471 471 db_repo = repo_model.get_by_repo_name(name)
472 472 # found repo that is on filesystem not in Kallithea database
473 473 if not db_repo:
474 474 log.info('repository %s not found, creating now', name)
475 475 added.append(name)
476 476 desc = (repo.description
477 477 if repo.description != 'unknown'
478 478 else '%s repository' % name)
479 479
480 480 new_repo = repo_model._create_repo(
481 481 repo_name=name,
482 482 repo_type=repo.alias,
483 483 description=desc,
484 484 repo_group=getattr(group, 'group_id', None),
485 485 owner=user,
486 486 enable_downloads=enable_downloads,
487 487 enable_statistics=enable_statistics,
488 488 private=private,
489 489 state=Repository.STATE_CREATED
490 490 )
491 491 sa.commit()
492 492 # we added that repo just now, and make sure it has githook
493 493 # installed, and updated server info
494 494 if new_repo.repo_type == 'git':
495 495 git_repo = new_repo.scm_instance
496 496 ScmModel().install_git_hooks(git_repo)
497 497 # update repository server-info
498 498 log.debug('Running update server info')
499 499 git_repo._update_server_info()
500 500 new_repo.update_changeset_cache()
501 501 elif install_git_hooks:
502 502 if db_repo.repo_type == 'git':
503 503 ScmModel().install_git_hooks(db_repo.scm_instance, force=overwrite_git_hooks)
504 504
505 505 removed = []
506 506 # remove from database those repositories that are not in the filesystem
507 507 for repo in sa.query(Repository).all():
508 508 if repo.repo_name not in initial_repo_dict:
509 509 if remove_obsolete:
510 510 log.debug("Removing non-existing repository found in db `%s`",
511 511 repo.repo_name)
512 512 try:
513 513 RepoModel().delete(repo, forks='detach', fs_remove=False)
514 514 sa.commit()
515 515 except Exception:
516 516 #don't hold further removals on error
517 517 log.error(traceback.format_exc())
518 518 sa.rollback()
519 519 removed.append(repo.repo_name)
520 520 return added, removed
521 521
522 522
523 523 def load_extensions(root_path):
524 path = os.path.join(root_path, 'rcextensions', '__init__.py')
525 if os.path.isfile(path):
526 ext = create_module('rc', path)
524 try:
525 ext = create_module('rc', os.path.join(root_path, 'rcextensions', '__init__.py'))
526 except FileNotFoundError:
527 return
528
529 log.info('Loaded rcextensions from %s', ext)
527 530 kallithea.EXTENSIONS = ext
528 log.debug('Found rcextensions now loading %s...', ext)
529 531
530 532 # Additional mappings that are not present in the pygments lexers
531 533 kallithea.config.conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(ext, 'EXTRA_MAPPINGS', {}))
532 534
533 # OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
534
535 # Override any INDEX_EXTENSIONS
535 536 if getattr(ext, 'INDEX_EXTENSIONS', []):
536 537 log.debug('settings custom INDEX_EXTENSIONS')
537 538 kallithea.config.conf.INDEX_EXTENSIONS = getattr(ext, 'INDEX_EXTENSIONS', [])
538 539
539 # ADDITIONAL MAPPINGS
540 # Additional INDEX_EXTENSIONS
540 541 log.debug('adding extra into INDEX_EXTENSIONS')
541 542 kallithea.config.conf.INDEX_EXTENSIONS.extend(getattr(ext, 'EXTRA_INDEX_EXTENSIONS', []))
542 543
543 544
544 545 #==============================================================================
545 546 # MISC
546 547 #==============================================================================
547 548
548 549 git_req_ver = StrictVersion('1.7.4')
549 550
550 551 def check_git_version():
551 552 """
552 553 Checks what version of git is installed on the system, and raise a system exit
553 554 if it's too old for Kallithea to work properly.
554 555 """
555 556 if 'git' not in kallithea.BACKENDS:
556 557 return None
557 558
558 559 if not settings.GIT_EXECUTABLE_PATH:
559 560 log.warning('No git executable configured - check "git_path" in the ini file.')
560 561 return None
561 562
562 563 try:
563 564 stdout, stderr = GitRepository._run_git_command(['--version'])
564 565 except RepositoryError as e:
565 566 # message will already have been logged as error
566 567 log.warning('No working git executable found - check "git_path" in the ini file.')
567 568 return None
568 569
569 570 if stderr:
570 571 log.warning('Error/stderr from "%s --version":\n%s', settings.GIT_EXECUTABLE_PATH, safe_str(stderr))
571 572
572 573 if not stdout:
573 574 log.warning('No working git executable found - check "git_path" in the ini file.')
574 575 return None
575 576
576 577 output = safe_str(stdout).strip()
577 578 m = re.search(r"\d+.\d+.\d+", output)
578 579 if m:
579 580 ver = StrictVersion(m.group(0))
580 581 log.debug('Git executable: "%s", version %s (parsed from: "%s")',
581 582 settings.GIT_EXECUTABLE_PATH, ver, output)
582 583 if ver < git_req_ver:
583 584 log.error('Kallithea detected %s version %s, which is too old '
584 585 'for the system to function properly. '
585 586 'Please upgrade to version %s or later. '
586 587 'If you strictly need Mercurial repositories, you can '
587 588 'clear the "git_path" setting in the ini file.',
588 589 settings.GIT_EXECUTABLE_PATH, ver, git_req_ver)
589 590 log.error("Terminating ...")
590 591 sys.exit(1)
591 592 else:
592 593 ver = StrictVersion('0.0.0')
593 594 log.warning('Error finding version number in "%s --version" stdout:\n%s',
594 595 settings.GIT_EXECUTABLE_PATH, output)
595 596
596 597 return ver
General Comments 0
You need to be logged in to leave comments. Login now