[rust] [depot_tools] Minimal `rustfmt` support for `git cl format`.

This CL provides minimal `git cl format` support to enforce correct Rust
formatting in presubmit checks.  For now the files are always fully
formatted - there is no support at this point for formatting only the
changed lines.

Manual tests (after temporarily, artificially introducing a formatting
error to one of .rs files under build/rest/tests):

*) git cl presubmit
   Result: The src directory requires source formatting.
           Please run: git cl format

*) git cl format --dry-run
   Result: Pretty/colorful diff printed out.

*) git cl format
   Result: Temporary formatting errors are fixed.

Bug: chromium:1231317
Change-Id: I114ece90630476f27871ebcd170162caa92c0871
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/3054980
Commit-Queue: Ɓukasz Anforowicz <lukasza@chromium.org>
Reviewed-by: Adrian Taylor <adetaylor@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@google.com>
Reviewed-by: danakj <danakj@chromium.org>
diff --git a/git_cl.py b/git_cl.py
index a87519a..268ff77 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -46,6 +46,7 @@
 import owners_finder
 import presubmit_canned_checks
 import presubmit_support
+import rustfmt
 import scm
 import setup_color
 import split_cl
@@ -5065,6 +5066,34 @@
   return return_value
 
 
+def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
+  """Runs rustfmt.  Just like _RunClangFormatDiff returns 2 to indicate that
+  presubmit checks have failed (and returns 0 otherwise)."""
+
+  if not rust_diff_files:
+    return 0
+
+  # Locate the rustfmt binary.
+  try:
+    rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
+  except rustfmt.NotFoundError as e:
+    DieWithError(e)
+
+  # TODO(crbug.com/1231317): Support formatting only the changed lines
+  # if `opts.full or settings.GetFormatFullByDefault()` is False.  See also:
+  # https://github.com/emilio/rustfmt-format-diff
+  cmd = [rustfmt_tool]
+  if opts.dry_run:
+    cmd.append('--check')
+  cmd += rust_diff_files
+  rustfmt_exitcode = subprocess2.call(cmd)
+
+  if opts.presubmit and rustfmt_exitcode != 0:
+    return 2
+  else:
+    return 0
+
+
 def MatchingFileType(file_name, extensions):
   """Returns True if the file name ends with one of the given extensions."""
   return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
@@ -5076,6 +5105,7 @@
   """Runs auto-formatting tools (clang-format etc.) on the diff."""
   CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
   GN_EXTS = ['.gn', '.gni', '.typemap']
+  RUST_EXTS = ['.rs']
   parser.add_option('--full', action='store_true',
                     help='Reformat the full content of all touched files')
   parser.add_option('--upstream', help='Branch to check against')
@@ -5109,6 +5139,18 @@
                     help='Print diff to stdout rather than modifying files.')
   parser.add_option('--presubmit', action='store_true',
                     help='Used when running the script from a presubmit.')
+
+  parser.add_option('--rust-fmt',
+                    dest='use_rust_fmt',
+                    action='store_true',
+                    default=rustfmt.IsRustfmtSupported(),
+                    help='Enables formatting of Rust file types using rustfmt.')
+  parser.add_option(
+      '--no-rust-fmt',
+      dest='use_rust_fmt',
+      action='store_false',
+      help='Disables formatting of Rust file types using rustfmt.')
+
   opts, args = parser.parse_args(args)
 
   if opts.python is not None and opts.no_python:
@@ -5158,6 +5200,7 @@
         x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
     ]
   python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
+  rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
   gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
 
   top_dir = settings.GetRoot()
@@ -5165,6 +5208,12 @@
   return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
                                      upstream_commit)
 
+  if opts.use_rust_fmt:
+    rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
+                                        upstream_commit)
+    if rust_fmt_return_value == 2:
+      return_value = 2
+
   # Similar code to above, but using yapf on .py files rather than clang-format
   # on C/C++ files
   py_explicitly_disabled = opts.python is not None and not opts.python