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'] email_domain = self.params['email_domain'] # 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) users = [] for commit in log: for username in set( re.findall('(?:\W+|^)@(\w[\w\.]*\w|\w)', commit['message'])): ci = commit.copy() ci.update({'user': username}) users.append(ci) users = sorted(users, key=lambda ko: ko['user']) mails = {} for user, commits in itertools.groupby(users, key=lambda ko: ko['user']): text = '<b>Branch:</b> %s\n' % branch.replace('refs/heads/', '') text += '<b>By user:</b> %s\n' % pusher text += '\n' for commit in commits: 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'], width=70)) text += '\n\n' mails[user + '@' + email_domain] = 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'] email_domain = self.params['email_domain'] # 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) users = [] for commit in log: for username in set(re.findall('(?:\W+|^)@(\w[\w\.]*\w|\w)', commit['message'])): ci = commit.copy() ci.update({'user': username}) users.append(ci) users = sorted(users, key=lambda ko: ko['user']) mails = {} for user, commits in itertools.groupby(users, key=lambda ko: ko['user']): text = '<b>Branch:</b> %s\n' % branch.replace('refs/heads/', '') text += '<b>By user:</b> %s\n' % pusher text += '\n' for commit in commits: 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'], width=70)) text += '\n\n' mails[user + '@' + email_domain] = 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
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) # 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, [] def print_commit(commit, formatter='\t%s'): ''' Print a commit using a formatter for each new line. The default formatter is a signle tabulation. ''' return '\n'.join([ formatter % i for i in [ "commit %s" % commit['commit'], "Merge: %s %s" % (parentCommits[0][:7], parentCommits[1][:7]), "Author: %s <%s>" % (commit['author_name'], commit['author_email']), "Date: %s" % commit['date'] ] + [""] + # Add a newline wrap(commit['message'], width=120) + [""] ]) permit = True log = hookutil.parse_git_log(self.repo_dir, branch, old_sha, new_sha, this_branch_only=False) messages = [] for commit in log: # Parse commit parents cmd = ['git', 'rev-list', '--parents', '-n', '1', commit['commit']] _, out, _ = hookutil.run(cmd, self.repo_dir) parentCommits = out.strip().split(' ')[1:] # Skip commit if it is not a merge commit if len(parentCommits) < 2: continue logging.debug("Found merge %s, parents: %s %s", commit['commit'][:7], parentCommits[0][:7], parentCommits[1][:7]) # Find branches that contain parent commits parentBranches = [] for parentCommit in parentCommits: cmd = ['git', 'branch', '--contains', parentCommit] ret, out, err = hookutil.run(cmd, self.repo_dir) # FIXME Skip if parent commit was not found on any branch if not out and not err and not ret: # These are stdout, stderr and return code that Popen.wait produces for 'git branch --continue' # with shell=False. When a commit does not exist in the remote, the command should return 129 # and an error meesage in stderr. With shell=True, it does not work as extected either (returns 1 # and git usage in stdout, similar to running just 'git' instead of 'git branch --continue ...'). continue if not out: parentBranches.append(branch.replace('refs/heads/', '')) else: parentBranches += [ br.replace("* ", "") for br in out.strip().split('\n') ] if len(set(parentBranches)) != 1: continue mergedBranch = parentBranches[0] logging.debug("All parents are on branch '%s'", mergedBranch) # First parent must be on the destination branch firstParent = parentCommits[0] cmd = ['git', 'branch', '--contains', firstParent] _, out, _ = hookutil.run(cmd, self.repo_dir) if not out.startswith('* '): permit = False text = '\n'.join([ "Merging a remote branch onto a local branch is prohibited when updating the remote with that local branch.", "", print_commit(commit) ] + wrap( "You must remove this merge by updating your local branch properly. Please rebase on top of the remote branch:", width=120 ) + ["", "\tgit pull --rebase origin %s" % mergedBranch, ""]) messages += [{'at': commit['commit'], 'text': text}] logging.info("%s is same-branch merge, permit = %s", commit['commit'][:7], permit) logging.debug("Permit: %s", permit) return permit, messages