#!/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,
    text=True,
  )
  (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()
