summaryrefslogtreecommitdiffstats
path: root/tools/git/for-all-touched-files.py
blob: a7e784ade3e1920ede69a20fdb597b89e7080f15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# 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] [-t TOKEN] QUOTED_COMMAND

Examples:
  %prog -x gyp,gypi "tools/format_xml.py [[FILENAME]]"
  %prog -c "tools/sort-headers.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',)


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.')
  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)

  extensions = opts.extensions
  if opts.cpp_only:
    extensions = _CPP_EXTENSIONS

  ForAllTouchedFiles(opts.branch, extensions, opts.token, args[0])


if __name__ == '__main__':
  main()