1#!/usr/bin/env python3 2# 3# CDDL HEADER START 4# 5# The contents of this file are subject to the terms of the 6# Common Development and Distribution License (the "License"). 7# You may not use this file except in compliance with the License. 8# 9# See LICENSE.txt included in this distribution for the specific 10# language governing permissions and limitations under the License. 11# 12# When distributing Covered Code, include this CDDL HEADER in each 13# file and include the License file at LICENSE.txt. 14# If applicable, add the following below this CDDL HEADER, with the 15# fields enclosed by brackets "[]" replaced with your own identifying 16# information: Portions Copyright [yyyy] [name of copyright owner] 17# 18# CDDL HEADER END 19# 20 21# 22# Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved. 23# 24 25import re 26import os 27import fnmatch 28import logging 29import urllib 30from tempfile import gettempdir 31 32from requests.exceptions import RequestException 33 34from .exitvals import ( 35 FAILURE_EXITVAL, 36 CONTINUE_EXITVAL, 37 SUCCESS_EXITVAL 38) 39from .patterns import PROJECT_SUBST, COMMAND_PROPERTY, CALL_PROPERTY 40from .utils import is_exe, check_create_dir, get_int, get_bool 41from .opengrok import get_repos, get_repo_type, get_uri, delete_project_data 42from .hook import run_hook 43from .command import Command 44from .commandsequence import ApiCall 45from .restful import call_rest_api, do_api_call 46 47from ..scm.repofactory import get_repository 48from ..scm.repository import RepositoryException 49 50 51# "constants" 52HOOK_TIMEOUT_PROPERTY = 'hook_timeout' 53CMD_TIMEOUT_PROPERTY = 'command_timeout' 54IGNORED_REPOS_PROPERTY = 'ignored_repos' 55PROXY_PROPERTY = 'proxy' 56INCOMING_PROPERTY = 'incoming_check' 57IGNORE_ERR_PROPERTY = 'ignore_errors' 58COMMANDS_PROPERTY = 'commands' 59DISABLED_PROPERTY = 'disabled' 60DISABLED_REASON_PROPERTY = 'disabled-reason' 61HOOKDIR_PROPERTY = 'hookdir' 62HOOKS_PROPERTY = 'hooks' 63LOGDIR_PROPERTY = 'logdir' 64PROJECTS_PROPERTY = 'projects' 65DISABLED_CMD_PROPERTY = 'disabled_command' 66HOOK_PRE_PROPERTY = "pre" 67HOOK_POST_PROPERTY = "post" 68STRIP_OUTGOING_PROPERTY = "strip_outgoing" 69 70 71def get_repos_for_project(project_name, uri, source_root, 72 ignored_repos=None, 73 commands=None, proxy=None, command_timeout=None, 74 headers=None, timeout=None): 75 """ 76 :param project_name: project name 77 :param uri: web application URI 78 :param source_root source root 79 :param ignored_repos: list of ignored repositories 80 :param commands: dictionary of commands - paths to SCM programs 81 :param proxy: dictionary of proxy servers - to be used as environment 82 variables 83 :param command_timeout: command timeout value in seconds 84 :param headers: optional HTTP headers dictionary 85 :param timeout: connect timeout for API calls 86 :return: list of Repository objects, the repository matching the project path will be first 87 """ 88 89 logger = logging.getLogger(__name__) 90 91 repos = [] 92 for repo_path in get_repos(logger, project_name, uri, 93 headers=headers, timeout=timeout): 94 logger.debug("Repository path = {}".format(repo_path)) 95 96 if ignored_repos: 97 r_path = os.path.relpath(repo_path, '/' + project_name) 98 if any(map(lambda repo: fnmatch.fnmatch(r_path, repo), 99 ignored_repos)): 100 logger.info("repository {} ignored".format(repo_path)) 101 continue 102 103 repo_type = get_repo_type(logger, repo_path, uri, 104 headers=headers, timeout=timeout) 105 if not repo_type: 106 raise RepositoryException("cannot determine type of repository {}". 107 format(repo_path)) 108 109 logger.debug("Repository type = {}".format(repo_type)) 110 111 repo = None 112 try: 113 # The OpenGrok convention is that the form of repo_path is absolute 114 # so joining the paths would actually spoil things. Hence, be 115 # careful. 116 if repo_path.startswith(os.path.sep): 117 path = source_root + repo_path 118 else: 119 path = os.path.join(source_root, repo_path) 120 121 repo = get_repository(path, 122 repo_type, 123 project_name, 124 env=proxy, 125 timeout=command_timeout, 126 commands=commands) 127 except (RepositoryException, OSError) as e: 128 logger.error("Cannot get repository for {}: {}". 129 format(repo_path, e)) 130 131 if repo: 132 if repo_path == os.path.sep + project_name: 133 repos.insert(0, repo) 134 else: 135 repos.append(repo) 136 137 return repos 138 139 140def get_project_config(config, project_name): 141 """ 142 Return per project configuration, if any. 143 :param config: global configuration 144 :param project_name name of the project 145 :return: project configuration dictionary or None 146 """ 147 148 logger = logging.getLogger(__name__) 149 150 project_config = None 151 projects = config.get(PROJECTS_PROPERTY) 152 if projects: 153 project_config = projects.get(project_name) 154 if not project_config: 155 for project_pattern in projects.keys(): 156 if re.match(project_pattern, project_name): 157 logger.debug("Project '{}' matched pattern '{}'". 158 format(project_name, project_pattern)) 159 project_config = projects.get(project_pattern) 160 break 161 162 return project_config 163 164 165def get_project_properties(project_config, project_name, hookdir): 166 """ 167 Get properties of project needed to perform mirroring. 168 :param project_config: project configuration dictionary 169 :param project_name: name of the project 170 :param hookdir: directory with hooks 171 :return: list of properties: prehook, posthook, hook_timeout, 172 command_timeout, use_proxy, ignored_repos, check_changes, strip_outgoing, ignore_errors 173 """ 174 175 prehook = None 176 posthook = None 177 hook_timeout = None 178 command_timeout = None 179 use_proxy = False 180 ignored_repos = None 181 check_changes = None 182 strip_outgoing = None 183 ignore_errors = None 184 185 logger = logging.getLogger(__name__) 186 187 if project_config: 188 logger.debug("Project '{}' has specific (non-default) config". 189 format(project_name)) 190 191 project_command_timeout = get_int(logger, "command timeout for " 192 "project {}". 193 format(project_name), 194 project_config. 195 get(CMD_TIMEOUT_PROPERTY)) 196 if project_command_timeout: 197 command_timeout = project_command_timeout 198 logger.debug("Project command timeout = {}". 199 format(command_timeout)) 200 201 project_hook_timeout = get_int(logger, "hook timeout for " 202 "project {}". 203 format(project_name), 204 project_config. 205 get(HOOK_TIMEOUT_PROPERTY)) 206 if project_hook_timeout: 207 hook_timeout = project_hook_timeout 208 logger.debug("Project hook timeout = {}". 209 format(hook_timeout)) 210 211 ignored_repos = project_config.get(IGNORED_REPOS_PROPERTY) 212 if ignored_repos: 213 logger.debug("has ignored repositories: {}". 214 format(ignored_repos)) 215 216 hooks = project_config.get(HOOKS_PROPERTY) 217 if hooks: 218 for hookname in hooks: 219 if hookname == HOOK_PRE_PROPERTY: 220 prehook = os.path.join(hookdir, hooks['pre']) 221 logger.debug("pre-hook = {}".format(prehook)) 222 elif hookname == HOOK_POST_PROPERTY: 223 posthook = os.path.join(hookdir, hooks['post']) 224 logger.debug("post-hook = {}".format(posthook)) 225 226 if project_config.get(PROXY_PROPERTY): 227 logger.debug("will use proxy") 228 use_proxy = True 229 230 if project_config.get(INCOMING_PROPERTY) is not None: 231 check_changes = get_bool(logger, ("incoming check for project {}". 232 format(project_name)), 233 project_config.get(INCOMING_PROPERTY)) 234 logger.debug("incoming check = {}".format(check_changes)) 235 236 if project_config.get(STRIP_OUTGOING_PROPERTY) is not None: 237 strip_outgoing = get_bool(logger, ("outgoing check for project {}". 238 format(project_name)), 239 project_config.get(STRIP_OUTGOING_PROPERTY)) 240 logger.debug("outgoing check = {}".format(check_changes)) 241 242 if project_config.get(IGNORE_ERR_PROPERTY) is not None: 243 ignore_errors = get_bool(logger, ("ignore errors for project {}". 244 format(project_name)), 245 project_config.get(IGNORE_ERR_PROPERTY)) 246 logger.debug("ignore errors = {}".format(check_changes)) 247 248 if not ignored_repos: 249 ignored_repos = [] 250 251 return prehook, posthook, hook_timeout, command_timeout, \ 252 use_proxy, ignored_repos, check_changes, strip_outgoing, ignore_errors 253 254 255def process_hook(hook_ident, hook, source_root, project_name, proxy, 256 hook_timeout): 257 """ 258 :param hook_ident: ident of the hook to be used in log entries 259 :param hook: hook 260 :param source_root: source root path 261 :param project_name: project name 262 :param proxy: proxy or None 263 :param hook_timeout: hook run timeout 264 :return: False if hook failed, else True 265 """ 266 if hook: 267 logger = logging.getLogger(__name__) 268 269 logger.info("Running {} hook".format(hook_ident)) 270 if run_hook(logger, hook, 271 os.path.join(source_root, project_name), proxy, 272 hook_timeout) != SUCCESS_EXITVAL: 273 logger.error("{} hook failed for project {}". 274 format(hook_ident, project_name)) 275 return False 276 277 return True 278 279 280def process_changes(repos, project_name, uri, headers=None): 281 """ 282 :param repos: repository list 283 :param project_name: project name 284 :param uri: web application URI 285 :param headers: optional dictionary of HTTP headers 286 :return: exit code 287 """ 288 logger = logging.getLogger(__name__) 289 290 changes_detected = False 291 292 logger.debug("Checking incoming changes for project {}".format(project_name)) 293 294 # check if the project is a new project - full index is necessary 295 try: 296 r = do_api_call('GET', get_uri(uri, 'api', 'v1', 'projects', 297 urllib.parse.quote_plus(project_name), 298 'property', 'indexed'), 299 headers=headers) 300 if not bool(r.json()): 301 changes_detected = True 302 logger.info('Project {} has not been indexed yet, ' 303 'overriding incoming check' 304 .format(project_name)) 305 except ValueError as e: 306 logger.error('Unable to parse project \'{}\' indexed flag: {}' 307 .format(project_name, e)) 308 return FAILURE_EXITVAL 309 except RequestException as e: 310 logger.error('Unable to determine project \'{}\' indexed flag: {}' 311 .format(project_name, e)) 312 return FAILURE_EXITVAL 313 314 # check if the project has any new changes in the SCM 315 if not changes_detected: 316 for repo in repos: 317 try: 318 if repo.incoming(): 319 logger.debug('Repository {} has incoming changes'. 320 format(repo)) 321 changes_detected = True 322 break 323 324 if repo.top_level(): 325 logger.debug('Repository {} is top level, finishing incoming check'. 326 format(repo)) 327 break 328 except RepositoryException: 329 logger.error('Cannot determine incoming changes for ' 330 'repository {}'.format(repo)) 331 return FAILURE_EXITVAL 332 333 if not changes_detected: 334 logger.info('No incoming changes for repositories in ' 335 'project {}'. 336 format(project_name)) 337 return CONTINUE_EXITVAL 338 339 return SUCCESS_EXITVAL 340 341 342def run_command(cmd, project_name): 343 cmd.execute() 344 if cmd.getretcode() != 0: 345 logger = logging.getLogger(__name__) 346 347 logger.error("Command for disabled project '{}' failed " 348 "with error code {}: {}". 349 format(project_name, cmd.getretcode(), 350 cmd.getoutputstr())) 351 352 353def handle_disabled_project(config, project_name, disabled_msg, headers=None, 354 timeout=None, api_timeout=None): 355 356 disabled_command = config.get(DISABLED_CMD_PROPERTY) 357 if disabled_command: 358 logger = logging.getLogger(__name__) 359 360 if disabled_command.get(CALL_PROPERTY): 361 call = disabled_command.get(CALL_PROPERTY) 362 api_call = ApiCall(call) 363 364 text = None 365 data = api_call.data 366 if type(data) is dict: 367 text = data.get("text") 368 369 # Is this perhaps OpenGrok API call to supply a Message for the UI ? 370 # If so and there was a string supplied, append it to the message text. 371 if text and api_call.uri.find("/api/v1/") > 0 and type(disabled_msg) is str: 372 logger.debug("Appending text to message: {}". 373 format(disabled_msg)) 374 api_call.data["text"] = text + ": " + disabled_msg 375 376 try: 377 call_rest_api(api_call, {PROJECT_SUBST: project_name}, 378 http_headers=headers, timeout=timeout, api_timeout=api_timeout) 379 except RequestException as e: 380 logger.error("API call failed for disabled command of " 381 "project '{}': {}". 382 format(project_name, e)) 383 elif disabled_command.get(COMMAND_PROPERTY): 384 command_args = disabled_command.get(COMMAND_PROPERTY) 385 args = [project_name] 386 if disabled_msg and type(disabled_msg) is str: 387 args.append(disabled_command) 388 389 cmd = Command(command_args, 390 env_vars=disabled_command.get("env"), 391 resource_limits=disabled_command.get("limits"), 392 args_subst={PROJECT_SUBST: project_name}, 393 args_append=args, excl_subst=True) 394 run_command(cmd, project_name) 395 else: 396 raise Exception(f"unknown disabled action: {disabled_command}") 397 398 399def get_mirror_retcode(ignore_errors, value): 400 if ignore_errors and value != SUCCESS_EXITVAL: 401 logger = logging.getLogger(__name__) 402 logger.info("error code is {} however '{}' property is true, " 403 "so returning success".format(value, IGNORE_ERR_PROPERTY)) 404 return SUCCESS_EXITVAL 405 406 return value 407 408 409def process_outgoing(repos, project_name): 410 """ 411 Detect and strip any outgoing changes for the repositories. 412 :param repos: list of repository objects 413 :param project_name: name of the project 414 :return: if any of the repositories had to be reset 415 """ 416 417 logger = logging.getLogger(__name__) 418 logger.info("Checking outgoing changes for project {}".format(project_name)) 419 420 ret = False 421 for repo in repos: 422 logger.debug("Checking outgoing changes for repository {}".format(repo)) 423 if repo.strip_outgoing(): 424 logger.debug('Repository {} in project {} had outgoing changes stripped'. 425 format(repo, project_name)) 426 ret = True 427 428 if repo.top_level(): 429 logger.debug('Repository {} is top level, finishing outgoing changeset handling'. 430 format(repo)) 431 break 432 433 return ret 434 435 436def mirror_project(config, project_name, check_changes, strip_outgoing, uri, 437 source_root, headers=None, timeout=None, api_timeout=None): 438 """ 439 Mirror the repositories of single project. 440 :param config global configuration dictionary 441 :param project_name: name of the project 442 :param check_changes: check for changes in the project or its repositories 443 and terminate if no change is found 444 :param strip_outgoing: check for outgoing changes in the repositories of the project, 445 strip the changes and wipe project data if such changes were found 446 :param uri web application URI 447 :param source_root source root 448 :param headers: optional dictionary of HTTP headers 449 :param timeout: connect timeout 450 :param api_timeout: optional timeout in seconds for API call response 451 :return exit code 452 """ 453 454 ret = SUCCESS_EXITVAL 455 456 logger = logging.getLogger(__name__) 457 458 project_config = get_project_config(config, project_name) 459 prehook, posthook, hook_timeout, command_timeout, use_proxy, \ 460 ignored_repos, \ 461 check_changes_proj, \ 462 strip_outgoing_proj, \ 463 ignore_errors_proj = get_project_properties(project_config, 464 project_name, 465 config. 466 get(HOOKDIR_PROPERTY)) 467 468 if not command_timeout: 469 command_timeout = config.get(CMD_TIMEOUT_PROPERTY) 470 if not hook_timeout: 471 hook_timeout = config.get(HOOK_TIMEOUT_PROPERTY) 472 473 if check_changes_proj is None: 474 check_changes_config = config.get(INCOMING_PROPERTY) 475 else: 476 check_changes_config = check_changes_proj 477 478 if strip_outgoing_proj is None: 479 strip_outgoing_config = config.get(STRIP_OUTGOING_PROPERTY) 480 else: 481 strip_outgoing_config = strip_outgoing_proj 482 483 if ignore_errors_proj is None: 484 ignore_errors = config.get(IGNORE_ERR_PROPERTY) 485 else: 486 ignore_errors = ignore_errors_proj 487 488 proxy = None 489 if use_proxy: 490 proxy = config.get(PROXY_PROPERTY) 491 492 # We want this to be logged to the log file (if any). 493 if project_config and project_config.get(DISABLED_PROPERTY): 494 handle_disabled_project(config, project_name, 495 project_config. 496 get(DISABLED_REASON_PROPERTY), 497 headers=headers, 498 timeout=timeout, 499 api_timeout=api_timeout) 500 logger.info("Project '{}' disabled, exiting". 501 format(project_name)) 502 return CONTINUE_EXITVAL 503 504 # 505 # Cache the repositories first. This way it will be known that 506 # something is not right, avoiding any needless pre-hook run. 507 # 508 repos = get_repos_for_project(project_name, 509 uri, 510 source_root, 511 ignored_repos=ignored_repos, 512 commands=config. 513 get(COMMANDS_PROPERTY), 514 proxy=proxy, 515 command_timeout=command_timeout, 516 headers=headers, 517 timeout=timeout) 518 if not repos: 519 logger.info("No repositories for project {}". 520 format(project_name)) 521 return CONTINUE_EXITVAL 522 523 if check_changes_config is not None: 524 check_changes = check_changes_config 525 526 if strip_outgoing_config is not None: 527 strip_outgoing = strip_outgoing_config 528 529 # Check outgoing changes first. If there are any, such changes will be stripped 530 # and the subsequent incoming check will do the right thing. 531 if strip_outgoing: 532 try: 533 r = process_outgoing(repos, project_name) 534 except RepositoryException as exc: 535 logger.error('Failed to handle outgoing changes for ' 536 'a repository in project {}: {}'.format(project_name, exc)) 537 return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL) 538 if r: 539 logger.info("Found outgoing changesets, removing data for project {}". 540 format(project_name)) 541 r = delete_project_data(logger, project_name, uri, 542 headers=headers, timeout=timeout, api_timeout=api_timeout) 543 if not r: 544 return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL) 545 546 # Check if the project or any of its repositories have changed. 547 if check_changes: 548 r = process_changes(repos, project_name, uri, headers=headers) 549 if r != SUCCESS_EXITVAL: 550 return get_mirror_retcode(ignore_errors, r) 551 552 if not process_hook(HOOK_PRE_PROPERTY, prehook, source_root, project_name, 553 proxy, hook_timeout): 554 return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL) 555 556 # 557 # If one of the repositories fails to sync, the whole project sync 558 # is treated as failed, i.e. the program will return FAILURE_EXITVAL. 559 # 560 for repo in repos: 561 logger.info("Synchronizing repository {}".format(repo.path)) 562 if repo.sync() != SUCCESS_EXITVAL: 563 logger.error("failed to synchronize repository {}". 564 format(repo.path)) 565 ret = FAILURE_EXITVAL 566 567 if repo.top_level(): 568 logger.debug("Repository {} is top level, breaking".format(repo)) 569 break 570 571 if not process_hook(HOOK_POST_PROPERTY, posthook, source_root, project_name, 572 proxy, hook_timeout): 573 return get_mirror_retcode(ignore_errors, FAILURE_EXITVAL) 574 575 return get_mirror_retcode(ignore_errors, ret) 576 577 578def check_project_configuration(multiple_project_config, hookdir=False, 579 proxy=False): 580 """ 581 Check configuration of given project 582 :param multiple_project_config: project configuration dictionary 583 :param hookdir: hook directory 584 :param proxy: proxy setting 585 :return: True if the configuration checks out, False otherwise 586 """ 587 588 logger = logging.getLogger(__name__) 589 590 # Quick sanity check. 591 known_project_tunables = [DISABLED_PROPERTY, CMD_TIMEOUT_PROPERTY, 592 HOOK_TIMEOUT_PROPERTY, PROXY_PROPERTY, 593 IGNORED_REPOS_PROPERTY, HOOKS_PROPERTY, 594 DISABLED_REASON_PROPERTY, INCOMING_PROPERTY, 595 IGNORE_ERR_PROPERTY, STRIP_OUTGOING_PROPERTY] 596 597 if not multiple_project_config: 598 return True 599 600 logger.debug("Checking project configuration") 601 602 for project_name, project_config in multiple_project_config.items(): 603 logger.debug("Checking configuration of project {}". 604 format(project_name)) 605 606 if project_config is None: 607 logger.warning("Project {} has empty configuration". 608 format(project_name)) 609 continue 610 611 diff = set(project_config.keys()).difference(known_project_tunables) 612 if diff: 613 logger.error("unknown project configuration option(s) '{}' " 614 "for project {}".format(diff, project_name)) 615 return False 616 617 if project_config.get(PROXY_PROPERTY) and not proxy: 618 logger.error("global proxy setting is needed in order to" 619 "have per-project proxy") 620 return False 621 622 hooks = project_config.get(HOOKS_PROPERTY) 623 if hooks: 624 if not hookdir: 625 logger.error("Need to have '{}' in the configuration " 626 "to run hooks".format(HOOKDIR_PROPERTY)) 627 return False 628 629 if not os.path.isdir(hookdir): 630 logger.error("Not a directory: {}".format(hookdir)) 631 return False 632 633 for hookname in hooks.keys(): 634 if hookname not in ["pre", "post"]: 635 logger.error("Unknown hook name '{}' for project '{}'". 636 format(hookname, project_name)) 637 return False 638 639 hookpath = os.path.join(hookdir, hooks.get(hookname)) 640 if not is_exe(hookpath): 641 logger.error("hook file {} for project '{}' does not exist" 642 " or not executable". 643 format(hookpath, project_name)) 644 return False 645 646 ignored_repos = project_config.get(IGNORED_REPOS_PROPERTY) 647 if ignored_repos: 648 if not isinstance(ignored_repos, list): 649 logger.error("{} for project {} is not a list". 650 format(IGNORED_REPOS_PROPERTY, project_name)) 651 return False 652 653 try: 654 re.compile(project_name) 655 except re.error: 656 logger.error("Not a valid regular expression: {}". 657 format(project_name)) 658 return False 659 660 return True 661 662 663def check_commands(commands): 664 """ 665 Validate the commands section of the configuration that allows 666 to override files used for SCM synchronization and incoming check. 667 This should be simple dictionary of string values. 668 :return: True if valid, False otherwise 669 """ 670 671 logger = logging.getLogger(__name__) 672 673 if commands is None: 674 return True 675 676 if type(commands) is not dict: 677 logger.error("commands sections is not a dictionary") 678 return False 679 680 for name, value in commands.items(): 681 try: 682 repo = get_repository(gettempdir(), name, "dummy_project", 683 commands=commands) 684 except RepositoryException as e: 685 logger.error("failed to check repository for '{}': '{}'". 686 format(name, e)) 687 return False 688 689 if repo is None: 690 logger.error("unknown repository type '{}' under '{}': '{}'". 691 format(name, COMMANDS_PROPERTY, value)) 692 return False 693 694 if not repo.check_command(): 695 return False 696 697 return True 698 699 700def check_configuration(config): 701 """ 702 Validate configuration 703 :param config: global configuration dictionary 704 :return: True if valid, False otherwise 705 """ 706 707 logger = logging.getLogger(__name__) 708 709 global_tunables = [HOOKDIR_PROPERTY, PROXY_PROPERTY, LOGDIR_PROPERTY, 710 COMMANDS_PROPERTY, PROJECTS_PROPERTY, 711 HOOK_TIMEOUT_PROPERTY, CMD_TIMEOUT_PROPERTY, 712 DISABLED_CMD_PROPERTY, INCOMING_PROPERTY, 713 IGNORE_ERR_PROPERTY, STRIP_OUTGOING_PROPERTY] 714 715 diff = set(config.keys()).difference(global_tunables) 716 if diff: 717 logger.error("unknown global configuration option(s): '{}'" 718 .format(diff)) 719 return False 720 721 # Make sure the log directory exists. 722 logdir = config.get(LOGDIR_PROPERTY) 723 if logdir: 724 check_create_dir(logger, logdir) 725 726 disabled_command = config.get(DISABLED_CMD_PROPERTY) 727 if disabled_command: 728 logger.debug("Disabled command: {}".format(disabled_command)) 729 730 if not check_commands(config.get(COMMANDS_PROPERTY)): 731 return False 732 733 if not check_project_configuration(config.get(PROJECTS_PROPERTY), 734 config.get(HOOKDIR_PROPERTY), 735 config.get(PROXY_PROPERTY)): 736 return False 737 738 return True 739