#!/usr/bin/python
# Copyright (c) 2009 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.

"""Top-level presubmit script for Chromium.

See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts for
details on the presubmit API built into gcl.
"""

# Files with these extensions will be considered source files.
SOURCE_FILE_EXTENSIONS = [
    '.c', '.cc', '.cpp', '.h', '.m', '.mm', '.py', '.mk', '.am', '.json',
]
EXCLUDED_PATHS = [
    r"breakpad[\\\/].*",
    r"chrome[\\\/]Debug[\\\/].*",
    r"chrome[\\\/]Release[\\\/].*",
    r"sconsbuild[\\\/].*",
    r"xcodebuild[\\\/].*",
    r"skia[\\\/].*",
    r".*third_party[\\\/].*",
    r"v8[\\\/].*",
]

def ReadFile(path):
  """Given a path, returns the full contents of the file.

  Reads files in binary format.
  """
  fo = open(path, 'rb')
  try:
    contents = fo.read()
  finally:
    fo.close()
  return contents


def CheckChangeOnUpload(input_api, output_api):
  # TODO(maruel): max_cols is temporarily disabled. Reenable once the source
  # tree is in better shape.
  results = []
  results.extend(LocalChecks(input_api, output_api, max_cols=0))
  results.extend(input_api.canned_checks.CheckChangeHasBugField(input_api,
                                                                output_api))
  results.extend(input_api.canned_checks.CheckChangeHasTestField(input_api,
                                                                 output_api))
  return results


def CheckChangeOnCommit(input_api, output_api):
  results = []
  # TODO(maruel): max_cols is temporarily disabled. Reenable once the source
  # tree is in better shape.
  results.extend(LocalChecks(input_api, output_api, max_cols=0))
  results.extend(input_api.canned_checks.CheckChangeHasBugField(input_api,
                                                                output_api))
  results.extend(input_api.canned_checks.CheckChangeHasTestField(input_api,
                                                                 output_api))
  # Make sure the tree is 'open'.
  results.extend(input_api.canned_checks.CheckTreeIsOpen(
      input_api, output_api,
      'http://chromium-status.appspot.com/status', '0'
  ))
  results.extend(CheckTryJobExecution(input_api, output_api))
  return results


def LocalChecks(input_api, output_api, max_cols=80):
  """Reports an error if for any source file in SOURCE_FILE_EXTENSIONS:
     - uses CR (or CRLF)
     - contains a TAB
     - has a line that ends with whitespace
     - contains a line >|max_cols| cols unless |max_cols| is 0.
     - File does not end in a newline, or ends in more than one.

  Note that the whole file is checked, not only the changes.
  """
  C_SOURCE_FILE_EXTENSIONS = ('.c', '.cc', '.cpp', '.h', '.inl')
  cr_files = []
  eof_files = []
  results = []
  excluded_paths = [input_api.re.compile(x) for x in EXCLUDED_PATHS]
  files = input_api.AffectedFiles(include_deletes=False)
  for f in files:
    path = f.LocalPath()
    root, ext = input_api.os_path.splitext(path)
    # Look for unsupported extensions.
    if not ext in SOURCE_FILE_EXTENSIONS:
      continue
    # Look for excluded paths.
    found = False
    for item in excluded_paths:
      if item.match(path):
        found = True
        break
    if found:
      continue

    # Need to read the file ourselves since AffectedFile.NewContents()
    # will normalize line endings.
    contents = ReadFile(f.AbsoluteLocalPath())
    if '\r' in contents:
      cr_files.append(path)

    # Check that the file ends in one and only one newline character.
    if len(contents) > 0 and (contents[-1:] != "\n" or contents[-2:-1] == "\n"):
      eof_files.append(path)

    local_errors = []
    # Remove end of line character.
    lines = contents.splitlines()
    line_num = 1
    for line in lines:
      if line.endswith(' '):
        local_errors.append(output_api.PresubmitError(
            '%s, line %s ends with whitespaces.' %
            (path, line_num)))
      # Accept lines with http://, https:// and C #define/#pragma/#include to
      # exceed the max_cols rule.
      if (max_cols and
          len(line) > max_cols and
          not 'http://' in line and
          not 'https://' in line and
          not (line[0] == '#' and ext in C_SOURCE_FILE_EXTENSIONS)):
        local_errors.append(output_api.PresubmitError(
            '%s, line %s has %s chars, please reduce to %d chars.' %
            (path, line_num, len(line), max_cols)))
      if '\t' in line:
        local_errors.append(output_api.PresubmitError(
            "%s, line %s contains a tab character." %
            (path, line_num)))
      line_num += 1
      # Just show the first 5 errors.
      if len(local_errors) == 6:
        local_errors.pop()
        local_errors.append(output_api.PresubmitError("... and more."))
        break
    results.extend(local_errors)

  if cr_files:
    results.append(output_api.PresubmitError(
        'Found CR (or CRLF) line ending in these files, please use only LF:',
        items=cr_files))
  if eof_files:
    results.append(output_api.PresubmitError(
        'These files should end in one (and only one) newline character:',
        items=eof_files))
  return results


def CheckTryJobExecution(input_api, output_api):
  url = "http://codereview.chromium.org/%d/get_build_results/%d" % (
            input_api.change.issue, input_api.change.patchset)
  outputs = []
  try:
    connection = input_api.urllib2.urlopen(url)
    # platform|status|url
    values = [item.split('|', 2) for item in connection.read().splitlines()]
    connection.close()
    statuses = map(lambda x: x[1], values)
    if 'failure' in statuses:
      failures = filter(lambda x: x[1] != 'success', values)
      long_text = '\n'.join("% 5s: % 7s %s" % (item[0], item[1], item[2])
                            for item in failures)
      # TODO(maruel): Change to a PresubmitPromptWarning once the try server is
      # stable enough and it seems to work fine.
      message = 'You had try job failures. Are you sure you want to check-in?'
      outputs.append(output_api.PresubmitNotifyResult(message=message,
                                                      long_text=long_text))
    elif 'pending' in statuses or len(values) != 3:
      long_text = '\n'.join("% 5s: % 7s %s" % (item[0], item[1], item[2])
                            for item in values)
      # TODO(maruel): Change to a PresubmitPromptWarning once the try server is
      # stable enough and it seems to work fine.
      message = 'You should try the patch first (and wait for it to finish).'
      outputs.append(output_api.PresubmitNotifyResult(message=message,
                                                      long_text=long_text))
  except input_api.urllib2.HTTPError, e:
    if e.code == 404:
      # Fallback to no try job.
      # TODO(maruel): Change to a PresubmitPromptWarning once the try server is
      # stable enough and it seems to work fine.
      outputs.append(output_api.PresubmitNotifyResult(
          'You should try the patch first.'))
    else:
      # Another HTTP error happened, warn the user.
      # TODO(maruel): Change to a PresubmitPromptWarning once it deemed to work
      # fine.
      outputs.append(output_api.PresubmitNotifyResult(
          'Got %s while looking for try job status.' % str(e)))
  return outputs