Look at PRESUBMIT.cfg file to disable style checks

- Use ConfigParser to look for hooks to disable in PRESUBMIT.cfg file in
  the project root.
- Add sample PRESUBMIT.cfg file that disables source style hooks.
- Add README file to explain PRESUBMIT.cfg.
- Add hint that directs user to README file for disabling hooks.

BUG=15162
TEST=Test with and without PRESUBMIT.cfg file.
     Test with and without disable flags in PRESUBMIT.cfg file
     Test without [Disable] section in PRESUBMIT.cfg file

Change-Id: Ia3a97516835f4f7f24cd35529d0e8db7afd0fbeb
Reviewed-on: http://gerrit.chromium.org/gerrit/740
Reviewed-by: Mandeep Singh Baines <msb@chromium.org>
Tested-by: Ryan Cui <rcui@chromium.org>
diff --git a/README b/README
new file mode 100644
index 0000000..2c53214
--- /dev/null
+++ b/README
@@ -0,0 +1,25 @@
+Disabling ChromiumOS source style checks
+===========================================
+
+If your project does not conform to the ChromiumOS source style (80 char lines,
+no tabs, ChromiumOS license header, etc.), you can disable the source style
+pre-upload hooks by copying the PRESUBMIT.cfg file in
+<checkout_dir>/src/repohooks/disable_cros_style_checks to the your project
+root.
+
+The sample config file disables all of the source style checks.  You can comment out the
+disable-flags for the checks you want to leave enabled.
+
+Some hints
+===========================================
+
+- Get the latest version of the hooks before running 'repo upload', by running
+  'repo sync chromiumos/repohooks'.
+     - When your hooks change, you will be prompted for permission to run the hooks even
+       if you answered 'yes-never-ask-again' previously.
+
+Reporting issues
+===========================================
+
+Please see https://sites.google.com/a/chromium.org/dev/chromium-os/developer-guide/gerrit-guide
+for instructions on reporting problems.
diff --git a/disable_cros_style_checks/PRESUBMIT.cfg b/disable_cros_style_checks/PRESUBMIT.cfg
new file mode 100644
index 0000000..51d8dd6
--- /dev/null
+++ b/disable_cros_style_checks/PRESUBMIT.cfg
@@ -0,0 +1,9 @@
+# This sample config file disables all of the ChromiumOS source style checks.
+# Comment out the disable-flags for any checks you want to leave enabled.
+
+[Hook Overrides]
+stray_whitespace_check: false
+long_line_check: false
+cros_license_check: false
+tab_check: false
+
diff --git a/pre-upload.py b/pre-upload.py
index 4b4799c..8f8aeef 100644
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -2,6 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import ConfigParser
 import json
 import os
 import re
@@ -38,6 +39,9 @@
 ]
 
 
+_CONFIG_FILE = 'PRESUBMIT.cfg'
+
+
 # General Helpers
 
 
@@ -315,30 +319,81 @@
 # Base
 
 
-COMMON_HOOKS = [_check_change_has_bug_field,
-                _check_change_has_test_field,
-                _check_change_has_proper_changeid,
-                _check_no_stray_whitespace,
-                _check_no_long_lines,
-                _check_license,
-                _check_no_tabs]
+# A list of hooks that are not project-specific
+_COMMON_HOOKS = [
+    _check_change_has_bug_field,
+    _check_change_has_test_field,
+    _check_change_has_proper_changeid,
+    _check_no_stray_whitespace,
+    _check_no_long_lines,
+    _check_license,
+    _check_no_tabs,
+]
 
 
-def _setup_project_hooks():
-  """Returns a dictionay of callbacks: dict[project] = [callback1, callback2]"""
-  return {
+# A dictionary of project-specific hooks(callbacks), indexed by project name.
+# dict[project] = [callback1, callback2]
+_PROJECT_SPECIFIC_HOOKS = {
     "chromiumos/third_party/kernel": [_run_checkpatch],
     "chromiumos/third_party/kernel-next": [_run_checkpatch],
     "chromeos/autotest-tools": [_run_json_check],
-    }
+}
 
 
-def _run_project_hooks(project, hooks):
+# A dictionary of flags (keys) that can appear in the config file, and the hook
+# that the flag disables (value)
+_DISABLE_FLAGS = {
+    'stray_whitespace_check': _check_no_stray_whitespace,
+    'long_line_check': _check_no_long_lines,
+    'cros_license_check': _check_license,
+    'tab_check': _check_no_tabs,
+}
+
+
+def _get_disabled_hooks():
+  """Returns a set of hooks disabled by the current project's config file.
+
+  Expects to be called within the project root.
+  """
+  SECTION = 'Hook Overrides'
+  config = ConfigParser.RawConfigParser()
+  try:
+    config.read(_CONFIG_FILE)
+    flags = config.options(SECTION)
+  except ConfigParser.Error:
+    return set([])
+
+  disable_flags = []
+  for flag in flags:
+    try:
+      if not config.getboolean(SECTION, flag): disable_flags.append(flag)
+    except ValueError as e:
+      msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
+      print msg + str(e)
+
+  disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
+  return set([_DISABLE_FLAGS[key] for key in disabled_keys])
+
+
+def _get_project_hooks(project):
+  """Returns a list of hooks that need to be run for a project.
+
+  Expects to be called from within the project root.
+  """
+  disabled_hooks = _get_disabled_hooks()
+  hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
+
+  if project in _PROJECT_SPECIFIC_HOOKS:
+    hooks.extend(_PROJECT_SPECIFIC_HOOKS[project])
+
+  return hooks
+
+
+def _run_project_hooks(project):
   """For each project run its project specific hook from the hooks dictionary.
 
   Args:
     project: name of project to run hooks for.
-    hooks: a dictionary of hooks indexed by project name
 
   Returns:
     Boolean value of whether any errors were ecountered while running the hooks.
@@ -348,10 +403,6 @@
   # hooks assume they are run from the root of the project
   os.chdir(proj_dir)
 
-  project_specific_hooks = []
-  if project in hooks:
-    project_specific_hooks = hooks[project]
-
   try:
     commit_list = _get_commits()
   except VerifyException as e:
@@ -359,10 +410,11 @@
     os.chdir(pwd)
     return True
 
+  hooks = _get_project_hooks(project)
   error_found = False
   for commit in commit_list:
     error_list = []
-    for hook in COMMON_HOOKS + project_specific_hooks:
+    for hook in hooks:
       hook_error = hook(project, commit)
       if hook_error:
         error_list.append(hook_error)
@@ -379,18 +431,16 @@
 
 
 def main(project_list, **kwargs):
-  hooks = _setup_project_hooks()
-
   found_error = False
   for project in project_list:
-    if _run_project_hooks(project, hooks):
+    if _run_project_hooks(project):
       found_error = True
 
   if (found_error):
     msg = ('Preupload failed due to errors in project(s). HINTS:\n'
-           '- To upload only current project, run \'repo upload .\'\n'
-           '- Errors may also be due to old upload hooks.  Please run '
-           '\'repo sync chromiumos/repohooks\' to update.')
+           '- To disable some source style checks, and for other hints, see '
+           '<checkout_dir>/src/repohooks/README\n'
+           '- To upload only current project, run \'repo upload .\'')
     print >> sys.stderr, msg
     sys.exit(1)