Home | History | Annotate | Download | only in clang-format
      1 #!/usr/bin/env python
      2 #
      3 #===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===#
      4 #
      5 #                     The LLVM Compiler Infrastructure
      6 #
      7 # This file is distributed under the University of Illinois Open Source
      8 # License. See LICENSE.TXT for details.
      9 #
     10 #===------------------------------------------------------------------------===#
     11 
     12 r"""
     13 ClangFormat Diff Reformatter
     14 ============================
     15 
     16 This script reads input from a unified diff and reformats all the changed
     17 lines. This is useful to reformat all the lines touched by a specific patch.
     18 Example usage for git/svn users:
     19 
     20   git diff -U0 HEAD^ | clang-format-diff.py -p1 -i
     21   svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i
     22 
     23 """
     24 
     25 import argparse
     26 import difflib
     27 import re
     28 import string
     29 import subprocess
     30 import StringIO
     31 import sys
     32 
     33 
     34 # Change this to the full path if clang-format is not on the path.
     35 binary = 'clang-format'
     36 
     37 
     38 def main():
     39   parser = argparse.ArgumentParser(description=
     40                                    'Reformat changed lines in diff. Without -i '
     41                                    'option just output the diff that would be '
     42                                    'introduced.')
     43   parser.add_argument('-i', action='store_true', default=False,
     44                       help='apply edits to files instead of displaying a diff')
     45   parser.add_argument('-p', metavar='NUM', default=0,
     46                       help='strip the smallest prefix containing P slashes')
     47   parser.add_argument('-regex', metavar='PATTERN', default=None,
     48                       help='custom pattern selecting file paths to reformat '
     49                       '(case sensitive, overrides -iregex)')
     50   parser.add_argument('-iregex', metavar='PATTERN', default=
     51                       r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc|js|ts|proto'
     52                       r'|protodevel|java)',
     53                       help='custom pattern selecting file paths to reformat '
     54                       '(case insensitive, overridden by -regex)')
     55   parser.add_argument('-v', '--verbose', action='store_true',
     56                       help='be more verbose, ineffective without -i')
     57   parser.add_argument(
     58       '-style',
     59       help=
     60       'formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)')
     61   args = parser.parse_args()
     62 
     63   # Extract changed lines for each file.
     64   filename = None
     65   lines_by_file = {}
     66   for line in sys.stdin:
     67     match = re.search('^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
     68     if match:
     69       filename = match.group(2)
     70     if filename == None:
     71       continue
     72 
     73     if args.regex is not None:
     74       if not re.match('^%s$' % args.regex, filename):
     75         continue
     76     else:
     77       if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
     78         continue
     79 
     80     match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
     81     if match:
     82       start_line = int(match.group(1))
     83       line_count = 1
     84       if match.group(3):
     85         line_count = int(match.group(3))
     86       if line_count == 0:
     87         continue
     88       end_line = start_line + line_count - 1;
     89       lines_by_file.setdefault(filename, []).extend(
     90           ['-lines', str(start_line) + ':' + str(end_line)])
     91 
     92   # Reformat files containing changes in place.
     93   for filename, lines in lines_by_file.iteritems():
     94     if args.i and args.verbose:
     95       print 'Formatting', filename
     96     command = [binary, filename]
     97     if args.i:
     98       command.append('-i')
     99     command.extend(lines)
    100     if args.style:
    101       command.extend(['-style', args.style])
    102     p = subprocess.Popen(command, stdout=subprocess.PIPE,
    103                          stderr=None, stdin=subprocess.PIPE)
    104     stdout, stderr = p.communicate()
    105     if p.returncode != 0:
    106       sys.exit(p.returncode);
    107 
    108     if not args.i:
    109       with open(filename) as f:
    110         code = f.readlines()
    111       formatted_code = StringIO.StringIO(stdout).readlines()
    112       diff = difflib.unified_diff(code, formatted_code,
    113                                   filename, filename,
    114                                   '(before formatting)', '(after formatting)')
    115       diff_string = string.join(diff, '')
    116       if len(diff_string) > 0:
    117         sys.stdout.write(diff_string)
    118 
    119 if __name__ == '__main__':
    120   main()
    121