# Copyright (C) 2012 by the Massachusetts Institute of Technology.1# All rights reserved.2#3# Export of this software from the United States of America may4# require a specific license from the United States Government.5# It is the responsibility of any person or organization contemplating6# export to obtain such a license before exporting.7#8# WITHIN THAT CONSTRAINT, permission to use, copy, modify, and9# distribute this software and its documentation for any purpose and10# without fee is hereby granted, provided that the above copyright11# notice appear in all copies and that both that copyright notice and12# this permission notice appear in supporting documentation, and that13# the name of M.I.T. not be used in advertising or publicity pertaining14# to distribution of the software without specific, written prior15# permission. Furthermore if you modify this software you must label16# your software as modified software and not distribute it in such a17# fashion that it might be confused with the original M.I.T. software.18# M.I.T. makes no representations about the suitability of19# this software for any purpose. It is provided "as is" without express20# or implied warranty.2122# This program attempts to detect MIT krb5 coding style violations23# attributable to the changes a series of git commits. It can be run24# from anywhere within a git working tree.2526import getopt27import os28import re29import sys30from subprocess import Popen, PIPE, call3132def usage():33u = ['Usage: cstyle [-w] [rev|rev1..rev2]',34'',35'By default, checks working tree against HEAD, or checks changes in',36'HEAD if the working tree is clean. With a revision option, checks',37'changes in rev or the series rev1..rev2. With the -w option,',38'checks working tree against rev (defaults to HEAD).']39sys.stderr.write('\n'.join(u) + '\n')40sys.exit(1)414243# Run a command and return a list of its output lines.44def run(args):45# subprocess.check_output would be ideal here, but requires Python 2.7.46p = Popen(args, stdout=PIPE, stderr=PIPE, universal_newlines=True)47out, err = p.communicate()48if p.returncode != 0:49sys.stderr.write('Failed command: ' + ' '.join(args) + '\n')50if err != '':51sys.stderr.write('stderr:\n' + err)52sys.stderr.write('Unexpected command failure, exiting\n')53sys.exit(1)54return out.splitlines()555657# Find the top level of the git working tree, or None if we're not in58# one.59def find_toplevel():60# git doesn't seem to have a way to do this, so we search by hand.61dir = os.getcwd()62while True:63if os.path.exists(os.path.join(dir, '.git')):64break65parent = os.path.dirname(dir)66if (parent == dir):67return None68dir = parent69return dir707172# Check for style issues in a file within rev (or within the current73# checkout if rev is None). Report only problems on line numbers in74# new_lines.75line_re = re.compile(r'^\s*(\d+) (.*)$')76def check_file(filename, rev, new_lines):77# Process only C source files under src.78root, ext = os.path.splitext(filename)79if not filename.startswith('src/') or ext not in ('.c', '.h', '.hin'):80return81dispname = filename[4:]8283if rev is None:84p1 = Popen(['cat', filename], stdout=PIPE)85else:86p1 = Popen(['git', 'show', rev + ':' + filename], stdout=PIPE)87p2 = Popen([sys.executable, 'src/util/cstyle-file.py'], stdin=p1.stdout,88stdout=PIPE, universal_newlines=True)89p1.stdout.close()90out, err = p2.communicate()91if p2.returncode != 0:92sys.exit(1)9394first = True95for line in out.splitlines():96m = line_re.match(line)97if int(m.group(1)) in new_lines:98if first:99print(' ' + dispname + ':')100first = False101print(' ' + line)102103104# Determine the lines of each file modified by diff (a sequence of105# strings) and check for style violations in those lines. rev106# indicates the version in which the new contents of each file can be107# found, or is None if the current contents are in the working copy.108chunk_header_re = re.compile(r'^@@ -\d+(,(\d+))? \+(\d+)(,(\d+))? @@')109def check_diff(diff, rev):110old_count, new_count, lineno = 0, 0, 0111filename = None112for line in diff:113if not line or line.startswith('\\ No newline'):114continue115if old_count > 0 or new_count > 0:116# We're in a chunk.117if line[0] == '+':118new_lines.append(lineno)119if line[0] in ('+', ' '):120new_count = new_count - 1121lineno = lineno + 1122if line[0] in ('-', ' '):123old_count = old_count - 1124elif line.startswith('+++ b/'):125# We're starting a new file. Check the last one.126if filename:127check_file(filename, rev, new_lines)128filename = line[6:]129new_lines = []130else:131m = chunk_header_re.match(line)132if m:133old_count = int(m.group(2) or '1')134lineno = int(m.group(3))135new_count = int(m.group(5) or '1')136137# Check the last file in the diff.138if filename:139check_file(filename, rev, new_lines)140141142# Check a sequence of revisions for style issues.143def check_series(revlist):144for rev in revlist:145sys.stdout.flush()146call(['git', 'show', '-s', '--oneline', rev])147diff = run(['git', 'diff-tree', '--no-commit-id', '--root', '-M',148'--cc', rev])149check_diff(diff, rev)150151152# Parse arguments.153try:154opts, args = getopt.getopt(sys.argv[1:], 'w')155except getopt.GetoptError as err:156print(str(err))157usage()158if len(args) > 1:159usage()160161# Change to the top level of the working tree so we easily run the file162# checker and refer to working tree files.163toplevel = find_toplevel()164if toplevel is None:165sys.stderr.write('%s must be run within a git working tree')166os.chdir(toplevel)167168if ('-w', '') in opts:169# Check the working tree against a base revision.170arg = 'HEAD'171if args:172arg = args[0]173check_diff(run(['git', 'diff', arg]), None)174elif args:175# Check the differences in a rev or a series of revs.176if '..' in args[0]:177check_series(run(['git', 'rev-list', '--reverse', args[0]]))178else:179check_series([args[0]])180else:181# No options or arguments. Check the differences against HEAD, or182# the differences in HEAD if the working tree is clean.183diff = run(['git', 'diff', 'HEAD'])184if diff:185check_diff(diff, None)186else:187check_series(['HEAD'])188189190