blob: 633fd078cdad4bcf5e8ac7e2b8ec8921b39383a1 [file] [log] [blame]
James Robinson646469d2014-10-03 15:33:28 -07001#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7 Invokes the specified (quoted) command for all files modified
8 between the current git branch and the specified branch or commit.
9
10 The special token [[FILENAME]] (or whatever you choose using the -t
11 flag) is replaced with each of the filenames of new or modified files.
12
13 Deleted files are not included. Neither are untracked files.
14
15Synopsis:
Etienne Membrives386015a2015-02-19 17:27:12 +010016 %prog [-b BRANCH] [-d] [-x EXTENSIONS|-c|-g] [-t TOKEN] QUOTED_COMMAND
James Robinson646469d2014-10-03 15:33:28 -070017
18Examples:
19 %prog -x gyp,gypi "tools/format_xml.py [[FILENAME]]"
20 %prog -c "tools/sort-headers.py [[FILENAME]]"
Etienne Membrives386015a2015-02-19 17:27:12 +010021 %prog -g "tools/sort_sources.py [[FILENAME]]"
James Robinson646469d2014-10-03 15:33:28 -070022 %prog -t "~~BINGO~~" "echo I modified ~~BINGO~~"
23"""
24
25import optparse
26import os
27import subprocess
28import sys
29
30
31# List of C++-like source file extensions.
32_CPP_EXTENSIONS = ('h', 'hh', 'hpp', 'c', 'cc', 'cpp', 'cxx', 'mm',)
Etienne Membrives386015a2015-02-19 17:27:12 +010033# List of build file extensions.
34_BUILD_EXTENSIONS = ('gyp', 'gypi', 'gn',)
James Robinson646469d2014-10-03 15:33:28 -070035
36
37def GitShell(args, ignore_return=False):
38 """A shell invocation suitable for communicating with git. Returns
39 output as list of lines, raises exception on error.
40 """
41 job = subprocess.Popen(args,
42 shell=True,
43 stdout=subprocess.PIPE,
44 stderr=subprocess.STDOUT)
45 (out, err) = job.communicate()
46 if job.returncode != 0 and not ignore_return:
47 print out
48 raise Exception("Error %d running command %s" % (
49 job.returncode, args))
50 return out.split('\n')
51
52
53def FilenamesFromGit(branch_name, extensions):
54 """Provides a list of all new and modified files listed by [git diff
55 branch_name] where branch_name can be blank to get a diff of the
56 workspace.
57
58 Excludes deleted files.
59
60 If extensions is not an empty list, include only files with one of
61 the extensions on the list.
62 """
63 lines = GitShell('git diff --stat=600,500 %s' % branch_name)
64 filenames = []
65 for line in lines:
66 line = line.lstrip()
67 # Avoid summary line, and files that have been deleted (no plus).
68 if line.find('|') != -1 and line.find('+') != -1:
69 filename = line.split()[0]
70 if filename:
71 filename = filename.rstrip()
72 ext = filename.rsplit('.')[-1]
73 if not extensions or ext in extensions:
74 filenames.append(filename)
75 return filenames
76
77
78def ForAllTouchedFiles(branch_name, extensions, token, command):
79 """For each new or modified file output by [git diff branch_name],
80 run command with token replaced with the filename. If extensions is
81 not empty, do this only for files with one of the extensions in that
82 list.
83 """
84 filenames = FilenamesFromGit(branch_name, extensions)
85 for filename in filenames:
86 os.system(command.replace(token, filename))
87
88
89def main():
90 parser = optparse.OptionParser(usage=__doc__)
91 parser.add_option('-x', '--extensions', default='', dest='extensions',
92 help='Limits to files with given extensions '
93 '(comma-separated).')
94 parser.add_option('-c', '--cpp', default=False, action='store_true',
95 dest='cpp_only',
96 help='Runs your command only on C++-like source files.')
Etienne Membrives386015a2015-02-19 17:27:12 +010097 # -g stands for GYP and GN.
98 parser.add_option('-g', '--build', default=False, action='store_true',
99 dest='build_only',
100 help='Runs your command only on build files.')
James Robinson646469d2014-10-03 15:33:28 -0700101 parser.add_option('-t', '--token', default='[[FILENAME]]', dest='token',
102 help='Sets the token to be replaced for each file '
103 'in your command (default [[FILENAME]]).')
104 parser.add_option('-b', '--branch', default='origin/master', dest='branch',
105 help='Sets what to diff to (default origin/master). Set '
106 'to empty to diff workspace against HEAD.')
107 opts, args = parser.parse_args()
108
109 if not args:
110 parser.print_help()
111 sys.exit(1)
112
Etienne Membrives386015a2015-02-19 17:27:12 +0100113 if opts.cpp_only and opts.build_only:
114 parser.error("--cpp and --build are mutually exclusive")
115
James Robinson646469d2014-10-03 15:33:28 -0700116 extensions = opts.extensions
117 if opts.cpp_only:
118 extensions = _CPP_EXTENSIONS
Etienne Membrives386015a2015-02-19 17:27:12 +0100119 if opts.build_only:
120 extensions = _BUILD_EXTENSIONS
James Robinson646469d2014-10-03 15:33:28 -0700121
122 ForAllTouchedFiles(opts.branch, extensions, opts.token, args[0])
123
124
125if __name__ == '__main__':
126 main()