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 def main():
     35   parser = argparse.ArgumentParser(description=
     36                                    'Reformat changed lines in diff. Without -i '
     37                                    'option just output the diff that would be '
     38                                    'introduced.')
     39   parser.add_argument('-i', action='store_true', default=False,
     40                       help='apply edits to files instead of displaying a diff')
     41   parser.add_argument('-p', metavar='NUM', default=0,
     42                       help='strip the smallest prefix containing P slashes')
     43   parser.add_argument('-regex', metavar='PATTERN', default=None,
     44                       help='custom pattern selecting file paths to reformat '
     45                       '(case sensitive, overrides -iregex)')
     46   parser.add_argument('-iregex', metavar='PATTERN', default=
     47                       r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc|js|ts|proto'
     48                       r'|protodevel|java)',
     49                       help='custom pattern selecting file paths to reformat '
     50                       '(case insensitive, overridden by -regex)')
     51   parser.add_argument('-sort-includes', action='store_true', default=False,
     52                       help='let clang-format sort include blocks')
     53   parser.add_argument('-v', '--verbose', action='store_true',
     54                       help='be more verbose, ineffective without -i')
     55   parser.add_argument('-style',
     56                       help='formatting style to apply (LLVM, Google, Chromium, '
     57                       'Mozilla, WebKit)')
     58   parser.add_argument('-binary', default='clang-format',
     59                       help='location of binary to use for clang-format')
     60   args = parser.parse_args()
     61 
     62   # Extract changed lines for each file.
     63   filename = None
     64   lines_by_file = {}
     65   for line in sys.stdin:
     66     match = re.search('^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
     67     if match:
     68       filename = match.group(2)
     69     if filename == None:
     70       continue
     71 
     72     if args.regex is not None:
     73       if not re.match('^%s$' % args.regex, filename):
     74         continue
     75     else:
     76       if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
     77         continue
     78 
     79     match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
     80     if match:
     81       start_line = int(match.group(1))
     82       line_count = 1
     83       if match.group(3):
     84         line_count = int(match.group(3))
     85       if line_count == 0:
     86         continue
     87       end_line = start_line + line_count - 1;
     88       lines_by_file.setdefault(filename, []).extend(
     89           ['-lines', str(start_line) + ':' + str(end_line)])
     90 
     91   # Reformat files containing changes in place.
     92   for filename, lines in lines_by_file.iteritems():
     93     if args.i and args.verbose:
     94       print 'Formatting', filename
     95     command = [args.binary, filename]
     96     if args.i:
     97       command.append('-i')
     98     if args.sort_includes:
     99       command.append('-sort-includes')
    100     command.extend(lines)
    101     if args.style:
    102       command.extend(['-style', args.style])
    103     p = subprocess.Popen(command, stdout=subprocess.PIPE,
    104                          stderr=None, stdin=subprocess.PIPE)
    105     stdout, stderr = p.communicate()
    106     if p.returncode != 0:
    107       sys.exit(p.returncode);
    108 
    109     if not args.i:
    110       with open(filename) as f:
    111         code = f.readlines()
    112       formatted_code = StringIO.StringIO(stdout).readlines()
    113       diff = difflib.unified_diff(code, formatted_code,
    114                                   filename, filename,
    115                                   '(before formatting)', '(after formatting)')
    116       diff_string = string.join(diff, '')
    117       if len(diff_string) > 0:
    118         sys.stdout.write(diff_string)
    119 
    120 if __name__ == '__main__':
    121   main()
    122