blob: 1a2422c708e573ea5583fa42a6ed121bfe7127d6 [file] [log] [blame]
#!/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())