| #!/usr/bin/env python |
| # Copyright 2016 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Runs source formatters on modified files. |
| |
| In order to find the files to be formatted, this uses `git diff-index` against |
| the newest parent commit in the upstream branch (or against HEAD if no such |
| commit is found). In result, files that are locally modified, staged or touched |
| by any commits introduced on the local branch are formatted. |
| """ |
| |
| import argparse |
| import os |
| import platform |
| import subprocess |
| import sys |
| import git_utils |
| import paths |
| |
| |
| class Command(object): |
| """A formatting command.""" |
| |
| def __init__(self, cmd, rangefn=None): |
| """Defines a command. |
| |
| Args: |
| cmd: A commandline template to run, as a sequence of strings; |
| must accept a bare filename as the final arg. |
| rangefn: Optional function that accepts a sequence of line ranges |
| and returns additional commandline entries. See self.Make() |
| for a description of the ranges. |
| """ |
| self._cmd = tuple(cmd) |
| self._rangefn = rangefn |
| |
| def Make(self, fname, ranges=()): |
| """Returns the command as a sequence of strings. |
| |
| Args: |
| fname: The file to run the command on. |
| ranges: An optional description of modified lines, as a sequence |
| of 1-indexed (start-line, num-modified-lines) pairs. Not all |
| commands will respect these ranges. |
| Returns: |
| The command to format the specified file. |
| """ |
| cmd = list(self._cmd) |
| if ranges and self._rangefn: |
| cmd.extend(self._rangefn(ranges)) |
| cmd.append(fname) |
| return cmd |
| |
| |
| host_platform = "%s-%s" % ( |
| platform.system().lower().replace("darwin", "mac"), |
| { |
| "x86_64": "x64", |
| "aarch64": "arm64", |
| }[platform.machine()], |
| ) |
| CLANG_TOOL = os.path.join(paths.BUILDTOOLS_ROOT, host_platform, "clang", "bin", |
| "clang-format") |
| DART_TOOL = os.path.join(paths.DART_ROOT, "bin", "dartfmt") |
| GN_TOOL = os.path.join(paths.BUILDTOOLS_ROOT, "gn") |
| GO_TOOL = os.path.join(paths.BUILDTOOLS_ROOT, host_platform, "go", "bin", "gofmt") |
| CHECK_HEADER_GUARDS_TOOL = os.path.join(paths.FUCHSIA_ROOT, "scripts", "style", |
| "check-header-guards.py") |
| |
| CLANG_CMD = Command( |
| (CLANG_TOOL, "-style=file", "-fallback-style=Google", "-sort-includes", |
| "-i"), |
| rangefn= |
| lambda ranges: ['-lines=%d:%d' % (st, st + ln - 1) for st, ln in ranges]) |
| DART_CMD = Command((DART_TOOL, "-w")) |
| GN_CMD = Command((GN_TOOL, "format")) |
| GO_CMD = Command((GO_TOOL, "-w")) |
| FIX_HEADER_GUARDS_COMMAND = Command((CHECK_HEADER_GUARDS_TOOL, "--fix")) |
| PYTHON_CMD = Command( |
| ("yapf", "-i"), |
| rangefn= |
| lambda ranges: ['--lines=%d-%d' % (st, st + ln - 1) for st, ln in ranges]) |
| |
| EXT_TO_COMMANDS = { |
| ".cc": [CLANG_CMD], |
| ".cpp": [CLANG_CMD], |
| ".dart": [DART_CMD], |
| ".gn": [GN_CMD], |
| ".gni": [GN_CMD], |
| ".go": [GO_CMD], |
| ".h": [FIX_HEADER_GUARDS_COMMAND, CLANG_CMD], |
| ".hh": [CLANG_CMD], |
| ".hpp": [CLANG_CMD], |
| ".py": [PYTHON_CMD], |
| ".ts": [CLANG_CMD], |
| } |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description="Format modified files.") |
| parser.add_argument( |
| "--dry-run", |
| dest="dry_run", |
| action="store_true", |
| default=False, |
| help="just pretend to run stuff") |
| parser.add_argument( |
| "--verbose", |
| dest="verbose", |
| action="store_true", |
| default=False, |
| help="tell me what you're doing") |
| parser.add_argument( |
| "--lines", |
| dest="lines", |
| action="store_true", |
| default=False, |
| help="only format modified lines (for supported languages)") |
| parser.add_argument( |
| "--all", |
| dest="all", |
| action="store_true", |
| default=False, |
| help="format all files in the repo, not just the modified ones") |
| args = parser.parse_args() |
| |
| # Find the files to be formatted. |
| ranges = {} |
| if args.all: |
| files = git_utils.get_all_files() |
| else: |
| files = git_utils.get_diff_files() |
| if args.lines: |
| ranges = git_utils.get_modified_lines(files) |
| |
| if args.verbose: |
| print |
| print "Files to be formatted:" |
| if not files: |
| print " (no files)" |
| return |
| |
| for file in files: |
| print " - " + file + " " + ', '.join(repr(s) for s in ranges[file]) |
| |
| # Run the formatters. |
| if args.dry_run: |
| print |
| print "Would run the following formatters (dry run):" |
| elif args.verbose: |
| print "Running the following formatters:" |
| |
| count = 0 |
| |
| for file in files: |
| # Skip deleted files. |
| if not os.path.isfile(file): |
| continue |
| |
| _, extension = os.path.splitext(file) |
| if extension not in EXT_TO_COMMANDS: |
| # Sniff for a #! header |
| with open(file, 'r') as fp: |
| head = fp.read(80) |
| if head.startswith('#!') and 'python' in head: |
| extension = '.py' |
| if extension not in EXT_TO_COMMANDS: |
| continue |
| |
| count += 1 |
| cmds = EXT_TO_COMMANDS[extension] |
| for cmd in cmds: |
| cmd = cmd.Make(file, ranges=ranges.get(file, ())) |
| if args.dry_run or args.verbose: |
| print cmd |
| |
| if args.dry_run: |
| continue |
| |
| try: |
| subprocess.check_call(cmd) |
| except Exception as e: |
| print " ".join(cmd) + " failed" |
| raise e |
| |
| if (args.dry_run or args.verbose) and not count: |
| print " (none)" |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |