1# Copyright (C) 2012 by the Massachusetts Institute of Technology. 2# All rights reserved. 3# 4# Export of this software from the United States of America may 5# require a specific license from the United States Government. 6# It is the responsibility of any person or organization contemplating 7# export to obtain such a license before exporting. 8# 9# WITHIN THAT CONSTRAINT, permission to use, copy, modify, and 10# distribute this software and its documentation for any purpose and 11# without fee is hereby granted, provided that the above copyright 12# notice appear in all copies and that both that copyright notice and 13# this permission notice appear in supporting documentation, and that 14# the name of M.I.T. not be used in advertising or publicity pertaining 15# to distribution of the software without specific, written prior 16# permission. Furthermore if you modify this software you must label 17# your software as modified software and not distribute it in such a 18# fashion that it might be confused with the original M.I.T. software. 19# M.I.T. makes no representations about the suitability of 20# this software for any purpose. It is provided "as is" without express 21# or implied warranty. 22 23# This program attempts to detect MIT krb5 coding style violations 24# attributable to the changes a series of git commits. It can be run 25# from anywhere within a git working tree. 26 27import getopt 28import os 29import re 30import sys 31from subprocess import Popen, PIPE, call 32 33def usage(): 34 u = ['Usage: cstyle [-w] [rev|rev1..rev2]', 35 '', 36 'By default, checks working tree against HEAD, or checks changes in', 37 'HEAD if the working tree is clean. With a revision option, checks', 38 'changes in rev or the series rev1..rev2. With the -w option,', 39 'checks working tree against rev (defaults to HEAD).'] 40 sys.stderr.write('\n'.join(u) + '\n') 41 sys.exit(1) 42 43 44# Run a command and return a list of its output lines. 45def run(args): 46 # subprocess.check_output would be ideal here, but requires Python 2.7. 47 p = Popen(args, stdout=PIPE, stderr=PIPE, universal_newlines=True) 48 out, err = p.communicate() 49 if p.returncode != 0: 50 sys.stderr.write('Failed command: ' + ' '.join(args) + '\n') 51 if err != '': 52 sys.stderr.write('stderr:\n' + err) 53 sys.stderr.write('Unexpected command failure, exiting\n') 54 sys.exit(1) 55 return out.splitlines() 56 57 58# Find the top level of the git working tree, or None if we're not in 59# one. 60def find_toplevel(): 61 # git doesn't seem to have a way to do this, so we search by hand. 62 dir = os.getcwd() 63 while True: 64 if os.path.exists(os.path.join(dir, '.git')): 65 break 66 parent = os.path.dirname(dir) 67 if (parent == dir): 68 return None 69 dir = parent 70 return dir 71 72 73# Check for style issues in a file within rev (or within the current 74# checkout if rev is None). Report only problems on line numbers in 75# new_lines. 76line_re = re.compile(r'^\s*(\d+) (.*)$') 77def check_file(filename, rev, new_lines): 78 # Process only C source files under src. 79 root, ext = os.path.splitext(filename) 80 if not filename.startswith('src/') or ext not in ('.c', '.h', '.hin'): 81 return 82 dispname = filename[4:] 83 84 if rev is None: 85 p1 = Popen(['cat', filename], stdout=PIPE) 86 else: 87 p1 = Popen(['git', 'show', rev + ':' + filename], stdout=PIPE) 88 p2 = Popen([sys.executable, 'src/util/cstyle-file.py'], stdin=p1.stdout, 89 stdout=PIPE, universal_newlines=True) 90 p1.stdout.close() 91 out, err = p2.communicate() 92 if p2.returncode != 0: 93 sys.exit(1) 94 95 first = True 96 for line in out.splitlines(): 97 m = line_re.match(line) 98 if int(m.group(1)) in new_lines: 99 if first: 100 print(' ' + dispname + ':') 101 first = False 102 print(' ' + line) 103 104 105# Determine the lines of each file modified by diff (a sequence of 106# strings) and check for style violations in those lines. rev 107# indicates the version in which the new contents of each file can be 108# found, or is None if the current contents are in the working copy. 109chunk_header_re = re.compile(r'^@@ -\d+(,(\d+))? \+(\d+)(,(\d+))? @@') 110def check_diff(diff, rev): 111 old_count, new_count, lineno = 0, 0, 0 112 filename = None 113 for line in diff: 114 if not line or line.startswith('\\ No newline'): 115 continue 116 if old_count > 0 or new_count > 0: 117 # We're in a chunk. 118 if line[0] == '+': 119 new_lines.append(lineno) 120 if line[0] in ('+', ' '): 121 new_count = new_count - 1 122 lineno = lineno + 1 123 if line[0] in ('-', ' '): 124 old_count = old_count - 1 125 elif line.startswith('+++ b/'): 126 # We're starting a new file. Check the last one. 127 if filename: 128 check_file(filename, rev, new_lines) 129 filename = line[6:] 130 new_lines = [] 131 else: 132 m = chunk_header_re.match(line) 133 if m: 134 old_count = int(m.group(2) or '1') 135 lineno = int(m.group(3)) 136 new_count = int(m.group(5) or '1') 137 138 # Check the last file in the diff. 139 if filename: 140 check_file(filename, rev, new_lines) 141 142 143# Check a sequence of revisions for style issues. 144def check_series(revlist): 145 for rev in revlist: 146 sys.stdout.flush() 147 call(['git', 'show', '-s', '--oneline', rev]) 148 diff = run(['git', 'diff-tree', '--no-commit-id', '--root', '-M', 149 '--cc', rev]) 150 check_diff(diff, rev) 151 152 153# Parse arguments. 154try: 155 opts, args = getopt.getopt(sys.argv[1:], 'w') 156except getopt.GetoptError as err: 157 print(str(err)) 158 usage() 159if len(args) > 1: 160 usage() 161 162# Change to the top level of the working tree so we easily run the file 163# checker and refer to working tree files. 164toplevel = find_toplevel() 165if toplevel is None: 166 sys.stderr.write('%s must be run within a git working tree') 167os.chdir(toplevel) 168 169if ('-w', '') in opts: 170 # Check the working tree against a base revision. 171 arg = 'HEAD' 172 if args: 173 arg = args[0] 174 check_diff(run(['git', 'diff', arg]), None) 175elif args: 176 # Check the differences in a rev or a series of revs. 177 if '..' in args[0]: 178 check_series(run(['git', 'rev-list', '--reverse', args[0]])) 179 else: 180 check_series([args[0]]) 181else: 182 # No options or arguments. Check the differences against HEAD, or 183 # the differences in HEAD if the working tree is clean. 184 diff = run(['git', 'diff', 'HEAD']) 185 if diff: 186 check_diff(diff, None) 187 else: 188 check_series(['HEAD']) 189