Home | History | Annotate | Download | only in git
      1 #!/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 
     15 Synopsis:
     16   %prog [-b BRANCH] [-d] [-x EXTENSIONS|-c] [-t TOKEN] QUOTED_COMMAND
     17 
     18 Examples:
     19   %prog -x gyp,gypi "tools/format_xml.py [[FILENAME]]"
     20   %prog -c "tools/sort-headers.py [[FILENAME]]"
     21   %prog -t "~~BINGO~~" "echo I modified ~~BINGO~~"
     22 """
     23 
     24 import optparse
     25 import os
     26 import subprocess
     27 import sys
     28 
     29 
     30 # List of C++-like source file extensions.
     31 _CPP_EXTENSIONS = ('h', 'hh', 'hpp', 'c', 'cc', 'cpp', 'cxx', 'mm',)
     32 
     33 
     34 def GitShell(args, ignore_return=False):
     35   """A shell invocation suitable for communicating with git. Returns
     36   output as list of lines, raises exception on error.
     37   """
     38   job = subprocess.Popen(args,
     39                          shell=True,
     40                          stdout=subprocess.PIPE,
     41                          stderr=subprocess.STDOUT)
     42   (out, err) = job.communicate()
     43   if job.returncode != 0 and not ignore_return:
     44     print out
     45     raise Exception("Error %d running command %s" % (
     46         job.returncode, args))
     47   return out.split('\n')
     48 
     49 
     50 def FilenamesFromGit(branch_name, extensions):
     51   """Provides a list of all new and modified files listed by [git diff
     52   branch_name] where branch_name can be blank to get a diff of the
     53   workspace.
     54 
     55   Excludes deleted files.
     56 
     57   If extensions is not an empty list, include only files with one of
     58   the extensions on the list.
     59   """
     60   lines = GitShell('git diff --stat=600,500 %s' % branch_name)
     61   filenames = []
     62   for line in lines:
     63     line = line.lstrip()
     64     # Avoid summary line, and files that have been deleted (no plus).
     65     if line.find('|') != -1 and line.find('+') != -1:
     66       filename = line.split()[0]
     67       if filename:
     68         filename = filename.rstrip()
     69         ext = filename.rsplit('.')[-1]
     70         if not extensions or ext in extensions:
     71           filenames.append(filename)
     72   return filenames
     73 
     74 
     75 def ForAllTouchedFiles(branch_name, extensions, token, command):
     76   """For each new or modified file output by [git diff branch_name],
     77   run command with token replaced with the filename. If extensions is
     78   not empty, do this only for files with one of the extensions in that
     79   list.
     80   """
     81   filenames = FilenamesFromGit(branch_name, extensions)
     82   for filename in filenames:
     83     os.system(command.replace(token, filename))
     84 
     85 
     86 def main():
     87   parser = optparse.OptionParser(usage=__doc__)
     88   parser.add_option('-x', '--extensions', default='', dest='extensions',
     89                     help='Limits to files with given extensions '
     90                     '(comma-separated).')
     91   parser.add_option('-c', '--cpp', default=False, action='store_true',
     92                     dest='cpp_only',
     93                     help='Runs your command only on C++-like source files.')
     94   parser.add_option('-t', '--token', default='[[FILENAME]]', dest='token',
     95                     help='Sets the token to be replaced for each file '
     96                     'in your command (default [[FILENAME]]).')
     97   parser.add_option('-b', '--branch', default='origin/master', dest='branch',
     98                     help='Sets what to diff to (default origin/master). Set '
     99                     'to empty to diff workspace against HEAD.')
    100   opts, args = parser.parse_args()
    101 
    102   if not args:
    103     parser.print_help()
    104     sys.exit(1)
    105 
    106   extensions = opts.extensions
    107   if opts.cpp_only:
    108     extensions = _CPP_EXTENSIONS
    109 
    110   ForAllTouchedFiles(opts.branch, extensions, opts.token, args[0])
    111 
    112 
    113 if __name__ == '__main__':
    114   main()
    115