def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) permit = True # Do not run the hook if the branch is being deleted if new_sha == '0' * 40: logging.debug("Deleting the branch, skip the hook") return True, [] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) messages = [] for commit in log: modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit'], ['.py']) def has_mixed_indent(file_contents): ''' Check if file lines start with tabs and spaces file_contents = open(file).read() ''' has_tab = False has_space = False for line in file_contents.split('\n'): if line.startswith('\t'): has_tab = True elif line.startswith(' '): has_space = True if has_tab and has_space: return True return False # Get the files from the repo and check indentation. for modfile in modfiles: # Skip deleted files if modfile['status'] == 'D': logging.debug("Deleted '%s', skip", modfile['path']) continue cmd = ['git', 'show', modfile['new_blob']] _, file_contents, _ = hookutil.run(cmd, self.repo_dir) permit_file = not has_mixed_indent(file_contents) logging.debug("modfile=%s, permit_file=%s", modfile['path'], permit_file) if not permit_file: messages.append({'at': commit['commit'], 'text': "Error: file '%s' has mixed indentation" % modfile['path']}) permit = permit and permit_file logging.debug("Permit: %s", permit) return permit, messages
def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) if not self.settings: return True permit = True # Do not run the hook if the branch is being deleted if new_sha == '0' * 40: logging.debug("Deleting the branch, skip the hook") return True, [] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) messages = [] for commit in log: modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit']) def has_good_copyright(file_contents, copyrights): ''' Check if file contains good copyright string ''' for (start, full) in copyrights: if re.search(start, file_contents): if not re.search(full, file_contents): return False return True for modfile in modfiles: # Skip deleted files if modfile['status'] == 'D': logging.debug("Deleted %s, skip", modfile['path']) continue cmd = ['git', 'show', modfile['new_blob']] _, file_contents, _ = hookutil.run(cmd, self.repo_dir) permit_file = has_good_copyright(file_contents, self.settings) logging.debug("modfile='%s', permit_file='%s'", modfile['path'], permit_file) if not permit_file: messages.append({'at': commit['commit'], 'text': "Error: Bad copyright in file '%s'!" % modfile['path']}) permit = permit and permit_file if not permit: text = 'Please update the copyright strings to match one of the following:\n\n\t- ' + '\n\t- '.join([full for (start, full) in self.settings]) messages.append({'at': new_sha, 'text': text + '\n'}) logging.debug("Permit: %s", permit) return permit, messages
def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) permit = True # Do not run the hook if the branch is being deleted if new_sha == '0' * 40: logging.debug("Deleting the branch, skip the hook") return True, [] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) messages = [] for commit in log: modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit']) def has_mixed_le(file_contents): ''' Check if file contains both lf and crlf file_contents = open(file).read() ''' if ('\r\n' in file_contents and '\n' in file_contents.replace('\r\n', '')): return True return False for modfile in modfiles: # Skip deleted files if modfile['status'] == 'D': logging.debug("Deleted %s, skip", modfile['path']) continue binary_attr = hookutil.get_attr( self.repo_dir, new_sha, modfile['path'], 'binary') if binary_attr != 'set': cmd = ['git', 'show', modfile['new_blob']] _, file_contents, _ = hookutil.run(cmd, self.repo_dir) permit_file = not has_mixed_le(file_contents) logging.debug("modfile='%s', permit_file='%s'", modfile['path'], permit_file) if not permit_file: messages.append({'at': commit['commit'], 'text': "Error: file '%s' has mixed line endings (CRLF/LF)" % modfile['path']}) permit = permit and permit_file logging.debug("Permit: %s", permit) return permit, messages
def compose_mail(self, branch, old_sha, new_sha): pusher = self.params['user_name'] base_url = self.params['base_url'] proj_key = self.params['proj_key'] repo_name = self.params['repo_name'] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha) files = [] for commit in log: show = hookutil.parse_git_show(self.repo_dir, commit['commit']) for modfile in show: owners_attr = hookutil.get_attr(self.repo_dir, new_sha, modfile['path'], 'owners') if owners_attr == 'unspecified' or owners_attr == 'unset': continue for owner in set(owners_attr.split(',')): files.append({'owner':owner, 'commit':commit, 'path':modfile}) files = sorted(files, key=lambda ko: ko['owner']) mails = {} for owner, commits in itertools.groupby(files, key=lambda ko: ko['owner']): text = '<b>Branch:</b> %s\n' % branch.replace('refs/heads/', '') text += '<b>By user:</b> %s\n' % pusher text += '\n' # No need to sort by commit hash because it is in order for commit, paths in itertools.groupby(commits, key=lambda kc: kc['commit']): link = base_url + \ "/projects/%s/repos/%s/commits/%s\n" % (proj_key, repo_name, commit['commit']) text += 'Commit: %s (%s)\n' % (commit['commit'], "<a href=%s>View in Stash</a>" % link) text += 'Author: %s %s\n' % (commit['author_name'], commit['author_email']) text += 'Date: %s\n' % commit['date'] text += '\n' text += '\t%s' % '\n\t'.join(wrap(commit['message'][:100], width=70)) if len(commit['message']) > 100: text += '...' text += '\n\n' for path in paths: text += '\t%s %s\n' % (path['path']['status'], path['path']['path']) text += '\n\n' mails[owner] = text return mails
def compose_mail(self, branch, old_sha, new_sha): pusher = self.params['user_name'] base_url = self.params['base_url'] proj_key = self.params['proj_key'] repo_name = self.params['repo_name'] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha) files = [] for commit in log: show = hookutil.parse_git_show(self.repo_dir, commit['commit']) for modfile in show: owners_attr = hookutil.get_attr(self.repo_dir, new_sha, modfile['path'], 'owners') if owners_attr == 'unspecified' or owners_attr == 'unset': continue for owner in set(owners_attr.split(',')): files.append({ 'owner': owner, 'commit': commit, 'path': modfile }) files = sorted(files, key=lambda ko: ko['owner']) mails = {} for owner, commits in itertools.groupby(files, key=lambda ko: ko['owner']): text = '<b>Branch:</b> %s\n' % branch.replace('refs/heads/', '') text += '<b>By user:</b> %s\n' % pusher text += '\n' # No need to sort by commit hash because it is in order for commit, paths in itertools.groupby( commits, key=lambda kc: kc['commit']): link = base_url + \ "/projects/%s/repos/%s/commits/%s\n" % (proj_key, repo_name, commit['commit']) text += 'Commit: %s (%s)\n' % ( commit['commit'], "<a href=%s>View in Stash</a>" % link) text += 'Author: %s %s\n' % (commit['author_name'], commit['author_email']) text += 'Date: %s\n' % commit['date'] text += '\n' text += '\t%s' % '\n\t'.join( wrap(commit['message'][:100], width=70)) if len(commit['message']) > 100: text += '...' text += '\n\n' for path in paths: text += '\t%s %s\n' % (path['path']['status'], path['path']['path']) text += '\n\n' mails[owner] = text return mails
def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) permit = True log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) for commit in log: print "Checking commit %s ..." % commit['commit'] # Filter python scripts from the files modified in new_sha modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit'], ['.py']) # Exit early if there are no modified python scripts in the changeset if not modfiles: return permit # Set up a working directory for pycodestyle and fill it with the blobs to be checked pycheck_workdir = tempfile.mkdtemp(suffix='pycheck') for modfile in modfiles: # Skip deleted files if modfile['status'] == 'D': logging.debug("Deleted '%s', skip", modfile['path']) continue cmd = ['git', 'show', modfile['new_blob']] _, file_contents, _ = hookutil.run(cmd, self.repo_dir) file_path = os.path.join(pycheck_workdir, modfile['path']) assert(not os.path.exists(file_path)) file_dir = os.path.join(pycheck_workdir, os.path.dirname(modfile['path'])) if not os.path.exists(file_dir): os.makedirs(os.path.join(pycheck_workdir, os.path.dirname(modfile['path']))) with open(file_path, 'w') as fd: fd.write(file_contents) # Copy setup.cfg to the working directory shutil.copy(os.path.join(os.path.dirname(__file__), 'setup.cfg'), pycheck_workdir) # Get the commit's diff; pycodestyle needs it to report only against modified lines cmd = ['git', 'show', commit['commit']] _, diff, _ = hookutil.run(cmd, self.repo_dir) local_dir = os.curdir os.chdir(pycheck_workdir) # Run pycodestyle in the working directory we have just prepared. selected_lines = pycodestyle.parse_udiff(diff, patterns=['*.py'], parent='') pep8style = pycodestyle.StyleGuide( { 'diff' : True, 'paths' : sorted(selected_lines), 'selected_lines': selected_lines, 'reporter' : pycodestyle.DiffReport } ) report = pep8style.check_files() os.chdir(local_dir) if report.total_errors: permit = False # Clean up shutil.rmtree(pycheck_workdir) logging.debug("Permit: %s" % permit) return permit, []
def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) permit = True # Do not run the hook if the branch is being deleted if new_sha == '0' * 40: logging.debug("Deleting the branch, skip the hook") return True, [] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) messages = [] for commit in log: modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit']) def has_mixed_le(file_contents): ''' Check if file contains both lf and crlf file_contents = open(file).read() ''' if ('\r\n' in file_contents and '\n' in file_contents.replace('\r\n', '')): return True return False for modfile in modfiles: # Skip deleted files if modfile['status'] == 'D': logging.debug("Deleted %s, skip", modfile['path']) continue binary_attr = hookutil.get_attr(self.repo_dir, new_sha, modfile['path'], 'binary') if binary_attr != 'set': cmd = ['git', 'show', modfile['new_blob']] _, file_contents, _ = hookutil.run(cmd, self.repo_dir) permit_file = not has_mixed_le(file_contents) logging.debug("modfile='%s', permit_file='%s'", modfile['path'], permit_file) if not permit_file: messages.append({ 'at': commit['commit'], 'text': "Error: file '%s' has mixed line endings (CRLF/LF)" % modfile['path'] }) permit = permit and permit_file logging.debug("Permit: %s", permit) return permit, messages
def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) try: base_url = self.params['base_url'] proj_key = self.params['proj_key'] repo_name = self.params['repo_name'] pull_id = self.params['pull_id'] pull_request_author_email = self.params['pull_request_author_email'] except KeyError as err: logging.error("%s not in hook settings", err) raise RuntimeError("%s not in hook settings, check githooks configuration" % err) auth = None try: auth_user = self.params['auth_user'] try: auth = TokenAuth(auth_user, self.params['auth_token']) except Exception: logging.warning("Could not read auth token, trying basic auth") try: auth = requests.auth.HTTPBasicAuth(auth_user, self.params['auth_password']) except Exception: logging.error("Could not read token and password for user %s", auth_user) logging.warning("Continue with no auth") except Exception: logging.error("Could not read auth_user") logging.warning("Continue with no auth") permit = False # Fetch pull request reviewers try: pr = requests.get("%s/rest/api/1.0/projects/%s/repos/%s/pull-requests/%s" % (base_url, proj_key, repo_name, pull_id), auth=auth) pr.raise_for_status() except Exception as err: err_msg = "Failed to fetch pull request data (%s)" % str(err) logging.error(err_msg) raise RuntimeError(err_msg) try: pr_reviewers = pr.json()['reviewers'] reviewers = [] for reviewer in pr_reviewers: if reviewer['role'] != 'REVIEWER': continue email = reviewer['user']['emailAddress'] if reviewer['approved']: reviewers.append(email) except KeyError as key: logging.error("Failed to parse %s from pull request # %s data" % (pull_id, str(key))) raise RuntimeError("Failed to parse pull request # %s data (%s: no such key)" % (pull_id, str(key))) logging.debug("Pull request reviewers who approved: %s", reviewers) # Parse modified files per commit log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha) files = [] for commit in log: modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit']) for modfile in modfiles: owners_attr = hookutil.get_attr(self.repo_dir, new_sha, modfile['path'], 'owners') if owners_attr == 'unspecified' or owners_attr == 'unset': continue for owner in owners_attr.split(','): # Skip this path as it is owned by the guy who merges the pull request # Go to next modfile processing if pull_request_author_email == owner: break # Avoid mail groups here -- check if Bitbucket user exists # # Do not fail if a mail group found in the owners list; # Those mail groups are valid for the change notification hook try: ru = requests.get("%s/rest/api/1.0/users/%s" % (base_url, owner.split('@')[0]), auth=auth) ru.raise_for_status() except Exception as err: logging.error("Failed to fetch user %s data (%s)" % (owner, str(err))) continue if owner not in reviewers: files.append({'commit':commit['commit'], 'owner':owner, 'path':modfile['path']}) if not files: permit = True logging.debug("files = []; either all approved or no approve required, permit = %s", permit) return permit, [] logging.debug("Unapproved files: %s", files) all_owners = list(set([f['owner'] for f in files])) if len(all_owners) > 1: all_owners_str = ', '.join(all_owners[:-1] + ' and ', all_owners[-1]) else: all_owners_str = ''.join(all_owners) print '\n'.join(wrap("<h4>This pull request must be approved by %s!</h4>" % all_owners_str, width=80)) print '\n'.join(wrap('Changes to the following files must be approved ' \ 'by their owners as specified in .gitattributes. ' \ 'Please add those people to the pull request reviewers ' 'if you haven\'t done so and wait for them to approve.', width=80)) print "<p>" print '\n'.join(wrap("List of files that require approval:", width=80)) for path, files_by_path in itertools.groupby(sorted(files, key=lambda k: k['path']), key=lambda ko: ko['path']): path_owners = list(set([f['owner'] for f in files_by_path])) text = "<i>%s</i> by " % path if len(path_owners) > 1: text += ', '.join(path_owners[:-1] + ' or ', path_owners[-1]) else: text += ''.join(path_owners) print '\n'.join(wrap(text, width=80)) print "</p>" logging.debug("Permit: %s", permit) return permit, []
def check(self, branch, old_sha, new_sha): logging.debug("Run: branch=%s, old_sha=%s, new_sha=%s", branch, old_sha, new_sha) logging.debug("params=%s", self.params) if not self.settings: return True, [] permit = True # Do not run the hook if the branch is being deleted if new_sha == '0' * 40: logging.debug("Deleting the branch, skip the hook") return True, [] # Before the hook is run git has already created # a new_sha commit object log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) messages = [] for commit in log: modfiles = hookutil.parse_git_show(self.repo_dir, commit['commit']) def has_good_copyright(file_contents, copyrights): ''' Check if file contains good copyright string ''' for (start, full) in copyrights: if re.search(start, file_contents): if not re.search(full, file_contents): return False return True for modfile in modfiles: # Skip deleted files if modfile['status'] == 'D': logging.debug("Deleted %s, skip", modfile['path']) continue cmd = ['git', 'show', modfile['new_blob']] _, file_contents, _ = hookutil.run(cmd, self.repo_dir) permit_file = has_good_copyright(file_contents, self.settings) logging.debug("modfile='%s', permit_file='%s'", modfile['path'], permit_file) if not permit_file: messages.append({ 'at': commit['commit'], 'text': "Error: Bad copyright in file '%s'!" % modfile['path'] }) permit = permit and permit_file if not permit: text = 'Please update the copyright strings to match one of the following:\n\n\t- ' + '\n\t- '.join( [full for (start, full) in self.settings]) messages.append({'at': new_sha, 'text': text + '\n'}) logging.debug("Permit: %s", permit) return permit, messages