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 test_get_attr(self): write_string('a.txt', 'data') write_string('b.txt', 'data') write_string('c.txt', 'data') write_string('.gitattributes', 'a.txt binary\nb.txt text') git(['add', 'a.txt', 'b.txt', 'c.txt', '.gitattributes']) git(['commit', '-m', 'initial commit']) git_call = git_async(['push', '-u', 'origin', 'master'], self.repo) request = self.get_request() import hookutil self.assertEquals( hookutil.get_attr(self.repo, request[2], 'a.txt', 'binary'), 'set') self.assertEquals( hookutil.get_attr(self.repo, request[2], 'a.txt', 'text'), 'unset') self.assertEquals( hookutil.get_attr(self.repo, request[2], 'b.txt', 'binary'), 'unspecified') self.assertEquals( hookutil.get_attr(self.repo, request[2], 'b.txt', 'text'), 'set') self.assertEquals( hookutil.get_attr(self.repo, request[2], 'c.txt', 'binary'), 'unspecified') self.assertEquals( hookutil.get_attr(self.repo, request[2], 'c.txt', 'text'), 'unspecified') self.write_response(0, 'success') git_async_result(git_call)
def test_get_attr(self): write_string('a.txt', 'data') write_string('b.txt', 'data') write_string('c.txt', 'data') write_string('.gitattributes', 'a.txt binary\nb.txt text') git(['add', 'a.txt', 'b.txt', 'c.txt', '.gitattributes']) git(['commit', '-m', 'initial commit']) git_call = git_async(['push', '-u', 'origin', 'master'], self.repo) request = self.get_request() import hookutil self.assertEquals(hookutil.get_attr(self.repo, request[2], 'a.txt', 'binary'), 'set') self.assertEquals(hookutil.get_attr(self.repo, request[2], 'a.txt', 'text'), 'unset') self.assertEquals(hookutil.get_attr(self.repo, request[2], 'b.txt', 'binary'), 'unspecified') self.assertEquals(hookutil.get_attr(self.repo, request[2], 'b.txt', 'text'), 'set') self.assertEquals(hookutil.get_attr(self.repo, request[2], 'c.txt', 'binary'), 'unspecified') self.assertEquals(hookutil.get_attr(self.repo, request[2], 'c.txt', 'text'), 'unspecified') self.write_response(0, 'success') git_async_result(git_call)
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 # 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, []