|
#!/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()) |