#!/usr/bin/python # The contents of this file are subject to the Common Public Attribution # License Version 1.0. (the "License"); you may not use this file except in # compliance with the License. You may obtain a copy of the License at # http://code.reddit.com/LICENSE. The License is based on the Mozilla Public # License Version 1.1, but Sections 14 and 15 have been added to cover use of # software over a computer network and provide for limited attribution for the # Original Developer. In addition, Exhibit A has been modified to be consistent # with Exhibit B. # # Software distributed under the License is distributed on an "AS IS" basis, # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is reddit. # # The Original Developer is the Initial Developer. The Initial Developer of # the Original Code is reddit Inc. # # All portions of the code written by reddit are Copyright (c) 2006-2012 reddit # Inc. All Rights Reserved. ############################################################################### """Check for new style guide violations in the current branch. This script is meant to be used in a CI process to ensure that new changes do not violate PEP-8, PEP-257, or any of the validity checks of pyflakes. """ import difflib import os import subprocess import sys DEVNULL = open("/dev/null", "w") TOOLS = [ ["pep8", "--repeat", "-q"], ["pep257"], ["pyflakes"], ] def assert_tools_available(): """Check if the external binaries needed are available or exit.""" for tool in TOOLS: binary = tool[0] try: subprocess.check_call(["which", binary], stdout=DEVNULL) except subprocess.CalledProcessError: print >> sys.stderr, ("command %r not found. " "please install it!" % binary) sys.exit(1) def assert_not_dirty(): """Check if there are uncommitted changes in the repo and exit if so.""" try: subprocess.check_call(["git", "diff", "--no-ext-diff", "--quiet", "--exit-code"]) except subprocess.CalledProcessError: print >> sys.stderr, ("you have uncommitted changes. " "please commit them!") sys.exit(1) def _parse_ref(ref): """Return the result of git rev-parse on the given ref.""" ref = subprocess.check_output(["git", "rev-parse", ref]) return ref.strip() def get_current_ref(): """Return the most descriptive name possible of the current HEAD.""" try: ref = subprocess.check_output(["git", "symbolic-ref", "HEAD"]).strip() return ref[len("refs/heads/"):] except subprocess.CalledProcessError: return _parse_ref("HEAD") def get_upstream_ref(): """Return the ref that this topic branch is based on.""" return _parse_ref("master@{upstream}") def get_root(): """Return the root directory of this git project.""" return os.path.dirname(_parse_ref("--git-dir")) def check_ref_out(ref): """Ask git to check out the specified ref.""" try: subprocess.check_call( ["git", "checkout", ref], stdout=DEVNULL, stderr=DEVNULL, ) except subprocess.CalledProcessError: print >> sys.stderr, "failed to check out %s" % ref sys.exit(1) def generate_report(ref, changed_files): """Run the tools on the specified files and return errors / warnings.""" check_ref_out(ref) report = [] for filepath in changed_files: for tool in TOOLS: command = tool + [filepath] print >> sys.stderr, " ".join(command) process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) lines = process.communicate()[0].splitlines() for line in lines: # if present, get rid of file/line number markings which will # cause false positives. pref, sep, suf = line.partition(" ") line = pref if not sep else suf report.append(line) return report def get_changed_files(old_ref, new_ref): """Return a list of files that have changed from one ref to another.""" root = get_root() changed_files_text = subprocess.check_output(["git", "diff", "--name-only", old_ref, new_ref]) changed_files = changed_files_text.splitlines() return [os.path.join(root, x) for x in changed_files] def main(): assert_tools_available() assert_not_dirty() current_ref = get_current_ref() base_ref = get_upstream_ref() changed_files = get_changed_files(base_ref, current_ref) try: new_report = generate_report(current_ref, changed_files) old_report = generate_report(base_ref, changed_files) finally: check_ref_out(current_ref) added, removed = 0, 0 for line in difflib.unified_diff(old_report, new_report): line = line.strip() if line == "+++" or line == "---": continue if line.startswith("+"): added += 1 elif line.startswith("-"): removed += 1 if added: print "added %d issues" % added if removed: print "removed %d issues!" % removed return 1 if added else 0 if __name__ == "__main__": sys.exit(main())