Add experimental support for python in 'git cl format'

Based on yapf (https://github.com/google/yapf) this
formatter currently only works with --full.  It defaults
to pep8 style and projects that use a different style
can add .style.yapf to the top level.

Review URL: https://codereview.chromium.org/1156743008

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@295547 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/.style.yapf b/.style.yapf
new file mode 100644
index 0000000..de0c6a7
--- /dev/null
+++ b/.style.yapf
@@ -0,0 +1,2 @@
+[style]
+based_on_style = chromium
diff --git a/gclient_utils.py b/gclient_utils.py
index fbcbc26..21c44c3 100644
--- a/gclient_utils.py
+++ b/gclient_utils.py
@@ -698,6 +698,8 @@
     return override
 
   primary_solution = GetPrimarySolutionPath()
+  if not primary_solution:
+    return None
   buildtools_path = os.path.join(primary_solution, 'buildtools')
   if not os.path.exists(buildtools_path):
     # Buildtools may be in the gclient root.
@@ -1171,6 +1173,7 @@
   logging.debug('Failed to get CPU count. Defaulting to 1.')
   return 1
 
+
 def DefaultDeltaBaseCacheLimit():
   """Return a reasonable default for the git config core.deltaBaseCacheLimit.
 
@@ -1183,6 +1186,7 @@
   else:
     return '512m'
 
+
 def DefaultIndexPackConfig(url=''):
   """Return reasonable default values for configuring git-index-pack.
 
@@ -1193,3 +1197,21 @@
   if url in THREADED_INDEX_PACK_BLACKLIST:
     result.extend(['-c', 'pack.threads=1'])
   return result
+
+
+def FindExecutable(executable):
+  """This mimics the "which" utility."""
+  path_folders = os.environ.get('PATH').split(os.pathsep)
+
+  for path_folder in path_folders:
+    target = os.path.join(path_folder, executable)
+    # Just incase we have some ~/blah paths.
+    target = os.path.abspath(os.path.expanduser(target))
+    if os.path.isfile(target) and os.access(target, os.X_OK):
+      return target
+    if sys.platform.startswith('win'):
+      for suffix in ('.bat', '.cmd', '.exe'):
+        alt_target = target + suffix
+        if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
+          return alt_target
+  return None
diff --git a/git_cache.py b/git_cache.py
index 8b27842..e80923c 100755
--- a/git_cache.py
+++ b/git_cache.py
@@ -185,24 +185,6 @@
     netpath = re.sub(r'\b-\b', '/', os.path.basename(path)).replace('--', '-')
     return 'https://%s' % netpath
 
-  @staticmethod
-  def FindExecutable(executable):
-    """This mimics the "which" utility."""
-    path_folders = os.environ.get('PATH').split(os.pathsep)
-
-    for path_folder in path_folders:
-      target = os.path.join(path_folder, executable)
-      # Just incase we have some ~/blah paths.
-      target = os.path.abspath(os.path.expanduser(target))
-      if os.path.isfile(target) and os.access(target, os.X_OK):
-        return target
-      if sys.platform.startswith('win'):
-        for suffix in ('.bat', '.cmd', '.exe'):
-          alt_target = target + suffix
-          if os.path.isfile(alt_target) and os.access(alt_target, os.X_OK):
-            return alt_target
-    return None
-
   @classmethod
   def SetCachePath(cls, cachepath):
     with cls.cachepath_lock:
@@ -273,12 +255,13 @@
     """
 
     python_fallback = False
-    if sys.platform.startswith('win') and not self.FindExecutable('7z'):
+    if (sys.platform.startswith('win') and
+        not gclient_utils.FindExecutable('7z')):
       python_fallback = True
     elif sys.platform.startswith('darwin'):
       # The OSX version of unzip doesn't support zip64.
       python_fallback = True
-    elif not self.FindExecutable('unzip'):
+    elif not gclient_utils.FindExecutable('unzip'):
       python_fallback = True
 
     gs_folder = 'gs://%s/%s' % (self.bootstrap_bucket, self.basedir)
diff --git a/git_cl.py b/git_cl.py
index ce36541..26f04fd 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -3183,12 +3183,14 @@
 
 @subcommand.usage('[files or directories to diff]')
 def CMDformat(parser, args):
-  """Runs clang-format on the diff."""
+  """Runs auto-formatting tools (clang-format etc.) on the diff."""
   CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
   parser.add_option('--full', action='store_true',
                     help='Reformat the full content of all touched files')
   parser.add_option('--dry-run', action='store_true',
                     help='Don\'t modify any file on disk.')
+  parser.add_option('--python', action='store_true',
+                    help='Format python code with yapf (experimental).')
   parser.add_option('--diff', action='store_true',
                     help='Print diff to stdout rather than modifying files.')
   opts, args = parser.parse_args(args)
@@ -3216,12 +3218,12 @@
 
   if opts.full:
     # Only list the names of modified files.
-    clang_diff_type = '--name-only'
+    diff_type = '--name-only'
   else:
     # Only generate context-less patches.
-    clang_diff_type = '-U0'
+    diff_type = '-U0'
 
-  diff_cmd = BuildGitDiffCmd(clang_diff_type, upstream_commit, args, CLANG_EXTS)
+  diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
   diff_output = RunGit(diff_cmd)
 
   top_dir = os.path.normpath(
@@ -3267,6 +3269,29 @@
     if opts.dry_run and len(stdout) > 0:
       return_value = 2
 
+  # Similar code to above, but using yapf on .py files rather than clang-format
+  # on C/C++ files
+  if opts.python:
+    diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
+    diff_output = RunGit(diff_cmd)
+    yapf_tool = gclient_utils.FindExecutable('yapf')
+    if yapf_tool is None:
+      DieWithError('yapf not found in PATH')
+
+    if opts.full:
+      files = diff_output.splitlines()
+      if files:
+        cmd = [yapf_tool]
+        if not opts.dry_run and not opts.diff:
+          cmd.append('-i')
+        stdout = RunCommand(cmd + files, cwd=top_dir)
+        if opts.diff:
+          sys.stdout.write(stdout)
+    else:
+      # TODO(sbc): yapf --lines mode still has some issues.
+      # https://github.com/google/yapf/issues/154
+      DieWithError('--python currently only works with --full')
+
   # Build a diff command that only operates on dart files. dart's formatter
   # does not have the nice property of only operating on modified chunks, so
   # hard code full.