blob: 0578c04470543d8dec2ac141f2d786335efc5247 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2011 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Invokes the specified (quoted) command for all files modified
between the current git branch and the specified branch or commit.
The special token [[FILENAME]] (or whatever you choose using the -t
flag) is replaced with each of the filenames of new or modified files.
Deleted files are not included. Neither are untracked files.
Synopsis:
%prog [-b BRANCH] [-d] [-x EXTENSIONS|-c|-g] [-t TOKEN] QUOTED_COMMAND
Examples:
%prog -x gyp,gypi "tools/format_xml.py [[FILENAME]]"
%prog -c "tools/sort-headers.py [[FILENAME]]"
%prog -g "tools/sort_sources.py [[FILENAME]]"
%prog -t "~~BINGO~~" "echo I modified ~~BINGO~~"
"""
import optparse
import os
import subprocess
import sys
# List of C++-like source file extensions.
_CPP_EXTENSIONS = ('h', 'hh', 'hpp', 'c', 'cc', 'cpp', 'cxx', 'mm',)
# List of build file extensions.
_BUILD_EXTENSIONS = ('gyp', 'gypi', 'gn',)
def GitShell(args, ignore_return=False):
"""A shell invocation suitable for communicating with git. Returns
output as list of lines, raises exception on error.
"""
job = subprocess.Popen(args,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
(out, err) = job.communicate()
if job.returncode != 0 and not ignore_return:
print(out)
raise Exception("Error %d running command %s" % (
job.returncode, args))
return out.split('\n')
def FilenamesFromGit(branch_name, extensions):
"""Provides a list of all new and modified files listed by [git diff
branch_name] where branch_name can be blank to get a diff of the
workspace.
Excludes deleted files.
If extensions is not an empty list, include only files with one of
the extensions on the list.
"""
lines = GitShell('git diff --stat=600,500 %s' % branch_name)
filenames = []
for line in lines:
line = line.lstrip()
# Avoid summary line, and files that have been deleted (no plus).
if line.find('|') != -1 and line.find('+') != -1:
filename = line.split()[0]
if filename:
filename = filename.rstrip()
ext = filename.rsplit('.')[-1]
if not extensions or ext in extensions:
filenames.append(filename)
return filenames
def ForAllTouchedFiles(branch_name, extensions, token, command):
"""For each new or modified file output by [git diff branch_name],
run command with token replaced with the filename. If extensions is
not empty, do this only for files with one of the extensions in that
list.
"""
filenames = FilenamesFromGit(branch_name, extensions)
for filename in filenames:
os.system(command.replace(token, filename))
def main():
parser = optparse.OptionParser(usage=__doc__)
parser.add_option('-x', '--extensions', default='', dest='extensions',
help='Limits to files with given extensions '
'(comma-separated).')
parser.add_option('-c', '--cpp', default=False, action='store_true',
dest='cpp_only',
help='Runs your command only on C++-like source files.')
# -g stands for GYP and GN.
parser.add_option('-g', '--build', default=False, action='store_true',
dest='build_only',
help='Runs your command only on build files.')
parser.add_option('-t', '--token', default='[[FILENAME]]', dest='token',
help='Sets the token to be replaced for each file '
'in your command (default [[FILENAME]]).')
parser.add_option('-b', '--branch', default='origin/master', dest='branch',
help='Sets what to diff to (default origin/master). Set '
'to empty to diff workspace against HEAD.')
opts, args = parser.parse_args()
if not args:
parser.print_help()
sys.exit(1)
if opts.cpp_only and opts.build_only:
parser.error("--cpp and --build are mutually exclusive")
extensions = opts.extensions
if opts.cpp_only:
extensions = _CPP_EXTENSIONS
if opts.build_only:
extensions = _BUILD_EXTENSIONS
ForAllTouchedFiles(opts.branch, extensions, opts.token, args[0])
if __name__ == '__main__':
main()