""" rcextensions module. """ import os import imp import string import functools here = os.path.dirname(os.path.abspath(__file__)) registered_extensions = dict() class DotDict(dict): def __contains__(self, k): try: return dict.__contains__(self, k) or hasattr(self, k) except: return False # only called if k not found in normal places def __getattr__(self, k): try: return object.__getattribute__(self, k) except AttributeError: try: return self[k] except KeyError: raise AttributeError(k) def __setattr__(self, k, v): try: object.__getattribute__(self, k) except AttributeError: try: self[k] = v except: raise AttributeError(k) else: object.__setattr__(self, k, v) def __delattr__(self, k): try: object.__getattribute__(self, k) except AttributeError: try: del self[k] except KeyError: raise AttributeError(k) else: object.__delattr__(self, k) def toDict(self): return unserialize(self) def __repr__(self): keys = list(self.iterkeys()) keys.sort() args = ', '.join(['%s=%r' % (key, self[key]) for key in keys]) return '%s(%s)' % (self.__class__.__name__, args) @staticmethod def fromDict(d): return serialize(d) def serialize(x): if isinstance(x, dict): return DotDict((k, serialize(v)) for k, v in x.iteritems()) elif isinstance(x, (list, tuple)): return type(x)(serialize(v) for v in x) else: return x def unserialize(x): if isinstance(x, dict): return dict((k, unserialize(v)) for k, v in x.iteritems()) elif isinstance(x, (list, tuple)): return type(x)(unserialize(v) for v in x) else: return x def load_extension(filename, async=False): """ use to load extensions inside rcextension folder. for example:: callback = load_extension('email.py', async=False) if callback: callback('foobar') put file named email.py inside rcextensions folder to load it. Changing async=True will make the call of the plugin async, it's useful for blocking calls like sending an email or notification with APIs. """ mod = ''.join(filename.split('.')[:-1]) loaded = imp.load_source(mod, os.path.join(here, filename)) callback = getattr(loaded, 'run', None) if not callback: raise Exception('Plugin missing `run` method') if async: # modify callback so it's actually an async call def _async_callback(*args, **kwargs): import threading thr = threading.Thread(target=callback, args=args, kwargs=kwargs) thr.start() if kwargs.get('_async_block'): del kwargs['_async_block'] thr.join() return _async_callback return callback def _verify_kwargs(expected_parameters, kwargs): """ Verify that exactly `expected_parameters` are passed in as `kwargs`. """ expected_parameters = set(expected_parameters) kwargs_keys = set(kwargs.keys()) if kwargs_keys != expected_parameters: missing_kwargs = expected_parameters - kwargs_keys unexpected_kwargs = kwargs_keys - expected_parameters raise AssertionError( "Missing parameters: %r, unexpected parameters: %s" % (missing_kwargs, unexpected_kwargs)) def verify_kwargs(required_args): """ decorator to verify extension calls arguments. :param required_args: """ def wrap(func): def wrapper(*args, **kwargs): _verify_kwargs(required_args, kwargs) return func(*args, **kwargs) return wrapper return wrap def register(name=None): def wrap(func): @functools.wraps(func) def wrapper(*args, **kwargs): # register load_extensions in kwargs, so we can chain plugins kwargs['_load_extension'] = load_extension # append this path for us to use added plugins or modules import sys _cur_path = os.path.dirname(os.path.abspath(__file__)) if _cur_path not in sys.path: sys.path.append(_cur_path) registered_extensions[func.__name__] = func return func(*args, **kwargs) return wrapper return wrap # ============================================================================= # END OF UTILITY FUNCTIONS HERE # ============================================================================= # Additional mappings that are not present in the pygments lexers # used for building stats # format is {'ext':['Names']} eg. {'py':['Python']} note: there can be # more than one name for extension # NOTE: that this will override any mappings in LANGUAGES_EXTENSIONS_MAP # build by pygments EXTRA_MAPPINGS = {} # additional lexer definitions for custom files it's overrides pygments lexers, # and uses defined name of lexer to colorize the files. Format is {'ext': # 'lexer_name'} List of lexers can be printed running: # >> python -c "import pprint;from pygments import lexers; # pprint.pprint([(x[0], x[1]) for x in lexers.get_all_lexers()]);" EXTRA_LEXERS = {} CONFIG = DotDict( slack=DotDict( api_key='api-key', api_url='slack-incoming-hook-url', default_room='#slack-channel', default_plugin_config={}, ), redmine=DotDict( api_key='api-key', default_tracker_url='https://redmine.tracker.url', default_project_id=None, default_status_resolved_id=3 ), ) # slack conf CONFIG.slack.default_plugin_config = { 'INCOMING_WEBHOOK_URL': CONFIG.slack.api_url, 'SLACK_TOKEN': CONFIG.slack.api_key, 'SLACK_ROOM': CONFIG.slack.default_room, 'SLACK_FROM': 'RhodeCode', 'SLACK_FROM_ICON_EMOJI': ':rhodecode:', } # redmine smart_pr configuration def configure_redmine_smart_pr(issues, kwargs): kwargs['REDMINE_ISSUES'] = issues kwargs['redmine_tracker_url'] = kwargs.pop( 'redmine_tracker_url', '') or CONFIG.redmine.default_tracker_url kwargs['redmine_api_key'] = kwargs.pop( 'redmine_api_key', '') or CONFIG.redmine.api_key kwargs['redmine_project_id'] = kwargs.pop( 'redmine_project_id', '') or CONFIG.redmine.default_project_id @register('CREATE_REPO_HOOK') @verify_kwargs( ['_load_extension', 'repo_name', 'repo_type', 'description', 'private', 'created_on', 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics', 'clone_uri', 'fork_id', 'group_id', 'created_by']) def _create_repo_hook(*args, **kwargs): """ POST CREATE REPOSITORY HOOK. This function will be executed after each repository is created. kwargs available: :param repo_name: :param repo_type: :param description: :param private: :param created_on: :param enable_downloads: :param repo_id: :param user_id: :param enable_statistics: :param clone_uri: :param fork_id: :param group_id: :param created_by: """ return 0 CREATE_REPO_HOOK = _create_repo_hook @register('CREATE_REPO_GROUP_HOOK') @verify_kwargs( ['_load_extension', 'group_name', 'group_parent_id', 'group_description', 'group_id', 'user_id', 'created_by', 'created_on', 'enable_locking']) def _create_repo_group_hook(*args, **kwargs): """ POST CREATE REPOSITORY GROUP HOOK, this function will be executed after each repository group is created. kwargs available: :param group_name: :param group_parent_id: :param group_description: :param group_id: :param user_id: :param created_by: :param created_on: :param enable_locking: """ return 0 CREATE_REPO_GROUP_HOOK = _create_repo_group_hook @register('PRE_CREATE_USER_HOOK') @verify_kwargs( ['_load_extension', 'username', 'password', 'email', 'firstname', 'lastname', 'active', 'admin', 'created_by']) def _pre_create_user_hook(*args, **kwargs): """ PRE CREATE USER HOOK, this function will be executed before each user is created, it returns a tuple of bool, reason. If bool is False the user creation will be stopped and reason will be displayed to the user. kwargs available: :param username: :param password: :param email: :param firstname: :param lastname: :param active: :param admin: :param created_by: """ reason = 'allowed' return True, reason PRE_CREATE_USER_HOOK = _pre_create_user_hook @register('CREATE_USER_HOOK') @verify_kwargs( ['_load_extension', 'username', 'full_name_or_username', 'full_contact', 'user_id', 'name', 'firstname', 'short_contact', 'admin', 'lastname', 'ip_addresses', 'extern_type', 'extern_name', 'email', 'api_key', 'api_keys', 'last_login', 'full_name', 'active', 'password', 'emails', 'inherit_default_permissions', 'created_by', 'created_on']) def _create_user_hook(*args, **kwargs): """ POST CREATE USER HOOK, this function will be executed after each user is created kwargs available: :param username: :param full_name_or_username: :param full_contact: :param user_id: :param name: :param firstname: :param short_contact: :param admin: :param lastname: :param ip_addresses: :param extern_type: :param extern_name: :param email: :param api_key: :param api_keys: :param last_login: :param full_name: :param active: :param password: :param emails: :param inherit_default_permissions: :param created_by: :param created_on: """ return 0 CREATE_USER_HOOK = _create_user_hook @register('DELETE_REPO_HOOK') @verify_kwargs( ['_load_extension', 'repo_name', 'repo_type', 'description', 'private', 'created_on', 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics', 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on']) def _delete_repo_hook(*args, **kwargs): """ POST DELETE REPOSITORY HOOK, this function will be executed after each repository deletion kwargs available: :param repo_name: :param repo_type: :param description: :param private: :param created_on: :param enable_downloads: :param repo_id: :param user_id: :param enable_statistics: :param clone_uri: :param fork_id: :param group_id: :param deleted_by: :param deleted_on: """ return 0 DELETE_REPO_HOOK = _delete_repo_hook @register('DELETE_USER_HOOK') @verify_kwargs( ['_load_extension', 'username', 'full_name_or_username', 'full_contact', 'user_id', 'name', 'firstname', 'short_contact', 'admin', 'lastname', 'ip_addresses', 'email', 'api_key', 'last_login', 'full_name', 'active', 'password', 'emails', 'inherit_default_permissions', 'deleted_by' ]) def _delete_user_hook(*args, **kwargs): """ POST DELETE USER HOOK, this function will be executed after each user is deleted kwargs available: :param username: :param full_name_or_username: :param full_contact: :param user_id: :param name: :param firstname: :param short_contact: :param admin: :param lastname: :param ip_addresses: :param ldap_dn: :param email: :param api_key: :param last_login: :param full_name: :param active: :param password: :param emails: :param inherit_default_permissions: :param deleted_by: """ return 0 DELETE_USER_HOOK = _delete_user_hook @register('PRE_PUSH_HOOK') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'repo_store_path']) def _pre_push_hook(*args, **kwargs): """ Post push hook kwargs available: :param server_url: url of instance that triggered this hook :param config: path to .ini config used :param scm: type of VS 'git' or 'hg' :param username: name of user who pushed :param ip: ip of who pushed :param action: push :param repository: repository name :param repo_store_path: full path to where repositories are stored """ return 0 PRE_PUSH_HOOK = _pre_push_hook @register('PUSH_HOOK') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'repo_store_path', 'pushed_revs']) def _push_hook(*args, **kwargs): """ POST PUSH HOOK, this function will be executed after each push it's executed after the build-in hook that RhodeCode uses for logging pushes kwargs available: :param server_url: url of instance that triggered this hook :param config: path to .ini config used :param scm: type of VS 'git' or 'hg' :param username: name of user who pushed :param ip: ip of who pushed :param action: push :param repository: repository name :param repo_store_path: full path to where repositories are stored :param pushed_revs: list of pushed commit ids """ # backward compat kwargs['commit_ids'] = kwargs['pushed_revs'] # fetch extra fields from repository call = load_extension('extra_fields.py') _extra_fields = {} if call: repo_extra_fields = call(**kwargs) # now update if we have extra fields, they have precedence # this way users can store any configuration inside the database per # repo for key, data in repo_extra_fields.items(): kwargs[key] = data['field_value'] _extra_fields[key] = data['field_value'] # fetch pushed commits, from commit_ids list call = load_extension('extract_commits.py') extracted_commits = {} if call: extracted_commits = call(**kwargs) # store the commits for the next call chain kwargs['COMMITS'] = extracted_commits # slack ! call = load_extension('slack_push_notify.py') if call: kwargs.update(CONFIG.slack.default_plugin_config) call(**kwargs) # fetch redmine issues from given commits call = load_extension('extract_redmine_issues.py') issues = {} if call: issues = call(**kwargs) # redmine smart commits call = load_extension('redmine_smart_commits.py') if call: kwargs['REDMINE_ISSUES'] = issues kwargs['redmine_tracker_url'] = kwargs.pop( 'redmine_tracker_url', '') or CONFIG.redmine.default_tracker_url kwargs['redmine_api_key'] = kwargs.pop( 'redmine_api_key', '') or CONFIG.redmine.api_key kwargs['redmine_status_resolved_id'] = kwargs.pop( 'redmine_status_resolved_id', '') or CONFIG.redmine.default_status_resolved_id kwargs['redmine_project_id'] = kwargs.pop( 'redmine_project_id', '') or CONFIG.redmine.default_project_id call(**kwargs) return 0 PUSH_HOOK = _push_hook @register('PRE_PULL_HOOK') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository']) def _pre_pull_hook(*args, **kwargs): """ Post pull hook kwargs available:: :param server_url: url of instance that triggered this hook :param config: path to .ini config used :param scm: type of VS 'git' or 'hg' :param username: name of user who pulled :param ip: ip of who pulled :param action: pull :param repository: repository name """ return 0 PRE_PULL_HOOK = _pre_pull_hook @register('PULL_HOOK') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository']) def _pull_hook(*args, **kwargs): """ POST PULL HOOK, this function will be executed after each push it's executed after the build-in hook that RhodeCode uses for logging pulls kwargs available: :param server_url: url of instance that triggered this hook :param config: path to .ini config used :param scm: type of VS 'git' or 'hg' :param username: name of user who pulled :param ip: ip of who pulled :param action: pull :param repository: repository name """ return 0 PULL_HOOK = _pull_hook # ============================================================================= # PULL REQUEST RELATED HOOKS # ============================================================================= @register('CREATE_PULL_REQUEST') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'pull_request_id', 'url', 'title', 'description', 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', 'mergeable', 'source', 'target', 'author', 'reviewers']) def _create_pull_request_hook(*args, **kwargs): """ """ # extract extra fields and default reviewers from target kwargs['REPOSITORY'] = kwargs['target']['repository'] call = load_extension('extra_fields.py') if call: repo_extra_fields = call(**kwargs) # now update if we have extra fields, they have precedence # this way users can store any configuration inside the database per # repo for key, data in repo_extra_fields.items(): kwargs[key] = data['field_value'] call = load_extension('default_reviewers.py') if call: # read default_reviewers key propagated from extra fields kwargs['default_reviewers'] = map(string.strip, kwargs.pop( 'default_reviewers', '').split(',')) call(**kwargs) # extract below from source repo as commits are there kwargs['REPOSITORY'] = kwargs['source']['repository'] # # fetch pushed commits, from commit_ids list # call = load_extension('extract_commits.py') # extracted_commits = {} # if call: # extracted_commits = call(**kwargs) # # store the commits for the next call chain # kwargs['COMMITS'] = extracted_commits # # # fetch issues from given commits # call = load_extension('extract_redmine_issues.py') # issues = {} # if call: # issues = call(**kwargs) # # # redmine smart pr update # call = load_extension('redmine_pr_flow.py') # if call: # # updates kwargs on the fly # configure_redmine_smart_pr(issues=issues, kwargs=kwargs) # call(**kwargs) # # # slack notification on merging PR # call = load_extension('slack_message.py') # if call: # kwargs.update(CONFIG.slack.default_plugin_config) # kwargs['SLACK_ROOM'] = '#develop' # kwargs['SLACK_MESSAGE'] = 'Pull request <%s|#%s> (%s) was created.' % ( # kwargs.get('url'), kwargs.get('pull_request_id'), kwargs.get('title')) # # call(**kwargs) return 0 CREATE_PULL_REQUEST = _create_pull_request_hook @register('REVIEW_PULL_REQUEST') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'pull_request_id', 'url', 'title', 'description', 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', 'mergeable', 'source', 'target', 'author', 'reviewers']) def _review_pull_request_hook(*args, **kwargs): """ """ # extract extra fields and default reviewers from target kwargs['REPOSITORY'] = kwargs['target']['repository'] # fetch extra fields call = load_extension('extra_fields.py') if call: repo_extra_fields = call(**kwargs) # now update if we have extra fields, they have precedence # this way users can store any configuration inside the database per # repo for key, data in repo_extra_fields.items(): kwargs[key] = data['field_value'] # extract below from source repo as commits are there kwargs['REPOSITORY'] = kwargs['source']['repository'] # fetch pushed commits, from commit_ids list call = load_extension('extract_commits.py') extracted_commits = {} if call: extracted_commits = call(**kwargs) # store the commits for the next call chain kwargs['COMMITS'] = extracted_commits # fetch issues from given commits call = load_extension('extract_redmine_issues.py') issues = {} if call: issues = call(**kwargs) # redmine smart pr update call = load_extension('redmine_pr_flow.py') if call: # updates kwargs on the fly configure_redmine_smart_pr(issues=issues, kwargs=kwargs) call(**kwargs) return 0 REVIEW_PULL_REQUEST = _review_pull_request_hook @register('UPDATE_PULL_REQUEST') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'pull_request_id', 'url', 'title', 'description', 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', 'mergeable', 'source', 'target', 'author', 'reviewers']) def _update_pull_request_hook(*args, **kwargs): """ """ # extract extra fields and default reviewers from target kwargs['REPOSITORY'] = kwargs['target']['repository'] # fetch extra fields call = load_extension('extra_fields.py') if call: repo_extra_fields = call(**kwargs) # now update if we have extra fields, they have precedence # this way users can store any configuration inside the database per # repo for key, data in repo_extra_fields.items(): kwargs[key] = data['field_value'] # extract below from source repo as commits are there kwargs['REPOSITORY'] = kwargs['source']['repository'] # fetch pushed commits, from commit_ids list call = load_extension('extract_commits.py') extracted_commits = {} if call: extracted_commits = call(**kwargs) # store the commits for the next call chain kwargs['COMMITS'] = extracted_commits # fetch issues from given commits call = load_extension('extract_redmine_issues.py') issues = {} if call: issues = call(**kwargs) # redmine smart pr updated call = load_extension('redmine_pr_flow.py') if call: # updates kwargs on the fly configure_redmine_smart_pr(issues=issues, kwargs=kwargs) call(**kwargs) return 0 UPDATE_PULL_REQUEST = _update_pull_request_hook @register('MERGE_PULL_REQUEST') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'pull_request_id', 'url', 'title', 'description', 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', 'mergeable', 'source', 'target', 'author', 'reviewers']) def _merge_pull_request_hook(*args, **kwargs): """ """ # extract extra fields and default reviewers from target kwargs['REPOSITORY'] = kwargs['target']['repository'] # fetch extra fields call = load_extension('extra_fields.py') if call: repo_extra_fields = call(**kwargs) # now update if we have extra fields, they have precedence # this way users can store any configuration inside the database per # repo for key, data in repo_extra_fields.items(): kwargs[key] = data['field_value'] # extract below from source repo as commits are there kwargs['REPOSITORY'] = kwargs['source']['repository'] # fetch pushed commits, from commit_ids list call = load_extension('extract_commits.py') extracted_commits = {} if call: extracted_commits = call(**kwargs) # store the commits for the next call chain kwargs['COMMITS'] = extracted_commits # fetch issues from given commits call = load_extension('extract_redmine_issues.py') issues = {} if call: issues = call(**kwargs) # redmine smart pr update call = load_extension('redmine_pr_flow.py') if call: # updates kwargs on the fly configure_redmine_smart_pr(issues=issues, kwargs=kwargs) call(**kwargs) # slack notification on merging PR call = load_extension('slack_message.py') if call: kwargs.update(CONFIG.slack.default_plugin_config) kwargs['SLACK_ROOM'] = '#develop' kwargs['SLACK_MESSAGE'] = 'Pull request <%s|#%s> (%s) was merged.' % ( kwargs.get('url'), kwargs.get('pull_request_id'), kwargs.get('title')) call(**kwargs) return 0 MERGE_PULL_REQUEST = _merge_pull_request_hook @register('CLOSE_PULL_REQUEST') @verify_kwargs( ['_load_extension', 'server_url', 'config', 'scm', 'username', 'ip', 'action', 'repository', 'pull_request_id', 'url', 'title', 'description', 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status', 'mergeable', 'source', 'target', 'author', 'reviewers']) def _close_pull_request_hook(*args, **kwargs): """ """ # extract extra fields and default reviewers from target kwargs['REPOSITORY'] = kwargs['target']['repository'] # fetch extra fields call = load_extension('extra_fields.py') if call: repo_extra_fields = call(**kwargs) # now update if we have extra fields, they have precedence # this way users can store any configuration inside the database per # repo for key, data in repo_extra_fields.items(): kwargs[key] = data['field_value'] # extract below from source repo as commits are there kwargs['REPOSITORY'] = kwargs['source']['repository'] # fetch pushed commits, from commit_ids list call = load_extension('extract_commits.py') extracted_commits = {} if call: extracted_commits = call(**kwargs) # store the commits for the next call chain kwargs['COMMITS'] = extracted_commits # fetch issues from given commits call = load_extension('extract_redmine_issues.py') issues = {} if call: issues = call(**kwargs) # redmine smart pr update call = load_extension('redmine_pr_flow.py') if call: # updates kwargs on the fly configure_redmine_smart_pr(issues=issues, kwargs=kwargs) call(**kwargs) return 0 CLOSE_PULL_REQUEST = _close_pull_request_hook