# HG changeset patch # User Marcin Kuzminski # Date 2018-11-07 17:02:25 # Node ID ab158566e17dd3173729fb60dcf99d113844e3ce # Parent 6262b28ecccba5dee212c1bfe5f8473ca58cfbae rcextensions: added example for validation of commit messages for git. diff --git a/rhodecode/config/rcextensions/examples/validate_author.py b/rhodecode/config/rcextensions/examples/validate_author.py deleted file mode 100644 diff --git a/rhodecode/config/rcextensions/examples/validate_commit_message.py b/rhodecode/config/rcextensions/examples/validate_commit_message.py deleted file mode 100644 diff --git a/rhodecode/config/rcextensions/examples/validate_commit_message_author.py b/rhodecode/config/rcextensions/examples/validate_commit_message_author.py new file mode 100644 --- /dev/null +++ b/rhodecode/config/rcextensions/examples/validate_commit_message_author.py @@ -0,0 +1,91 @@ +# Example to validate commit message or author using some sort of rules + + +@has_kwargs({ + 'server_url': 'url of instance that triggered this hook', + 'config': 'path to .ini config used', + 'scm': 'type of version control "git", "hg", "svn"', + 'username': 'username of actor who triggered this event', + 'ip': 'ip address of actor who triggered this hook', + 'action': '', + 'repository': 'repository name', + 'repo_store_path': 'full path to where repositories are stored', + 'commit_ids': 'pre transaction metadata for commit ids', + 'hook_type': '', + 'user_agent': 'Client user agent, e.g git or mercurial CLI version', +}) +@has_kwargs({ + 'server_url': 'url of instance that triggered this hook', + 'config': 'path to .ini config used', + 'scm': 'type of version control "git", "hg", "svn"', + 'username': 'username of actor who triggered this event', + 'ip': 'ip address of actor who triggered this hook', + 'action': '', + 'repository': 'repository name', + 'repo_store_path': 'full path to where repositories are stored', + 'commit_ids': 'pre transaction metadata for commit ids', + 'hook_type': '', + 'user_agent': 'Client user agent, e.g git or mercurial CLI version', +}) +def _pre_push_hook(*args, **kwargs): + """ + Post push hook + To stop version control from storing the transaction and send a message to user + use non-zero HookResponse with a message, e.g return HookResponse(1, 'Not allowed') + + This message will be shown back to client during PUSH operation + + Commit ids might look like that:: + + [{u'hg_env|git_env': ..., + u'multiple_heads': [], + u'name': u'default', + u'new_rev': u'd0befe0692e722e01d5677f27a104631cf798b69', + u'old_rev': u'd0befe0692e722e01d5677f27a104631cf798b69', + u'ref': u'', + u'total_commits': 2, + u'type': u'branch'}] + """ + import re + from .helpers import extra_fields, extract_pre_commits + from .utils import str2bool + + # returns list of dicts with key-val fetched from extra fields + repo_extra_fields = extra_fields.run(**kwargs) + + # optionally use 'extra fields' to control the logic per repo + should_validate = str2bool(repo_extra_fields.get('validate_author', True)) + + # optionally store validation regex into extra fields + validation_regex = repo_extra_fields.get('validation_regex', '') + + def validate_commit_message(commit_message, message_regex=None): + """ + This function validates commit_message against some sort of rules. + It should return a valid boolean, and a reason for failure + """ + + if "secret_string" in commit_message: + msg = "!!Push forbidden: secret string found in commit messages" + return False, msg + + if validation_regex: + regexp = re.compile(validation_regex) + if not regexp.match(message): + msg = "!!Push forbidden: commit message does not match regexp" + return False, msg + + return True, '' + + if should_validate: + # returns list of dicts with key-val fetched from extra fields + commit_list = extract_pre_commits.run(**kwargs) + + for commit_data in commit_list: + message = commit_data['message'] + + message_valid, reason = validate_commit_message(message, validation_regex) + if not message_valid: + return HookResponse(1, reason) + + return HookResponse(0, '') diff --git a/rhodecode/config/rcextensions/helpers/extract_pre_commits.py b/rhodecode/config/rcextensions/helpers/extract_pre_commits.py --- a/rhodecode/config/rcextensions/helpers/extract_pre_commits.py +++ b/rhodecode/config/rcextensions/helpers/extract_pre_commits.py @@ -27,6 +27,7 @@ us in hooks:: """ import re import collections +import json def get_hg_commits(repo, refs): @@ -36,6 +37,31 @@ def get_hg_commits(repo, refs): def get_git_commits(repo, refs): commits = [] + + for data in refs: + # we should now extract commit data + old_rev = data['old_rev'] + new_rev = data['new_rev'] + + if '00000000' in old_rev: + # new branch, we don't need to extract nothing + return commits + + git_env = dict(data['git_env']) + cmd = [ + 'log', + '--pretty=format:{"commit_id": "%H", "author": "%aN <%aE>", "date": "%ad", "message": "%f"}', + '{}...{}'.format(old_rev, new_rev) + ] + + stdout, stderr = repo.run_git_command(cmd, env=git_env) + for line in stdout.splitlines(): + try: + data = json.loads(line) + commits.append(data) + except Exception: + print('Failed to load data from GIT line') + return commits @@ -51,13 +77,14 @@ def run(*args, **kwargs): commits = [] - for rev_data in kwargs['commit_ids']: - new_environ = dict((k, v) for k, v in rev_data['hg_env']) - if vcs_type == 'git': + for rev_data in kwargs['commit_ids']: + new_environ = dict((k, v) for k, v in rev_data['git_env']) commits = get_git_commits(vcs_repo, kwargs['commit_ids']) if vcs_type == 'hg': + for rev_data in kwargs['commit_ids']: + new_environ = dict((k, v) for k, v in rev_data['hg_env']) commits = get_hg_commits(vcs_repo, kwargs['commit_ids']) return commits diff --git a/rhodecode/config/rcextensions/utils.py b/rhodecode/config/rcextensions/utils.py --- a/rhodecode/config/rcextensions/utils.py +++ b/rhodecode/config/rcextensions/utils.py @@ -145,3 +145,41 @@ def maybe_log_call(name, args, kwargs): if hasattr(rcextensions, 'calls'): calls = rcextensions.calls calls[name].append((args, kwargs)) + + +def str2bool(_str): + """ + returns True/False value from given string, it tries to translate the + string into boolean + + :param _str: string value to translate into boolean + :rtype: boolean + :returns: boolean from given string + """ + if _str is None: + return False + if _str in (True, False): + return _str + _str = str(_str).strip().lower() + return _str in ('t', 'true', 'y', 'yes', 'on', '1') + + +def aslist(obj, sep=None, strip=True): + """ + Returns given string separated by sep as list + + :param obj: + :param sep: + :param strip: + """ + if isinstance(obj, (basestring,)): + lst = obj.split(sep) + if strip: + lst = [v.strip() for v in lst] + return lst + elif isinstance(obj, (list, tuple)): + return obj + elif obj is None: + return [] + else: + return [obj] \ No newline at end of file