Home | History | Annotate | Download | only in clang-format
      1 #!/usr/bin/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|proto'
     52                       r'|protodevel)',
     53                       help='custom pattern selecting file paths to reformat '
     54                       '(case insensitive, overridden by -regex)')
     55   parser.add_argument(
     56       '-style',
     57       help=
     58       'formatting style to apply (LLVM, Google, Chromium, Mozilla, WebKit)')
     59   args = parser.parse_args()
     60 
     61   # Extract changed lines for each file.
     62   filename = None
     63   lines_by_file = {}
     64   for line in sys.stdin:
     65     match = re.search('^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
     66     if match:
     67       filename = match.group(2)
     68     if filename == None:
     69       continue
     70 
     71     if args.regex is not None:
     72       if not re.match('^%s$' % args.regex, filename):
     73         continue
     74     else:
     75       if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
     76         continue
     77 
     78     match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
     79     if match:
     80       start_line = int(match.group(1))
     81       line_count = 1
     82       if match.group(3):
     83         line_count = int(match.group(3))
     84       if line_count == 0:
     85         continue
     86       end_line = start_line + line_count - 1;
     87       lines_by_file.setdefault(filename, []).extend(
     88           ['-lines', str(start_line) + ':' + str(end_line)])
     89 
     90   # Reformat files containing changes in place.
     91   for filename, lines in lines_by_file.iteritems():
     92     command = [binary, filename]
     93     if args.i:
     94       command.append('-i')
     95     command.extend(lines)
     96     if args.style:
     97       command.extend(['-style', args.style])
     98     p = subprocess.Popen(command, stdout=subprocess.PIPE,
     99                          stderr=None, stdin=subprocess.PIPE)
    100     stdout, stderr = p.communicate()
    101     if p.returncode != 0:
    102       sys.exit(p.returncode);
    103 
    104     if not args.i:
    105       with open(filename) as f:
    106         code = f.readlines()
    107       formatted_code = StringIO.StringIO(stdout).readlines()
    108       diff = difflib.unified_diff(code, formatted_code,
    109                                   filename, filename,
    110                                   '(before formatting)', '(after formatting)')
    111       diff_string = string.join(diff, '')
    112       if len(diff_string) > 0:
    113         sys.stdout.write(diff_string)
    114 
    115 if __name__ == '__main__':
    116   main()
    117