#!/usr/bin/env python import os import subprocess import tempfile import sys import filecmp import optparse import shutil import difflib import re def hg_modified_files(): files = os.popen ('hg st -nma') return [filename.strip() for filename in files] def copy_file(filename): [tmp,pathname] = tempfile.mkstemp() src = open(filename, 'r') dst = open(pathname, 'w') for line in src: dst.write(line) dst.close() src.close() return pathname # generate a temporary configuration file def uncrustify_config_file(level): level2 = """ nl_collapse_empty_body=False nl_if_brace=Add nl_brace_else=Add nl_elseif_brace=Add nl_else_brace=Add nl_while_brace=Add nl_do_brace=Add nl_for_brace=Add nl_brace_while=Add nl_switch_brace=Add nl_after_case=True nl_namespace_brace=Remove nl_after_brace_open=True nl_class_leave_one_liners=False nl_enum_leave_one_liners=False nl_func_leave_one_liners=False nl_if_leave_one_liners=False nl_class_colon=Ignore nl_after_access_spec=1 nl_after_semicolon=True pos_class_colon=Lead pos_class_comma=Trail pos_bool=Lead nl_class_init_args=Add nl_template_class=Add nl_class_brace=Add # does not work very well nl_func_type_name=Ignore nl_func_scope_name=Ignore nl_func_type_name_class=Ignore nl_func_proto_type_name=Ignore # function\\n( nl_func_paren=Remove nl_fdef_brace=Add nl_struct_brace=Add nl_enum_brace=Add nl_union_brace=Add mod_full_brace_do=Add mod_full_brace_for=Add mod_full_brace_if=Add mod_full_brace_while=Add mod_full_brace_for=Add mod_remove_extra_semicolon=True # max code width #code_width=128 #ls_for_split_full=True #ls_func_split_full=True """ level1 = """ # extra spaces here and there sp_brace_typedef=Add sp_enum_assign=Add sp_before_sparen=Add sp_after_semi_for=Add sp_arith=Add sp_assign=Add sp_compare=Add sp_func_class_paren=Add sp_after_type=Add sp_type_func=Add sp_angle_paren=Add """ level0 = """ sp_func_proto_paren=Add sp_func_def_paren=Add sp_func_call_paren=Add sp_after_semi_for=Ignore sp_before_sparen=Ignore sp_type_func=Ignore sp_after_type=Ignore nl_class_leave_one_liners=True nl_enum_leave_one_liners=True nl_func_leave_one_liners=True nl_assign_leave_one_liners=True #nl_collapse_empty_body=False nl_getset_leave_one_liners=True nl_if_leave_one_liners=True nl_fdef_brace=Ignore # finally, indentation configuration indent_with_tabs=0 indent_namespace=false indent_columns=2 indent_brace=2 indent_case_brace=2 indent_class=true indent_class_colon=True # alignment indent_align_assign=False align_left_shift=True # comment reformating disabled cmt_reflow_mode=1 # do not touch comments at all cmt_indent_multi=False # really, do not touch them """ [tmp,pathname] = tempfile.mkstemp() dst = open(pathname, 'w') dst.write(level0) if level >= 1: dst.write(level1) if level >= 2: dst.write(level2) dst.close() return pathname ## PatchChunkLine class class PatchChunkLine: ## @var __type # type ## @var __line # line ## @var SRC # Source SRC = 1 ## @var DST # Destination DST = 2 ## @var BOTH # Both BOTH = 3 def __init__(self): """! Initializer @param self The current class @return none """ self.__type = 0 self.__line = '' def set_src(self,line): """! Set source @param self The current class @param line source line @return none """ self.__type = self.SRC self.__line = line def set_dst(self,line): """! Set destination @param self The current class @param line destination line @return none """ self.__type = self.DST self.__line = line def set_both(self,line): """! Set both @param self The current class @param line @return none """ self.__type = self.BOTH self.__line = line def append_to_line(self, s): """! Append to line @param self The current class @param s line to append @return none """ self.__line = self.__line + s def line(self): """! Get line @param self The current class @return line """ return self.__line def is_src(self): """! Is source @param self The current class @return true if type is source """ return self.__type == self.SRC or self.__type == self.BOTH def is_dst(self): """! Is destination @param self The current class @return true if type is destination """ return self.__type == self.DST or self.__type == self.BOTH def write(self, f): """! Write to file @param self The current class @param f file @return exception if invalid type """ if self.__type == self.SRC: f.write('-%s\n' % self.__line) elif self.__type == self.DST: f.write('+%s\n' % self.__line) elif self.__type == self.BOTH: f.write(' %s\n' % self.__line) else: raise Exception('invalid patch') ## PatchChunk class class PatchChunk: ## @var __lines # list of lines ## @var __src_pos # source position ## @var __dst_pos # destination position ## @var src # source ## @var dst # destination def __init__(self, src_pos, dst_pos): """! Initializer @param self: this object @param src_pos: source position @param dst_pos: destination position @return none """ self.__lines = [] self.__src_pos = int(src_pos) self.__dst_pos = int(dst_pos) def src_start(self): """! Source start function @param self this object @return source position """ return self.__src_pos def add_line(self,line): """! Add line function @param self The current class @param line line to add @return none """ self.__lines.append(line) def src(self): """! Get source lines @param self The current class @return the source lines """ src = [] for line in self.__lines: if line.is_src(): src.append(line) return src def dst(self): """! Get destination lines @param self The current class @return the destination lines """ dst = [] for line in self.__lines: if line.is_dst(): dst.append(line) return dst def src_len(self): """! Get number of source lines @param self The current class @return number of source lines """ return len(self.src()) def dst_len(self): """! Get number of destinaton lines @param self The current class @return number of destination lines """ return len(self.dst()) def write(self,f): """! Write lines to file @param self The current class @param f: file to write to @return none """ f.write('@@ -%d,%d +%d,%d @@\n' % (self.__src_pos, self.src_len(), self.__dst_pos, self.dst_len())) for line in self.__lines: line.write(f) ## Patch class class Patch: ## @var __src # source ## @var __dst # destination ## @var __chunks # chunks def __init__(self): """! Initializer @param self The current class @return none """ self.__src = '' self.__dst = '' self.__chunks = [] def add_chunk(self, chunk): """! Add chunk @param self this object @param chunk chunk @return none """ self.__chunks.append(chunk) def chunks(self): """! Get the chunks @param self The current class @return the chunks """ return self.__chunks def set_src(self,src): """! Set source @param self this object @param src source @return none """ self.__src = src def set_dst(self,dst): """! Set destination @param self this object @param dst destintion @return none """ self.__dst = dst def apply(self,filename): """! Apply function @param self The current class @param filename file name @return none """ # XXX: not implemented return def write(self,f): """! Write to file @param self The current class @param f the file @return none """ f.write('--- %s\n' % self.__src ) f.write('+++ %s\n' % self.__dst ) for chunk in self.__chunks: chunk.write(f) def parse_patchset(generator): src_file = re.compile('^--- (.*)$') dst_file = re.compile('^\+\+\+ (.*)$') chunk_start = re.compile('^@@ -([0-9]+),([0-9]+) \+([0-9]+),([0-9]+) @@') src = re.compile('^-(.*)$') dst = re.compile('^\+(.*)$') both = re.compile('^ (.*)$') patchset = [] current_patch = None for line in generator: m = src_file.search(line) if m is not None: current_patch = Patch() patchset.append(current_patch) current_patch.set_src(m.group(1)) continue m = dst_file.search(line) if m is not None: current_patch.set_dst(m.group(1)) continue m = chunk_start.search(line) if m is not None: current_chunk = PatchChunk(m.group(1), m.group(3)) current_patch.add_chunk(current_chunk) continue m = src.search(line) if m is not None: l = PatchChunkLine() l.set_src(m.group(1)) current_chunk.add_line(l) continue m = dst.search(line) if m is not None: l = PatchChunkLine() l.set_dst(m.group(1)) current_chunk.add_line(l) continue m = both.search(line) if m is not None: l = PatchChunkLine() l.set_both(m.group(1)) current_chunk.add_line(l) continue raise Exception() return patchset def remove_trailing_whitespace_changes(patch_generator): whitespace = re.compile('^(.*)([ \t]+)$') patchset = parse_patchset(patch_generator) for patch in patchset: for chunk in patch.chunks(): src = chunk.src() dst = chunk.dst() try: for i in range(0,len(src)): s = src[i] d = dst[i] m = whitespace.search(s.line()) if m is not None and m.group(1) == d.line(): d.append_to_line(m.group(2)) except: return patchset return patchset def indent(source, debug, level): output = tempfile.mkstemp()[1] # apply uncrustify cfg = uncrustify_config_file(level) if debug: sys.stderr.write('original file=' + source + '\n') sys.stderr.write('uncrustify config file=' + cfg + '\n') sys.stderr.write('temporary file=' + output + '\n') try: uncrust = subprocess.Popen(['uncrustify', '-c', cfg, '-f', source, '-o', output], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) (out, err) = uncrust.communicate('') if debug: sys.stderr.write(out) sys.stderr.write(err) except OSError: raise Exception ('uncrustify not installed') # generate a diff file src = open(source, 'r') dst = open(output, 'r') diff = difflib.unified_diff(src.readlines(), dst.readlines(), fromfile=source, tofile=output) src.close() dst.close() if debug: initial_diff = tempfile.mkstemp()[1] sys.stderr.write('initial diff file=' + initial_diff + '\n') tmp = open(initial_diff, 'w') tmp.writelines(diff) tmp.close() final_diff = tempfile.mkstemp()[1] if level < 3: patchset = remove_trailing_whitespace_changes(diff); dst = open(final_diff, 'w') if len(patchset) != 0: patchset[0].write(dst) dst.close() else: dst = open(final_diff, 'w') dst.writelines(diff) dst.close() # apply diff file if debug: sys.stderr.write('final diff file=' + final_diff + '\n') shutil.copyfile(source,output) patch = subprocess.Popen(['patch', '-p1', '-i', final_diff, output], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) (out, err) = patch.communicate('') if debug: sys.stderr.write(out) sys.stderr.write(err) return output def indent_files(files, diff=False, debug=False, level=0, inplace=False): output = [] for f in files: dst = indent(f, debug=debug, level=level) output.append([f,dst]) # First, copy to inplace if inplace: for src,dst in output: shutil.copyfile(dst,src) return True # now, compare failed = [] for src,dst in output: if filecmp.cmp(src,dst) == 0: failed.append([src, dst]) if len(failed) > 0: if not diff: print('Found %u badly indented files:' % len(failed)) for src,dst in failed: print(' ' + src) else: for src,dst in failed: s = open(src, 'r').readlines() d = open(dst, 'r').readlines() for line in difflib.unified_diff(s, d, fromfile=src, tofile=dst): sys.stdout.write(line) return False return True def run_as_hg_hook(ui, repo, **kwargs): # hack to work around mercurial < 1.3 bug from mercurial import lock, error lock.LockError = error.LockError # actually do the work files = hg_modified_files() if not indent_files(files, inplace=False): return True return False def run_as_main(): parser = optparse.OptionParser() parser.add_option('--debug', action='store_true', dest='debug', default=False, help='Output some debugging information') parser.add_option('-l', '--level', type='int', dest='level', default=0, help="Level of style conformance: higher levels include all lower levels. " "level=0: re-indent only. level=1: add extra spaces. level=2: insert extra newlines and " "extra braces around single-line statements. level=3: remove all trailing spaces") parser.add_option('--check-hg-hook', action='store_true', dest='hg_hook', default=False, help='Get the list of files to check from mercurial\'s list of modified ' 'and added files and assume that the script runs as a pretxncommit mercurial hook') parser.add_option('--check-hg', action='store_true', dest='hg', default=False, help="Get the list of files to check from mercurial\'s list of modified and added files") parser.add_option('-f', '--check-file', action='store', dest='file', default='', help="Check a single file") parser.add_option('--diff', action='store_true', dest='diff', default=False, help="Generate a diff on stdout of the indented files") parser.add_option('-i', '--in-place', action='store_true', dest='in_place', default=False, help="Indent the input files in-place") (options,args) = parser.parse_args() debug = options.debug if options.hg_hook: files = hg_modified_files() if not indent_files(files, debug=options.debug, level=options.level, inplace=False): sys.exit(1) elif options.hg: files = hg_modified_files() indent_files(files, diff=options.diff, debug=options.debug, level=options.level, inplace=options.in_place) elif options.file != '': file = options.file if not os.path.exists(file) or \ not os.path.isfile(file): print('file %s does not exist' % file) sys.exit(1) indent_files([file], diff=options.diff, debug=options.debug, level=options.level, inplace=options.in_place) sys.exit(0) if __name__ == '__main__': # try: run_as_main() # except Exception, e: # sys.stderr.write(str(e) + '\n') # sys.exit(1)