Repository: kythe/kythe
Pull request #4851 (2021-03-08 18:34:17)

build: remove the use of arc in favor of pre-commit
https://pre-commit.com/  available via `apt install pre-commit`

By default the checks which run a formatter will only verify that the formatter wouldn't change anything. Running the checks manually via: `pre-commit run --hook-stage manual` will actually run the formatters (and `staticcheck`).  I've also fixed all of the warnings produced via `pre-commit run --all-files` with the exception of the `staticcheck` issues.

Buildbot has been updated, but new images have not been built or pushed yet.

shahms: Note: running `setup_hooks.sh` again will keep the previous script, you may need to run `pre-commit install -t pre-push -f` manually to overwrite the previous script.
shahms: Buildbot managed to push a new, backward-incompatible release today (v3.0.0, so new even the documentation thinks it isn't out yet), which has complicated this a bit.  I *think* I have it fixed, but ugh.

Patch:
diff --git a/.arcconfig b/.arcconfig
deleted file mode 100644
index c67e5d0db..000000000
--- a/.arcconfig
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "phabricator.uri" : "https://phabricator-dot-kythe-repo.appspot.com/",
-  "linter.scriptandregex.script": "tools/arc/linter.sh",
-  "linter.scriptandregex.regex":
-      "/^((?P<name>.+?)::)?(?P<severity>warning|error|advice):(?P<line>\\d+)? (?P<message>.*)$/m",
-  "load": [
-    "tools/arc"
-  ],
-  "unit.engine": "BazelTestEngine"
-}
diff --git a/.arclint b/.arclint
deleted file mode 100644
index 1b667c6d4..000000000
--- a/.arclint
+++ /dev/null
@@ -1,26 +0,0 @@
-{
-  "linters": {
-    "spelling": {
-      "type": "spelling",
-      "exclude": "(^third_party|kythe/java/com/google/devtools/kythe/platform/java/helpers/SignatureGenerator.java)"
-    },
-    "chmod": {
-      "type": "chmod",
-      "exclude": "!^(tools/platforms/configs/.*)$!"
-    },
-    "merge-conflict": {
-      "type": "merge-conflict"
-    },
-    "go": {
-      "type": "golint",
-      "include": "(\\.go$)",
-      "exclude": "(^third_party|^kythe/proto/|/testdata/)"
-    },
-    "custom": {
-      "type": "script-and-regex",
-      "script-and-regex.script": "tools/arc/linter.sh",
-      "script-and-regex.regex": "/^((?P<name>.+?)::)?(?P<severity>warning|error|advice):(?P<line>\\d+)? (?P<message>.*)$/m",
-      "exclude": "!(^|/)(go\\.mod|go\\.sum|\\.[^/]*|third_party/.*|tools/platforms/configs/versions.bzl|kythe/rust/cargo/(remote/)?BUILD[^/]*\\.bazel)$!"
-    }
-  }
-}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..ae7388197
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,88 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+-   repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v3.4.0
+    hooks:
+    -   id: trailing-whitespace
+        args: ['--markdown-linebreak-ext=md,txt']
+    -   id: end-of-file-fixer
+    -   id: check-yaml
+    -   id: check-added-large-files
+    -   id: check-case-conflict
+    -   id: check-merge-conflict
+    -   id: check-executables-have-shebangs
+-   repo: meta
+    hooks:
+    -   id: check-hooks-apply
+    -   id: check-useless-excludes
+-   repo: local
+    hooks:
+    -   id: buildifier
+        name: Format w/ buildifier
+        description: Runs `buildifier` on Bazel files
+        entry: buildifier --mode=fix
+        language: system
+        files: '^(.*/)?(BUILD\.bazel|BUILD)$|\.bzl$|^WORKSPACE$'
+    -   id: copyright
+        name: Check for Kythe Copyright Header
+        language: pygrep
+        entry: 'Copyright 20[12][0-9] The Kythe Authors. All rights reserved.'
+        args: [--negate]
+        files: '^.*$'
+        # Use a verbose Python regex and YAML single-quoted string to make this more legible.
+        exclude: '(?x)
+            ^(AUTHORS|CONTRIBUTORS|WORKSPACE|LICENSE)$
+            |(^|/)BUILD([^/*]*\.bazel)?$
+            |\.(pb\.go|md|yaml|yml|json|html|bzl|lock|patch|BUILD|WORKSPACE|kzip|tex|tac|bazelproject)$
+            |(^|/)(\.[^/]*|go\.mod|go\.sum)$
+            |^tools/|^third_party/|^kythe/web/site/
+            |worker/info/(admin|host)$
+            |/project\.clj$|/style\.css$|bazelrc$|/Doxyfile\.kythe$|/docs/llvm-2014/Makefile$'
+    -   id: shellcheck
+        name: shellcheck
+        description: Runs `shellcheck` on shell files
+        language: system
+        entry: shellcheck
+        args: [-e, SC2064] # Silence the trap argument quotation warnings
+        types: [shell]
+        exclude: 'kythe/extractors/openjdk8/extract\.sh'
+    -   id: google-java-format
+        name: Format w/ google-java-format
+        description: Runs `google-java-format` on Java files
+        language: system
+        entry: google-java-format -i
+        types: [java]
+    -   id: clang-format
+        name: Format w/ clang-format
+        description: Formats files using clang-format
+        language: system
+        entry: clang-format-11 --style=file -i
+        files: '\.(c|cc|h|i|m|js|proto)$'
+    -   id: gofmt
+        name: Format w/ gofmt
+        description: Runs `gofmt` on Go files
+        language: system
+        entry: gofmt -w
+        types: [go]
+    -   id: golint
+        name: golint
+        description: Runs `golint` on Go files
+        language: system
+        entry: golint
+        types: [go]
+        exclude: '^kythe/proto/.*'
+    -   id: staticcheck
+        name: staticcheck
+        description: Runs `staticcheck` on Go files
+        language: script
+        entry: tools/git/run-staticcheck.sh
+        types: [go]
+        stages: [manual]
+    -   id: bazel-test
+        name: Bazel Tests
+        description: Runs affected Bazel tests
+        language: script
+        entry: tools/git/test-affected-targets.sh
+        stages: [push]
+exclude: '(.*/testdata/.*)|(third_party/.*)|(tools/platforms/configs/(versions.bzl|default_toolchain_config_spec_name/.*)$)'
diff --git a/RELEASES.md b/RELEASES.md
index d39cae4aa..d0df6b2eb 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -562,7 +562,7 @@
   *  split semantic callers within xrefs group (#3776) ([87e1e2ed](https://github.com/kythe/kythe/commit/87e1e2ed393dad8abb0865ff7fdc36eab81e6f60), closes [#3768](https://github.com/kythe/kythe/issues/3768))
   *  convert DirectoryReply to FileDirectory for serving data (#3604) ([0ef9b748](https://github.com/kythe/kythe/commit/0ef9b7487088173ba66916eb84f199642eab5949))
   *  make pipeline more tolerant to some errors (#3490) ([dc33424b](https://github.com/kythe/kythe/commit/dc33424b08fda5c6dbdfbc4598851e32cf95833e))
-* **proto:** 
+* **proto:**
   *  include deprecation tags for messages, enums and enum values. (#3822) ([c602555d](https://github.com/kythe/kythe/commit/c602555d75c8cff60722027eb99062b5b07c6153))
   *  link BuildDetails proto (#49) ([e9b68f17](https://github.com/kythe/kythe/commit/e9b68f1708ebeb4c11e0a7ff155ab0a5480d3a35))
   *  proto fields have subkind "field" (#4098) ([ac6cb14b](https://github.com/kythe/kythe/commit/ac6cb14b3a1d7d91abbfcc94a879b8b3ca961f4e))
diff --git a/kythe/docs/.gitignore b/kythe/docs/.gitignore
index 0b84df0f0..2d19fc766 100644
--- a/kythe/docs/.gitignore
+++ b/kythe/docs/.gitignore
@@ -1,1 +1,1 @@
-*.html
\ No newline at end of file
+*.html
diff --git a/kythe/docs/kythe-overview.txt b/kythe/docs/kythe-overview.txt
index 72e64a420..8ac6d952a 100644
--- a/kythe/docs/kythe-overview.txt
+++ b/kythe/docs/kythe-overview.txt
@@ -81,7 +81,7 @@ protocols and data formats* for representing, accessing and querying
 source code information as data, Kythe allows *language analysis and
 indexing to be run as services*.  This, in turn, enables lightweight
 (``thin'') composition of analysis tools with client tools such as
-editors, IDEs, and code browsers. 
+editors, IDEs, and code browsers.
 
 A hub-and-spoke model reduces the overall work to integrate _L_ languages, _C_
 clients, and _B_ build systems from a worst-case of O(L×C×B) -- combinatorial
diff --git a/kythe/docs/kythe-storage.txt b/kythe/docs/kythe-storage.txt
index fcdda0eba..8cfeb6360 100644
--- a/kythe/docs/kythe-storage.txt
+++ b/kythe/docs/kythe-storage.txt
@@ -701,4 +701,3 @@ leveldb::
   An implementation of a graph store using link:http://leveldb.org[LevelDB]
   (via link:http://github.com/jmhodges/levigo[levigo]).
   [link:/repo/kythe/go/storage/leveldb/leveldb.go[source]]
-
diff --git a/kythe/docs/kythe-verifier.txt b/kythe/docs/kythe-verifier.txt
index d0237d513..7e7ab46e1 100644
--- a/kythe/docs/kythe-verifier.txt
+++ b/kythe/docs/kythe-verifier.txt
@@ -146,7 +146,7 @@ as is the case when the next line begins with a magic comment as previously
 described. It is an error if matching `tok` is ambiguous. Substring matching is
 used. `tok` may be a literal string; `@text` and `@"text"` are equivalent.
 
-For example, the following two rules both match tokens from the same line. 
+For example, the following two rules both match tokens from the same line.
 If `@text` and `@more` were swapped in this example, the rules would remain
 valid.
 
diff --git a/kythe/docs/rfc/2909.md b/kythe/docs/rfc/2909.md
index 407842ead..337d0fb6a 100644
--- a/kythe/docs/rfc/2909.md
+++ b/kythe/docs/rfc/2909.md
@@ -194,7 +194,7 @@ message PagedEdgeSet {
     string kind = 1;
     repeated Edge edge = 2;
   }
-  
+
   Node source = 1;
   repeated EdgeGroup group = 2;
 }
@@ -235,4 +235,3 @@ will be necessarily expansive.  The current implementation is intimately tied to
 Transitionally, the columnar format will be added alongside the current `CombinedTable`
 implementation so that either it or the new table format will be accepted by the `http_server` until
 the legacy format is deprecated and removed.
-
diff --git a/kythe/docs/schema/example-base.sh b/kythe/docs/schema/example-base.sh
index 70943d00b..f0d2a3f1f 100644
--- a/kythe/docs/schema/example-base.sh
+++ b/kythe/docs/schema/example-base.sh
@@ -48,7 +48,8 @@ error() {
 }
 export -f error
 
-export TMP="$(mktemp -d 2>/dev/null || mktemp -d -t 'kythetest')"
+export TMP
+TMP="$(mktemp -d 2>/dev/null || mktemp -d -t 'kythetest')"
 trap 'rm -rf "$TMP"' EXIT ERR INT
 
 case "$LANGUAGE" in
diff --git a/kythe/docs/schema/example-cxx.sh b/kythe/docs/schema/example-cxx.sh
index f06cda590..0734b73a8 100755
--- a/kythe/docs/schema/example-cxx.sh
+++ b/kythe/docs/schema/example-cxx.sh
@@ -39,7 +39,7 @@ touch "$ARGS_FILE"
 # main source text, more files can be specified with header lines formatted like
 # "#example filename".  The lines proceeding these header lines will be placed
 # next to test.cc in "$SRCS/filename".
-TEST_MAIN="$SRCS/test.cc"
+export TEST_MAIN="$SRCS/test.cc"
 
 # The raw filter input will be placed into this file for later syntax highlighting
 RAW_EXAMPLE="$TMP/raw.hcc"
@@ -83,6 +83,7 @@ CXX_ARGS="-std=c++1y $(cat "$ARGS_FILE")"
 
 for TEST_CC in "${SRCS}"/*.cc
 do
+  # shellcheck disable=SC2086
   "$CXX_INDEXER_BIN" --ignore_unimplemented=false \
       --experimental_record_dataflow_edges -i "${TEST_CC}" -- $CXX_ARGS \
       >> "${TEST_ENTRIES}"
diff --git a/kythe/docs/schema/example-go.sh b/kythe/docs/schema/example-go.sh
index 545b12ef0..4c374447e 100755
--- a/kythe/docs/schema/example-go.sh
+++ b/kythe/docs/schema/example-go.sh
@@ -60,4 +60,3 @@ fi
 
 source-highlight --failsafe --output=STDOUT --src-lang go -i "$SRCFILE"
 echo "</div>"
-
diff --git a/kythe/docs/schema/example-objc.sh b/kythe/docs/schema/example-objc.sh
index 5aabc74e4..0f118481a 100755
--- a/kythe/docs/schema/example-objc.sh
+++ b/kythe/docs/schema/example-objc.sh
@@ -39,7 +39,7 @@ touch "$ARGS_FILE"
 # main source text, more files can be specified with header lines formatted like
 # "#example filename".  The lines proceeding these header lines will be placed
 # next to test.m in "$SRCS/filename".
-TEST_MAIN="$SRCS/test.m"
+export TEST_MAIN="$SRCS/test.m"
 
 # The raw filter input will be placed into this file for later syntax highlighting
 RAW_EXAMPLE="$TMP/raw.hcc"
@@ -83,6 +83,8 @@ CXX_ARGS="-fblocks $(cat "$ARGS_FILE")"
 
 for TEST_M in "${SRCS}"/*.m
 do
+  # CXX_ARGS is intentionally unquoted
+  # shellcheck disable=SC2086
   "$CXX_INDEXER_BIN" --ignore_unimplemented=false -i "${TEST_M}" -- $CXX_ARGS \
       >> "${TEST_ENTRIES}"
 done
diff --git a/kythe/docs/schema/influences-relation.txt b/kythe/docs/schema/influences-relation.txt
index aedabb5bd..9386344b8 100644
--- a/kythe/docs/schema/influences-relation.txt
+++ b/kythe/docs/schema/influences-relation.txt
@@ -27,7 +27,7 @@ that fit the following examples:
 
 * find places where a flag passed to a program directly affects a different
   program value
-* find places where the return value from a sensitive function is passed to an 
+* find places where the return value from a sensitive function is passed to an
   insensitive sink, like leaking a key to a logging function
 * identify the places that affect a program value that was assigned something
   unexpected at runtime
diff --git a/kythe/docs/schema/schema.txt b/kythe/docs/schema/schema.txt
index 843b9d2c6..f6cd60852 100644
--- a/kythe/docs/schema/schema.txt
+++ b/kythe/docs/schema/schema.txt
@@ -68,8 +68,8 @@ Edge kinds
 ----------
 .Reverse Edges
 TIP: The Kythe API uses `%` to denote a reverse edge.
-For example, if NodeA `defines/binding` NodeB, then NodeB 
-`%/kythe/edge/defines/binding` NodeA. Reverse edges are constructed 
+For example, if NodeA `defines/binding` NodeB, then NodeB
+`%/kythe/edge/defines/binding` NodeA. Reverse edges are constructed
 during post-processing, and should not be emitted by indexers.
 
 [[aliases]]
diff --git a/kythe/examples/proto/example.proto b/kythe/examples/proto/example.proto
index 2d6d00eda..10f904fcc 100644
--- a/kythe/examples/proto/example.proto
+++ b/kythe/examples/proto/example.proto
@@ -20,5 +20,4 @@ package kythe.examples.proto.example;
 
 // A single proto message.
 //- @Foo defines/binding MessageFoo?
-message Foo {
-}
+message Foo {}
diff --git a/kythe/extractors/bazel/extract.sh b/kythe/extractors/bazel/extract.sh
index 721001af0..2f663cce9 100755
--- a/kythe/extractors/bazel/extract.sh
+++ b/kythe/extractors/bazel/extract.sh
@@ -51,6 +51,7 @@ set -ex
 
 if [[ -n "$KYTHE_SYSTEM_DEPS" ]]; then
   echo "Installing $KYTHE_SYSTEM_DEPS"
+  # shellcheck disable=SC2086
   # TODO(jaysachs): unclear if we should bail if any packages fail to install
   apt-get update && \
   apt-get upgrade -y && \
@@ -71,7 +72,7 @@ if [[ -n "$KYTHE_BAZEL_TARGET" ]]; then
   # needs to see two valid target expressions (//foo/... and -//foo/test/...)
   # not one invalid target expression with white space
   # ("//foo/... -//foo/test/...").
-  /kythe/bazel_wrapper.sh --bazelrc=$KYTHE_RELEASE/extractors.bazelrc "$@" --override_repository kythe_release=$KYTHE_RELEASE -- $KYTHE_BAZEL_TARGET
+  /kythe/bazel_wrapper.sh --bazelrc="$KYTHE_RELEASE/extractors.bazelrc" "$@" --override_repository "kythe_release=$KYTHE_RELEASE" -- "$KYTHE_BAZEL_TARGET"
 else
   # If the user supplied a bazel query, execute it and run bazel, but we have to
   # shard the results to different bazel runs because the bazel command line
@@ -84,8 +85,8 @@ fi
 
 # Collect any extracted compilations.
 mkdir -p "$KYTHE_OUTPUT_DIRECTORY"
-find bazel-out/*/extra_actions/external/kythe_release -name '*.kzip' | \
-  xargs -r /kythe/tools/kzip merge --append --encoding "$KYTHE_KZIP_ENCODING" --output "$KYTHE_OUTPUT_DIRECTORY/compilations.kzip"
+find bazel-out/*/extra_actions/external/kythe_release -name '*.kzip' -print0 | \
+  xargs --null -r /kythe/tools/kzip merge --append --encoding "$KYTHE_KZIP_ENCODING" --output "$KYTHE_OUTPUT_DIRECTORY/compilations.kzip"
 
 # Record the timestamp of the git commit in a metadata kzip.
 /kythe/tools/kzip create_metadata \
diff --git a/kythe/extractors/gcp/examples/mvn.yaml b/kythe/extractors/gcp/examples/mvn.yaml
index c74ee5688..b5e782088 100644
--- a/kythe/extractors/gcp/examples/mvn.yaml
+++ b/kythe/extractors/gcp/examples/mvn.yaml
@@ -57,4 +57,3 @@ artifacts:
 tags:
   - '${_CORPUS}'
   - 'kythe_extract_maven'
-
diff --git a/kythe/extractors/openjdk8/run.sh b/kythe/extractors/openjdk8/run.sh
index db6114c74..d4269018c 100755
--- a/kythe/extractors/openjdk8/run.sh
+++ b/kythe/extractors/openjdk8/run.sh
@@ -21,7 +21,7 @@
 
 usage() {
   echo "usage: $(basename "$0") output-path" >&2
-  exit $1
+  exit "$1"
 }
 
 OUTPUT="$1"
@@ -35,4 +35,4 @@ OUTPUT="$(readlink -e "$OUTPUT")"
 cd "$(dirname "$0")"/../../..
 bazel build //kythe/extractors/openjdk
 
-docker run -ti --rm -v $OUTPUT:/idx openjdk-extractor
+docker run -ti --rm -v "$OUTPUT:/idx openjdk-extractor"
diff --git a/kythe/go/extractors/cmd/gotool/analyze_packages.sh b/kythe/go/extractors/cmd/gotool/analyze_packages.sh
index 664e28745..a11be3f1c 100755
--- a/kythe/go/extractors/cmd/gotool/analyze_packages.sh
+++ b/kythe/go/extractors/cmd/gotool/analyze_packages.sh
@@ -34,7 +34,7 @@ done
 
 # golang build tags can optionally be specified with the `KYTHE_GO_BUILD_TAGS`
 # env variable.
-if [ ! -z "$KYTHE_GO_BUILD_TAGS" ]; then
+if [ -n "$KYTHE_GO_BUILD_TAGS" ]; then
   FLAGS+=( "--buildtags=$KYTHE_GO_BUILD_TAGS" )
 fi
 
@@ -61,7 +61,7 @@ fi
 
 # cd into the top-level git directory of our package and query git for the
 # commit timestamp.
-pushd "$(go env GOPATH)/src/$(dirname ${PACKAGES[0]})"
+pushd "$(go env GOPATH)/src/$(dirname "${PACKAGES[0]}")"
 TIMESTAMP="$(git log --pretty='%ad' -n 1 HEAD)"
 popd
 
diff --git a/kythe/go/extractors/config/preprocessor/Dockerfile b/kythe/go/extractors/config/preprocessor/Dockerfile
index 2381e7602..ffc02eb05 100644
--- a/kythe/go/extractors/config/preprocessor/Dockerfile
+++ b/kythe/go/extractors/config/preprocessor/Dockerfile
@@ -1,3 +1,17 @@
+# Copyright 2021 The Kythe Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 FROM scratch
 ADD kythe/go/extractors/config/preprocessor/preprocessor preprocessor
 ENTRYPOINT ["/preprocessor"]
diff --git a/kythe/go/util/schema/update_data.sh b/kythe/go/util/schema/update_data.sh
index ec30ae452..87cbbdbef 100755
--- a/kythe/go/util/schema/update_data.sh
+++ b/kythe/go/util/schema/update_data.sh
@@ -1,4 +1,17 @@
 #!/bin/sh
+# Copyright 2021 The Kythe Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
 set -e
 readonly output=kythe/go/util/schema/indexdata.go
diff --git a/kythe/java/com/google/devtools/kythe/analyzers/indexer.bazelproject b/kythe/java/com/google/devtools/kythe/analyzers/indexer.bazelproject
index bc451d04c..b0f3c7d0a 100644
--- a/kythe/java/com/google/devtools/kythe/analyzers/indexer.bazelproject
+++ b/kythe/java/com/google/devtools/kythe/analyzers/indexer.bazelproject
@@ -32,4 +32,3 @@ targets:
   //kythe/javatests/com/google/devtools/kythe/analyzers/java:all
   //kythe/javatests/com/google/devtools/kythe/analyzers/jvm/...:all
   //kythe/java/com/google/devtools/kythe/extractors/shared
-
diff --git a/kythe/java/com/google/devtools/kythe/extractors/java/standalone/javac-wrapper.sh b/kythe/java/com/google/devtools/kythe/extractors/java/standalone/javac-wrapper.sh
index 839fdfa1c..8dcca4ce4 100755
--- a/kythe/java/com/google/devtools/kythe/extractors/java/standalone/javac-wrapper.sh
+++ b/kythe/java/com/google/devtools/kythe/extractors/java/standalone/javac-wrapper.sh
@@ -40,7 +40,7 @@
 #   KYTHE_EXTRACT_ONLY: if set, suppress the call to javac after extraction
 
 if [[ -z "$JAVA_HOME" ]]; then
-  readonly JAVABIN="$(which java)"
+  readonly JAVABIN="$(command -v java)"
 else
   readonly JAVABIN="$JAVA_HOME/bin/java"
 fi
@@ -55,7 +55,9 @@ fix_permissions() {
   local dir="${1:?missing path}"
   # We cannot use stat -c to get user and group because it varies too much from
   # system to system.
-  local ug=$(ls -ld "$dir" | awk '{print $3":"$4}')
+  local ug
+  # shellcheck disable=SC2012
+  ug="$(ls -ld "$dir" | awk '{print $3":"$4}')"
   chown -R "$ug" "$dir"
 }
 cleanup() {
diff --git a/kythe/java/com/google/devtools/kythe/platform/shared/ProtobufMetadataLoader.java b/kythe/java/com/google/devtools/kythe/platform/shared/ProtobufMetadataLoader.java
index 8d236ae85..c8bce081e 100644
--- a/kythe/java/com/google/devtools/kythe/platform/shared/ProtobufMetadataLoader.java
+++ b/kythe/java/com/google/devtools/kythe/platform/shared/ProtobufMetadataLoader.java
@@ -138,8 +138,7 @@ public class ProtobufMetadataLoader implements MetadataLoader {
       }
       fileVNames.add(rule.vname);
       rule.vname =
-          rule.vname
-              .toBuilder()
+          rule.vname.toBuilder()
               .setSignature(protoPath.toString())
               .setLanguage(PROTOBUF_LANGUAGE)
               .build();
diff --git a/kythe/ocaml/js_indexer/dumpKytheCommand.ml b/kythe/ocaml/js_indexer/dumpKytheCommand.ml
index 1d91159db..f8d5ddb55 100644
--- a/kythe/ocaml/js_indexer/dumpKytheCommand.ml
+++ b/kythe/ocaml/js_indexer/dumpKytheCommand.ml
@@ -93,7 +93,7 @@ let main option_values path filename () =
 
   match (Marshal.from_channel ic : DKS.resp_t) with
   | (Some err, None) -> handle_error err
-  | (None, Some jsons) -> List.iter 
+  | (None, Some jsons) -> List.iter
                             (fun j -> print_endline (json_to_string j)) jsons
   | (_, _) -> assert false
 
diff --git a/kythe/ocaml/js_indexer/flow.patch b/kythe/ocaml/js_indexer/flow.patch
index 5ef5e3cea..418194915 100644
--- a/kythe/ocaml/js_indexer/flow.patch
+++ b/kythe/ocaml/js_indexer/flow.patch
@@ -32,7 +32,7 @@ index 1645b4e..454f440 100644
      DumpTypesCommand.command;
 +    DumpKytheCommand.command;
    ]
- 
+
    (* status commands, which need a list of other commands *)
 diff --git a/third_party/flow/src/server/dumpKytheService_js.ml b/third_party/flow/src/server/dumpKytheService_js.ml
 new file mode 120000
@@ -49,7 +49,7 @@ index ccb2e08..a01582e 100644
 @@ -193,6 +193,11 @@ struct
      Marshal.to_channel oc (err, resp) [];
      flush oc
- 
+
 +  let dump_kythe file_input oc =
 +    let resp : DumpKytheService_js.resp_t = DumpKytheService_js.query file_input in
 +    Marshal.to_channel oc resp [];
diff --git a/kythe/proto/explore.proto b/kythe/proto/explore.proto
index bb7d27594..412fc60eb 100644
--- a/kythe/proto/explore.proto
+++ b/kythe/proto/explore.proto
@@ -51,35 +51,29 @@ service ExploreService {
   // The Callers/Callees functions are distinct from XrefService.CrossReferences
   // in that these functions capture the semantic relationships between methods,
   // rather than the locations in the code base where a method is called.
-  rpc Callers(CallersRequest) returns (CallersReply) {
-  }
+  rpc Callers(CallersRequest) returns (CallersReply) {}
 
   // Returns the (recursive) callees of a specified function (that is, what
   // functions this function calls), as a directed graph.
-  rpc Callees(CalleesRequest) returns (CalleesReply) {
-  }
+  rpc Callees(CalleesRequest) returns (CalleesReply) {}
 
   // Returns the parents of a specified node (for example,
   // the file for a class, or the class for a function).
   // Note that in some cases a node may have more than one parent.
-  rpc Parents(ParentsRequest) returns (ParentsReply) {
-  }
+  rpc Parents(ParentsRequest) returns (ParentsReply) {}
 
   // Returns the children of a specified node (for example,
   // the classes contained in a file, or the functions contained in a class).
-  rpc Children(ChildrenRequest) returns (ChildrenReply) {
-  }
+  rpc Children(ChildrenRequest) returns (ChildrenReply) {}
 
   // Returns the hierarchy (supertypes and subtypes, including implementations)
   // of a specified type, as a directed acyclic graph.
   // NOT YET IMPLEMENTED
-  rpc TypeHierarchy(TypeHierarchyRequest) returns (TypeHierarchyReply) {
-  }
+  rpc TypeHierarchy(TypeHierarchyRequest) returns (TypeHierarchyReply) {}
 
   // Returns the parameters of a specified function.
   // NOT YET IMPLEMENTED
-  rpc Parameters(ParametersRequest) returns (ParametersReply) {
-  }
+  rpc Parameters(ParametersRequest) returns (ParametersReply) {}
 }
 
 // Shared/utility messages
diff --git a/kythe/proto/filetree.proto b/kythe/proto/filetree.proto
index 1d671a849..d4c71b389 100644
--- a/kythe/proto/filetree.proto
+++ b/kythe/proto/filetree.proto
@@ -24,16 +24,13 @@ option java_package = "com.google.devtools.kythe.proto";
 // FileTreeService provides an interface to explore a tree of files.
 service FileTreeService {
   // CorpusRoots returns all known corpus/root pairs for stored files.
-  rpc CorpusRoots(CorpusRootsRequest) returns (CorpusRootsReply) {
-  }
+  rpc CorpusRoots(CorpusRootsRequest) returns (CorpusRootsReply) {}
 
   // Directory returns the file/sub-directory contents of the given directory.
-  rpc Directory(DirectoryRequest) returns (DirectoryReply) {
-  }
+  rpc Directory(DirectoryRequest) returns (DirectoryReply) {}
 }
 
-message CorpusRootsRequest {
-}
+message CorpusRootsRequest {}
 
 message CorpusRootsReply {
   message Corpus {
diff --git a/kythe/proto/graph.proto b/kythe/proto/graph.proto
index e31059531..1aaf499f4 100644
--- a/kythe/proto/graph.proto
+++ b/kythe/proto/graph.proto
@@ -35,13 +35,11 @@ option go_package = "graph_go_proto";
 // without any facts or edges is not considered to exist.
 service GraphService {
   // Nodes returns a subset of the facts for each of the requested nodes.
-  rpc Nodes(NodesRequest) returns (NodesReply) {
-  }
+  rpc Nodes(NodesRequest) returns (NodesReply) {}
 
   // Edges returns a subset of the outbound edges for each of a set of
   // requested nodes.
-  rpc Edges(EdgesRequest) returns (EdgesReply) {
-  }
+  rpc Edges(EdgesRequest) returns (EdgesReply) {}
 }
 
 message NodesRequest {
diff --git a/kythe/proto/link.proto b/kythe/proto/link.proto
index 251144c6b..58811d563 100644
--- a/kythe/proto/link.proto
+++ b/kythe/proto/link.proto
@@ -40,8 +40,7 @@ service LinkService {
   // Resolve returns the links matching the specified request.  The server is
   // permitted to limit the size of the result set in order to ensure fast
   // responses. The server will report an error if its limits are exceeded.
-  rpc Resolve(LinkRequest) returns (LinkReply) {
-  }
+  rpc Resolve(LinkRequest) returns (LinkReply) {}
 }
 
 message LinkRequest {
diff --git a/kythe/proto/storage.proto b/kythe/proto/storage.proto
index eccbfcdbd..41819658a 100644
--- a/kythe/proto/storage.proto
+++ b/kythe/proto/storage.proto
@@ -149,8 +149,7 @@ message WriteRequest {
 }
 
 // Response to a WriteRequest
-message WriteReply {
-}
+message WriteReply {}
 
 // Request for a stream of Entry objects resulting from a full scan of a
 // GraphStore.
diff --git a/kythe/proto/storage_service.proto b/kythe/proto/storage_service.proto
index bfa10b065..fe659164c 100644
--- a/kythe/proto/storage_service.proto
+++ b/kythe/proto/storage_service.proto
@@ -29,15 +29,13 @@ service GraphStore {
   // Read responds with all Entry messages that match the given ReadRequest.
   // The Read operation should be implemented with time complexity proportional
   // to the size of the return set.
-  rpc Read(ReadRequest) returns (stream Entry) {
-  }
+  rpc Read(ReadRequest) returns (stream Entry) {}
 
   // Scan responds with all Entry messages matching the given ScanRequest.  If a
   // ScanRequest field is empty, any entry value for that field matches and will
   // be returned.  Scan is similar to Read, but with no time complexity
   // restrictions.
-  rpc Scan(ScanRequest) returns (stream Entry) {
-  }
+  rpc Scan(ScanRequest) returns (stream Entry) {}
 
   // Write atomically inserts or updates a collection of entries into the store.
   // Each update is a tuple of the form (kind, target, fact, value).  For each
@@ -46,8 +44,7 @@ service GraphStore {
   // value') that may exist.  Note that this operation cannot delete any data
   // from the store; entries are only ever inserted or updated.  Apart from
   // acting atomically, no other constraints are placed on the implementation.
-  rpc Write(WriteRequest) returns (WriteReply) {
-  }
+  rpc Write(WriteRequest) returns (WriteReply) {}
 }
 
 // ShardedGraphStores can be arbitrarily sharded for parallel processing.
@@ -55,10 +52,8 @@ service GraphStore {
 // results when the store is being written to.  Shards are indexed from 0.
 service ShardedGraphStore {
   // Count returns the number of entries in the given shard.
-  rpc Count(CountRequest) returns (CountReply) {
-  }
+  rpc Count(CountRequest) returns (CountReply) {}
 
   // Shard responds with each Entry in the given shard.
-  rpc Shard(ShardRequest) returns (stream Entry) {
-  }
+  rpc Shard(ShardRequest) returns (stream Entry) {}
 }
diff --git a/kythe/proto/xref.proto b/kythe/proto/xref.proto
index 5487b43d0..478dac236 100644
--- a/kythe/proto/xref.proto
+++ b/kythe/proto/xref.proto
@@ -46,20 +46,17 @@ import "kythe/proto/common.proto";
 service XRefService {
   // Decorations returns an index of the nodes and edges associated with a
   // particular file node.
-  rpc Decorations(DecorationsRequest) returns (DecorationsReply) {
-  }
+  rpc Decorations(DecorationsRequest) returns (DecorationsReply) {}
 
   // CrossReferences returns the global references, definitions, declarations,
   // callers, and related nodes of a set of requested nodes.
-  rpc CrossReferences(CrossReferencesRequest) returns (CrossReferencesReply) {
-  }
+  rpc CrossReferences(CrossReferencesRequest) returns (CrossReferencesReply) {}
 
   // Documentation takes a set of tickets for semantic objects and returns
   // documentation about them, including generated signatures and
   // user-provided text. The documentation may refer to tickets for other
   // nodes in the graph.
-  rpc Documentation(DocumentationRequest) returns (DocumentationReply) {
-  }
+  rpc Documentation(DocumentationRequest) returns (DocumentationReply) {}
 }
 
 // A Location represents a single span of zero or more contiguous bytes of a
diff --git a/kythe/release/appengine/buildbot/Dockerfile b/kythe/release/appengine/buildbot/Dockerfile
index 084fcc396..484921ef9 100644
--- a/kythe/release/appengine/buildbot/Dockerfile
+++ b/kythe/release/appengine/buildbot/Dockerfile
@@ -12,40 +12,36 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-FROM debian:buster
+FROM ubuntu:focal
 
-ADD buster-backports.list /etc/apt/sources.list.d/
 RUN apt-get update && \
     apt-get upgrade -y && \
-    DEBIAN_FRONTEND=noninteractive apt-get install -t buster-backports -y \
-      ca-certificates gnupg \
-      # Buildbot dependencies
-      python3 python3-dev python3-pip wget git \
-      # Linter dependencies
-      arcanist shellcheck \
+    DEBIAN_FRONTEND=noninteractive apt-get install -y \
+      # Buildbot and pre-commit dependencies
+      python python3 python3-dev python3-pip wget git \
       # Required by go tool build of Kythe
       golang-go libleveldb-dev libbrotli-dev \
       # Bazel's fallback @local_jdk
       openjdk-11-jdk \
       # Bazel dependencies
-      pkg-config zip g++ zlib1g-dev unzip \
+      zip zlib1g-dev unzip \
       # Kythe C++ dependencies
-      gcc uuid-dev flex bison \
+      clang-11 uuid-dev flex bison \
+      # Linter dependencies
+      shellcheck clang-format-11 \
       # Kythe misc dependencies
       asciidoc source-highlight graphviz curl parallel jq && \
     apt-get clean
 
-# Install recent clang tools.
-ADD llvm-toolchain.list /etc/apt/sources.list.d/
-RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key| apt-key add - && \
-    apt-get update && apt-get install -y clang-11 clang-format-11 && apt-get clean && \
-    update-alternatives \
+# Make clang-11 the default
+RUN update-alternatives \
       --install /usr/bin/clang        clang        /usr/bin/clang-11    100 \
       --slave   /usr/bin/clang++      clang++      /usr/bin/clang++-11      \
       --slave   /usr/bin/clang-format clang-format /usr/bin/clang-format-11
 
-# Install Buildbot
+# Install pip-packages
 RUN pip3 install --upgrade pip
+RUN pip3 install pre-commit
 RUN pip3 install buildbot
 RUN pip3 install buildbot-www buildbot-console-view buildbot-grid-view buildbot-waterfall-view psycopg2-binary txrequests
 RUN pip3 install --upgrade six service_identity pyasn1 cryptography pyopenssl
@@ -56,9 +52,9 @@ RUN go get github.com/bazelbuild/buildtools/buildifier
 RUN go get golang.org/x/lint/golint
 RUN go get honnef.co/go/tools/cmd/staticcheck
 ENV PATH=$PATH:/root/go/bin
-RUN wget https://github.com/google/google-java-format/releases/download/google-java-format-1.7/google-java-format-1.7-all-deps.jar && \
-   mv google-java-format-1.7-all-deps.jar /usr/bin/google-java-format.jar && \
-   echo 'exec java -jar /usr/bin/google-java-format.jar "$@"' >/usr/bin/google-java-format && \
+RUN wget https://github.com/google/google-java-format/releases/download/google-java-format-1.9/google-java-format-1.9-all-deps.jar && \
+   mv google-java-format-1.9-all-deps.jar /usr/bin/google-java-format.jar && \
+   /bin/echo -e '#!/bin/sh\nexec java -jar /usr/bin/google-java-format.jar "$@"' >/usr/bin/google-java-format && \
    chmod +x /usr/bin/google-java-format
 
 # Install Bazelisk and wrapper script
@@ -73,6 +69,7 @@ ADD bazelrc /root/.bazelrc
 ADD start.sh /buildbot/
 ADD worker /buildbot/worker
 ADD master /buildbot/master
+
 ADD secrets.tar /buildbot
 RUN buildbot checkconfig /buildbot/master/master.cfg
 
diff --git a/kythe/release/appengine/buildbot/README b/kythe/release/appengine/buildbot/README.md
similarity index 100%
rename from kythe/release/appengine/buildbot/README
rename to kythe/release/appengine/buildbot/README.md
diff --git a/kythe/release/appengine/buildbot/master/master.cfg b/kythe/release/appengine/buildbot/master/master.cfg
index 477d4916c..85730d179 100644
--- a/kythe/release/appengine/buildbot/master/master.cfg
+++ b/kythe/release/appengine/buildbot/master/master.cfg
@@ -19,6 +19,7 @@ from os import path, listdir
 import json
 import urllib.request
 
+from buildbot.reporters.generators.build import BuildStartEndStatusGenerator
 from buildbot.changes.github import GitHubPullrequestPoller
 from buildbot.plugins import *
 from twisted.internet import defer
@@ -28,9 +29,9 @@ c['buildbotNetUsageData'] = None
 
 # https://github.com/buildbot/buildbot/issues/3627
 # db_url and a few other places don't support Interpolate, so do it manually.
-secrets_dir = path.join(path.dirname(__file__), "../secrets")
+secrets_dir = path.join(path.dirname(__file__), '../secrets')
 secrets_dict = {
-    p: open(path.join(secrets_dir, p)).read().rstrip("\n")
+    p: open(path.join(secrets_dir, p)).read().rstrip('\n')
     for p in listdir(secrets_dir)
 }
 
@@ -42,7 +43,7 @@ c['secretsProviders'] = [secrets.SecretInAFile(dirname=secrets_dir)]
 # a Worker object, specifying a unique worker name and password.  The same
 # worker name and password must be configured on the worker.
 c['workers'] = [
-    worker.Worker("local-worker", secrets_dict["worker_password"]),
+    worker.Worker('local-worker', secrets_dict['worker_password']),
 ]
 
 # 'protocols' contains information about protocols which master will use for
@@ -56,65 +57,66 @@ c['protocols'] = {'pb': {'port': 9989}}
 
 
 def pullRequestFilter(pr):
-    # Exclude anything that explicitly says 'nobuildbot'
-    for label in pr['labels']:
-        if label['name'] == 'nobuildbot':
-            return False
-
-    # Allow all Kythe maintainer Pull Requests
-    url = 'https://api.github.com/teams/2988296/memberships/' + pr['user'][
-        'login']
-    req = urllib.request.Request(
-        url,
-        headers={
-            'User-Agent': 'Buildbot',
-            'Authorization': 'token {github_token}'.format(**secrets_dict)
-        })
-    try:
-        with urllib.request.urlopen(req) as f:
-            userMembership = json.loads(f.read().decode('utf-8'))
-            if userMembership['state'] == 'active' and userMembership[
-                    'role'] == 'maintainer':
-                return True
-    except:
-        # User is not a member of the Kythe team
-        pass
-
-    # Allow explicitly labeled PRs by non-maintainers if they signed the CLA
-    signed_cla = False
-    requested = False
-    for label in pr['labels']:
-        if label['name'] == 'cla: yes':
-            signed_cla = True
-        elif label['name'] == 'buildbot':
-            requested = True
-    return signed_cla and requested
+  # Exclude anything that explicitly says 'nobuildbot'
+  for label in pr['labels']:
+    if label['name'] == 'nobuildbot':
+      return False
+
+  # Allow all Kythe maintainer Pull Requests
+  url = 'https://api.github.com/teams/2988296/memberships/' + pr['user']['login']
+  req = urllib.request.Request(
+      url,
+      headers={
+          'User-Agent': 'Buildbot',
+          'Authorization': 'token {github_token}'.format(**secrets_dict)
+      })
+  try:
+    with urllib.request.urlopen(req) as f:
+      userMembership = json.loads(f.read().decode('utf-8'))
+      if userMembership['state'] == 'active' and userMembership[
+          'role'] == 'maintainer':
+        return True
+  except:
+    # User is not a member of the Kythe team
+    pass
+
+  # Allow explicitly labeled PRs by non-maintainers if they signed the CLA
+  signed_cla = False
+  requested = False
+  for label in pr['labels']:
+    if label['name'] == 'cla: yes':
+      signed_cla = True
+    elif label['name'] == 'buildbot':
+      requested = True
+  return signed_cla and requested
 
 
 c['change_source'] = []
 
 
 def add_github_change_sources(repo, owner='kythe'):
-    # Track master
-    c['change_source'].append(
-        changes.GitPoller('git://github.com/%s/%s.git' % (owner, repo),
-                          project='%s/%s' % (owner, repo),
-                          workdir='%s-workdir' % repo,
-                          branches=['master'],
-                          category='master',
-                          pollAtLaunch=True,
-                          pollInterval=300))
-    # Track pull requests
-    c['change_source'].append(
-        GitHubPullrequestPoller(owner=owner,
-                                repo=repo,
-                                repository_type='git',
-                                pullrequest_filter=pullRequestFilter,
-                                category='pull',
-                                magic_link=True,
-                                pollAtLaunch=True,
-                                pollInterval=60,
-                                token=secrets_dict['github_token']))
+  # Track master
+  c['change_source'].append(
+      changes.GitPoller(
+          'git://github.com/%s/%s.git' % (owner, repo),
+          project='%s/%s' % (owner, repo),
+          workdir='%s-workdir' % repo,
+          branches=['master'],
+          category='master',
+          pollAtLaunch=True,
+          pollInterval=300))
+  # Track pull requests
+  c['change_source'].append(
+      GitHubPullrequestPoller(
+          owner=owner,
+          repo=repo,
+          repository_type='git',
+          pullrequest_filter=pullRequestFilter,
+          category='pull',
+          magic_link=True,
+          pollAtLaunch=True,
+          pollInterval=60,
+          token=secrets_dict['github_token']))
 
 
 add_github_change_sources('kythe')
@@ -137,166 +139,192 @@ releaseBuilders = [
 ]
 
 linters = [
-    'arc-lint',
+    'pre-commit',
 ]
 
 allBuilders = bazelBuilders + goBuilders + linters + releaseBuilders
 
 c['schedulers'] = []
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="lint-pull-requests",
-                                     change_filter=util.ChangeFilter(
-                                         category='pull',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=linters))
+    schedulers.SingleBranchScheduler(
+        name='lint-pull-requests',
+        change_filter=util.ChangeFilter(
+            category='pull',
+            project=['kythe/kythe'],
+        ),
+        builderNames=linters))
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="bazel-master",
-                                     change_filter=util.ChangeFilter(
-                                         category='master',
-                                         branch='master',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=bazelBuilders))
+    schedulers.SingleBranchScheduler(
+        name='bazel-master',
+        change_filter=util.ChangeFilter(
+            category='master',
+            branch='master',
+            project=['kythe/kythe'],
+        ),
+        builderNames=bazelBuilders))
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="bazel-pull-requests",
-                                     change_filter=util.ChangeFilter(
-                                         category='pull',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=bazelBuilders))
+    schedulers.SingleBranchScheduler(
+        name='bazel-pull-requests',
+        change_filter=util.ChangeFilter(
+            category='pull',
+            project=['kythe/kythe'],
+        ),
+        builderNames=bazelBuilders))
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="go-master",
-                                     change_filter=util.ChangeFilter(
-                                         category='master',
-                                         branch='master',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=goBuilders))
+    schedulers.SingleBranchScheduler(
+        name='go-master',
+        change_filter=util.ChangeFilter(
+            category='master',
+            branch='master',
+            project=['kythe/kythe'],
+        ),
+        builderNames=goBuilders))
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="go-pull-requests",
-                                     change_filter=util.ChangeFilter(
-                                         category='pull',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=goBuilders))
+    schedulers.SingleBranchScheduler(
+        name='go-pull-requests',
+        change_filter=util.ChangeFilter(
+            category='pull',
+            project=['kythe/kythe'],
+        ),
+        builderNames=goBuilders))
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="release-master",
-                                     change_filter=util.ChangeFilter(
-                                         category='master',
-                                         branch='master',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=releaseBuilders))
+    schedulers.SingleBranchScheduler(
+        name='release-master',
+        change_filter=util.ChangeFilter(
+            category='master',
+            branch='master',
+            project=['kythe/kythe'],
+        ),
+        builderNames=releaseBuilders))
 c['schedulers'].append(
-    schedulers.SingleBranchScheduler(name="release-pull-requests",
-                                     change_filter=util.ChangeFilter(
-                                         category='pull',
-                                         project=['kythe/kythe'],
-                                     ),
-                                     builderNames=releaseBuilders))
+    schedulers.SingleBranchScheduler(
+        name='release-pull-requests',
+        change_filter=util.ChangeFilter(
+            category='pull',
+            project=['kythe/kythe'],
+        ),
+        builderNames=releaseBuilders))
 
 # Allow builders to be "forced"
 c['schedulers'].append(
-    schedulers.ForceScheduler(name="force", builderNames=allBuilders))
+    schedulers.ForceScheduler(name='force', builderNames=allBuilders))
 
 ####### BUILDERS
 
-arcLintSteps = util.BuildFactory()
-arcLintSteps.addStep(
-    steps.GitHub(repourl=util.Property('repository',
-                                       'git://github.com/kythe/kythe.git'),
-                 mode='full',
-                 method='copy'))
-arcLintSteps.addStep(
-    steps.ShellCommand(command=[
-        "arc", "lint", "--never-apply-patches", "--rev", "origin/master"
+preCommitSteps = util.BuildFactory()
+preCommitSteps.addStep(
+    steps.GitHub(
+        repourl=util.Property('repository', 'git://github.com/kythe/kythe.git'),
+        mode='full',
+        method='copy'))
+preCommitSteps.addStep(
+    steps.ShellSequence(commands=[
+        util.ShellArg(command=[
+            'pre-commit',
+            'run',
+            '--hook-stage',
+            'commit',
+            '--from-ref',
+            'origin/master',
+            '--to-ref',
+            'HEAD',
+        ]),
+        util.ShellArg(command=[
+            # If any of the formatters modified local files, output the diff.
+            'git',
+            'diff',
+        ])
     ]))
 
 bazelKytheSteps = util.BuildFactory()
 bazelKytheSteps.addStep(
-    steps.GitHub(repourl=util.Property('repository',
-                                       'git://github.com/kythe/kythe.git'),
-                 mode='full',
-                 method='copy'))
+    steps.GitHub(
+        repourl=util.Property('repository', 'git://github.com/kythe/kythe.git'),
+        mode='full',
+        method='copy'))
 bazelBinEnv = {'USE_BAZEL_VERSION': util.Property('bazel_version')}
 bazelKytheSteps.addStep(
-    steps.ShellCommand(command=["bazel", "version"], env=bazelBinEnv))
+    steps.ShellCommand(command=['bazel', 'version'], env=bazelBinEnv))
 bazelKytheSteps.addStep(
-    steps.ShellCommand(command=[
-        "bazel", "test",
-        util.Property('bazel_flags', default=[]),
-        util.Property('bazel_targets', default=['//...'])
-    ],
-                       env=bazelBinEnv))
+    steps.ShellCommand(
+        command=[
+            'bazel', 'test',
+            util.Property('bazel_flags', default=[]),
+            util.Property('bazel_targets', default=['//...'])
+        ],
+        env=bazelBinEnv))
 
 goKytheSteps = util.BuildFactory()
 goKytheSteps.addStep(
-    steps.GitHub(repourl=util.Property('repository',
-                                       'git://github.com/kythe/kythe.git'),
-                 workdir='src/kythe.io',
-                 mode='full',
-                 method='copy'))
-goKytheSteps.addStep(steps.ShellCommand(command=["go", "version"]))
+    steps.GitHub(
+        repourl=util.Property('repository', 'git://github.com/kythe/kythe.git'),
+        workdir='src/kythe.io',
+        mode='full',
+        method='copy'))
+goKytheSteps.addStep(steps.ShellCommand(command=['go', 'version']))
 goKytheSteps.addStep(
-    steps.ShellCommand(command=["go", "get", "-t", "kythe.io/kythe/go/..."],
-                       env={
-                           "GOPATH": util.Property('builddir'),
-                           "GO111MODULE": "off"
-                       },
-                       haltOnFailure=True))
+    steps.ShellCommand(
+        command=['go', 'get', '-t', 'kythe.io/kythe/go/...'],
+        env={
+            'GOPATH': util.Property('builddir'),
+            'GO111MODULE': 'off'
+        },
+        haltOnFailure=True))
 goKytheSteps.addStep(
-    steps.ShellCommand(command=["go", "test", "kythe.io/kythe/go/..."],
-                       env={
-                           "GOPATH": util.Property('builddir'),
-                           "GO111MODULE": "off"
-                       }))
+    steps.ShellCommand(
+        command=['go', 'test', 'kythe.io/kythe/go/...'],
+        env={
+            'GOPATH': util.Property('builddir'),
+            'GO111MODULE': 'off'
+        }))
 
 goModuleKytheSteps = util.BuildFactory()
 goModuleKytheSteps.addStep(
-    steps.GitHub(repourl=util.Property('repository',
-                                       'git://github.com/kythe/kythe.git'),
-                 mode='full',
-                 method='copy'))
-goModuleKytheSteps.addStep(steps.ShellCommand(command=["go", "version"]))
+    steps.GitHub(
+        repourl=util.Property('repository', 'git://github.com/kythe/kythe.git'),
+        mode='full',
+        method='copy'))
+goModuleKytheSteps.addStep(steps.ShellCommand(command=['go', 'version']))
 goModuleKytheSteps.addStep(
-    steps.ShellCommand(command=["go", "test", "./kythe/..."],
-                       env={"GO111MODULE": "on"},
-                       haltOnFailure=True))
+    steps.ShellCommand(
+        command=['go', 'test', './kythe/...'],
+        env={'GO111MODULE': 'on'},
+        haltOnFailure=True))
 # Check that the Kythe module files are up-to-date
 goModuleKytheSteps.addStep(
     steps.ShellCommand(
-        command="! { git diff --color=always -- go.{mod,sum} | grep .; }",
+        command='! { git diff --color=always -- go.{mod,sum} | grep .; }',
         warnOnFailure=True))
 
-build_lock = util.WorkerLock("worker_builds",
-                             maxCount=1,
-                             maxCountForWorker={'local-worker': 3})
+build_lock = util.WorkerLock(
+    'worker_builds', maxCount=1, maxCountForWorker={'local-worker': 3})
 
 c['builders'] = []
 c['builders'].append(
-    util.BuilderConfig(name="arc-lint",
-                       workernames=["local-worker"],
-                       factory=arcLintSteps))
+    util.BuilderConfig(
+        name='pre-commit', workernames=['local-worker'],
+        factory=preCommitSteps))
 c['builders'].append(
-    util.BuilderConfig(name="bazel-minversion",
-                       workernames=["local-worker"],
-                       properties={
-                           'bazel_version': 'min',
-                           'bazel_flags': ['--config=remote']
-                       },
-                       locks=[build_lock.access('counting')],
-                       factory=bazelKytheSteps))
+    util.BuilderConfig(
+        name='bazel-minversion',
+        workernames=['local-worker'],
+        properties={
+            'bazel_version': 'min',
+            'bazel_flags': ['--config=remote']
+        },
+        locks=[build_lock.access('counting')],
+        factory=bazelKytheSteps))
 c['builders'].append(
-    util.BuilderConfig(name="bazel",
-                       workernames=["local-worker"],
-                       properties={'bazel_flags': ['--config=remote']},
-                       locks=[build_lock.access('counting')],
-                       factory=bazelKytheSteps))
+    util.BuilderConfig(
+        name='bazel',
+        workernames=['local-worker'],
+        properties={'bazel_flags': ['--config=remote']},
+        locks=[build_lock.access('counting')],
+        factory=bazelKytheSteps))
 c['builders'].append(
     util.BuilderConfig(
-        name="bazel-release",
-        workernames=["local-worker"],
+        name='bazel-release',
+        workernames=['local-worker'],
         properties={
             'bazel_flags': ['--config=remote', '-c', 'opt', '--stamp'],
             'bazel_targets': [
@@ -314,10 +342,11 @@ c['builders'].append(
 #                       factory=goKytheSteps))
 
 c['builders'].append(
-    util.BuilderConfig(name="go-module",
-                       workernames=["local-worker"],
-                       locks=[build_lock.access('counting')],
-                       factory=goModuleKytheSteps))
+    util.BuilderConfig(
+        name='go-module',
+        workernames=['local-worker'],
+        locks=[build_lock.access('counting')],
+        factory=goModuleKytheSteps))
 
 ####### BUILDBOT SERVICES
 
@@ -325,38 +354,46 @@ c['builders'].append(
 @util.renderer
 @defer.inlineCallbacks
 def renderBuildState(props):
-    build = yield props.master.data.get(
-        ('builders', props.getProperty('buildername'), 'builds',
-         props.getProperty('buildnumber')))
-    defer.returnValue(build['state_string'].capitalize())
+  build = yield props.master.data.get(
+      ('builders', props.getProperty('buildername'), 'builds',
+       props.getProperty('buildnumber')))
+  defer.returnValue(build['state_string'].capitalize())
 
 
 # Push GitHub status messages for every build start/end.
 c['services'] = []
 c['services'].append(
-    reporters.GitHubStatusPush(token=secrets_dict['github_token'],
-                               startDescription=renderBuildState,
-                               endDescription=renderBuildState))
+    reporters.GitHubStatusPush(
+        token=secrets_dict['github_token'],
+        generators=[
+            BuildStartEndStatusGenerator(
+                start_formatter=reporters.MessageFormatterRenderable(
+                    subject=renderBuildState, template=renderBuildState),
+                end_formatter=reporters.MessageFormatterRenderable(
+                    subject=renderBuildState, template=renderBuildState),
+            )
+        ]))
 
 ####### PROJECT IDENTITY
 
-c['title'] = "Kythe CI"
-c['titleURL'] = "https://buildbot-dot-kythe-repo.appspot.com/"
-c['buildbotURL'] = "https://buildbot-dot-kythe-repo.appspot.com/"
+c['title'] = 'Kythe CI'
+c['titleURL'] = 'https://buildbot-dot-kythe-repo.appspot.com/'
+c['buildbotURL'] = 'https://buildbot-dot-kythe-repo.appspot.com/'
 c['www'] = {
     'port':
         8080,  # AppEngine web port
     'plugins':
         dict(waterfall_view={}, console_view={}, grid_view={}),
     'auth':
-        util.GitHubAuth(secrets_dict["github_auth_id"],
-                        secrets_dict["github_auth_secret"],
-                        apiVersion=4,
-                        getTeamsMembership=True),
+        util.GitHubAuth(
+            secrets_dict['github_auth_id'],
+            secrets_dict['github_auth_secret'],
+            apiVersion=4,
+            getTeamsMembership=True),
     'authz':
         util.Authz(
             # restrict control access to users in the 'google' org
-            allowRules=[util.AnyControlEndpointMatcher(role="google")],
+            allowRules=[util.AnyControlEndpointMatcher(role='google')],
             roleMatchers=[util.RolesFromGroups()]),
 }
 
@@ -364,6 +401,6 @@ c['www'] = {
 
 c['db'] = {
     'db_url':
-        "postgresql+psycopg2://{db_userpass}@/buildbot?host={db_host}".format(
+        'postgresql+psycopg2://{db_userpass}@/buildbot?host={db_host}'.format(
             **secrets_dict),
 }
diff --git a/kythe/release/appengine/default/.gcloudignore b/kythe/release/appengine/default/.gcloudignore
index 199e6d9f2..f91c3786a 100644
--- a/kythe/release/appengine/default/.gcloudignore
+++ b/kythe/release/appengine/default/.gcloudignore
@@ -22,4 +22,4 @@
 # Test binary, build with `go test -c`
 *.test
 # Output of the go coverage tool, specifically when used with LiteIDE
-*.out
\ No newline at end of file
+*.out
diff --git a/kythe/release/appengine/xrefs/build_serving_tables.sh b/kythe/release/appengine/xrefs/build_serving_tables.sh
index 5323d185c..57ec9a6fa 100755
--- a/kythe/release/appengine/xrefs/build_serving_tables.sh
+++ b/kythe/release/appengine/xrefs/build_serving_tables.sh
@@ -86,7 +86,7 @@ if [[ -n "$(find "$GRAPHSTORE" -maxdepth 0 -empty)" ]]; then
     trap "rm -rf '$TMP_REPO'" EXIT ERR INT
     COMMIT=$(git rev-parse --abbrev-ref HEAD)
     echo "Checking out $COMMIT to $TMP_REPO for file indexing" >&2
-    git --work-tree="$TMP_REPO" checkout -f $COMMIT
+    git --work-tree="$TMP_REPO" checkout -f "$COMMIT"
 
     cd "$TMP_REPO"
     "$TOOLS/directory_indexer" --vnames kythe/data/vnames.json | \
diff --git a/kythe/release/appengine/xrefs/deploy.sh b/kythe/release/appengine/xrefs/deploy.sh
index 3594e0888..fc54a4cae 100755
--- a/kythe/release/appengine/xrefs/deploy.sh
+++ b/kythe/release/appengine/xrefs/deploy.sh
@@ -42,4 +42,3 @@ if [[ -n "$DEV" ]]; then
 else
   gcloud app deploy --promote --server gcr.appengine.google.com app.yaml
 fi
-
diff --git a/kythe/release/base/fix_permissions.sh b/kythe/release/base/fix_permissions.sh
index 59841052e..c933c75ca 100755
--- a/kythe/release/base/fix_permissions.sh
+++ b/kythe/release/base/fix_permissions.sh
@@ -13,5 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 dir="${1:?missing path}"
+# shellcheck disable=SC2012
 ug=$(ls -ld "$dir" | awk '{print $3":"$4}')
 chown -R "$ug" "$dir"
diff --git a/kythe/release/kythe.sh b/kythe/release/kythe.sh
index a0c5ee5b0..9cfd6ff3b 100644
--- a/kythe/release/kythe.sh
+++ b/kythe/release/kythe.sh
@@ -124,7 +124,8 @@ if [[ -z "$INDEXING" ]]; then
 fi
 
 drive_indexer_kzip() {
-  local lang="$(basename "$(dirname "$1")")"
+  local lang
+  lang="$(basename "$(dirname "$1")")"
   local analyzer="/kythe/bin/${lang}_indexer"
   if [[ ! -x "$analyzer" ]]; then
     if [[ -n "$IGNORE_UNHANDLED" ]]; then
diff --git a/kythe/release/maven_extractor.sh b/kythe/release/maven_extractor.sh
index d0401bfb2..5a3825d7b 100644
--- a/kythe/release/maven_extractor.sh
+++ b/kythe/release/maven_extractor.sh
@@ -49,6 +49,6 @@ if [[ ! -s "$CP" ]]; then
   echo > "$CP"
 fi
 
-SRCS="$(find -name '*.java')"
+readarray -t SRCS < <(find . -name '*.java')
 
-java -jar /kythe/bin/javac_extractor_deploy.jar -cp "@$CP" $SRCS
+java -jar /kythe/bin/javac_extractor_deploy.jar -cp "@$CP" "${SRCS[@]}"
diff --git a/kythe/release/package_release.sh b/kythe/release/package_release.sh
index ab96e564e..91fdae84f 100755
--- a/kythe/release/package_release.sh
+++ b/kythe/release/package_release.sh
@@ -58,6 +58,7 @@ PBASE="$OUT.dir/$(basename "$OUT" .tar.gz)"
 P=$PBASE
 
 mkdir -p "$PBASE"
+# shellcheck disable=SC2064
 trap "rm -rf '$PWD/$OUT.dir'" EXIT ERR INT
 
 VERBOSE=
@@ -101,7 +102,7 @@ while [[ $# -gt 0 ]]; do
           break
         fi
       done
-      if [[ ! -z "$skip" ]]; then
+      if [[ -n "$skip" ]]; then
         log "Excluding $1"
       elif [[ -z "$RELPATHS" ]]; then
         log "Copying $1 to $P"
diff --git a/kythe/release/setup_release.sh b/kythe/release/setup_release.sh
index ff6a0fcda..a0b321162 100755
--- a/kythe/release/setup_release.sh
+++ b/kythe/release/setup_release.sh
@@ -39,7 +39,7 @@
 #       release commit
 #   12) "Publish release"
 
-cd "$(dirname $0)"/../..
+cd "$(dirname "$0")"/../..
 
 if [[ "$(git rev-parse --abbrev-ref HEAD)" != "master" ]]; then
   echo "ERROR: not on master branch" >&2
@@ -62,6 +62,7 @@ if [[ "$previous_version" != v*.*.* ]]; then
 fi
 previous_version=${previous_version#v}
 
+# shellcheck disable=SC2206
 components=(${previous_version//./ })
 ((components[2]++))
 
diff --git a/kythe/rust/fuchsia_extractor/README.md b/kythe/rust/fuchsia_extractor/README.md
index 21e339501..2166f56c4 100644
--- a/kythe/rust/fuchsia_extractor/README.md
+++ b/kythe/rust/fuchsia_extractor/README.md
@@ -172,4 +172,3 @@ Other than that, the packaged files are:
   subdirectories.
 - `*.rlib` files which are the required part of the compilation, even though
   the current indexer does not use them.
-
diff --git a/kythe/rust/fuchsia_extractor/src/bin/main.rs b/kythe/rust/fuchsia_extractor/src/bin/main.rs
index 7487265eb..549a899a8 100644
--- a/kythe/rust/fuchsia_extractor/src/bin/main.rs
+++ b/kythe/rust/fuchsia_extractor/src/bin/main.rs
@@ -353,10 +353,10 @@ fn corpus_root_for(relative_path: &PathBuf) -> &'static CorpusRoot {
 /// `src_path` is the absolute, but not necessarily canonicalized path to the
 /// file to add.
 fn add_source_input(
-    mut archive: &mut kzip::Writer, 
-    src_path: &PathBuf, 
-    options: &Options, 
-    mut required_inputs: &mut Vec<String>, 
+    mut archive: &mut kzip::Writer,
+    src_path: &PathBuf,
+    options: &Options,
+    mut required_inputs: &mut Vec<String>,
     mut file_inputs: &mut Vec<CompilationUnit_FileInput>,
     ) -> Result<()> {
         // "../../file.txt"
diff --git a/kythe/typescript/browse b/kythe/typescript/browse
index 4054c1920..87c2972fe 100755
--- a/kythe/typescript/browse
+++ b/kythe/typescript/browse
@@ -21,11 +21,11 @@ KYTHE="${KYTHE:-/opt/kythe}"
 rm -f -- graphstore/* tables/*
 mkdir -p graphstore tables
 # Read JSON entries from standard in to a graphstore.
-$KYTHE/tools/entrystream --read_format=json \
-  | $KYTHE/tools/write_entries -graphstore graphstore
+"$KYTHE/tools/entrystream" --read_format=json \
+  | "$KYTHE/tools/write_entries" -graphstore graphstore
 # Convert the graphstore to serving tables.
-$KYTHE/tools/write_tables -graphstore graphstore -out=tables
+"$KYTHE/tools/write_tables" -graphstore graphstore -out=tables
 # Host the browser UI.
-$KYTHE/tools/http_server -serving_table tables \
+"$KYTHE/tools/http_server" -serving_table tables \
   -public_resources="$KYTHE/web/ui" \
   -listen=":${BROWSE_PORT}"
diff --git a/kythe/typescript/languageserver/vscode-extension/.vscode/launch.json b/kythe/typescript/languageserver/vscode-extension/.vscode/launch.json
index 8283b5a3f..382669eda 100644
--- a/kythe/typescript/languageserver/vscode-extension/.vscode/launch.json
+++ b/kythe/typescript/languageserver/vscode-extension/.vscode/launch.json
@@ -2,7 +2,7 @@
 {
 	"version": "0.1.0",
 	"configurations": [
-	
+
 		{
 			"name": "Launch Extension",
 			"type": "extensionHost",
diff --git a/kythe/typescript/languageserver/vscode-extension/README.md b/kythe/typescript/languageserver/vscode-extension/README.md
index fff065082..1932d4ef5 100644
--- a/kythe/typescript/languageserver/vscode-extension/README.md
+++ b/kythe/typescript/languageserver/vscode-extension/README.md
@@ -20,7 +20,7 @@ npm run watch
 It should be possible to debug the extension by opening the `vscode-extension`
 folder in Visual Studio Code and pressing `F5`.
 
-For more information, see `.vscode/launch.json` and 
+For more information, see `.vscode/launch.json` and
 [Running and Debugging Your Extension](https://code.visualstudio.com/docs/extensions/debugging-extensions)
 
 ## Packaging
diff --git a/kythe/web/site/contributing.md b/kythe/web/site/contributing.md
index 0183c1fb9..a2d5c446d 100644
--- a/kythe/web/site/contributing.md
+++ b/kythe/web/site/contributing.md
@@ -35,10 +35,11 @@ Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) specification
 to bypass these checks.  In addition to the tools necessary to build Kythe, the
 following are necessary to run the installed hooks:
 
-- arcanist
+- pre-commit
 - buildifier
-- clang-format
+- clang-format (>= 11)
 - gofmt
+- golint
 - google-java-format
 - jq
 - shellcheck
diff --git a/kythe/web/site/css/main.scss b/kythe/web/site/css/main.scss
old mode 100755
new mode 100644
diff --git a/kythe/web/site/js/main.js b/kythe/web/site/js/main.js
index 3e9690e82..c300a96ba 100644
--- a/kythe/web/site/js/main.js
+++ b/kythe/web/site/js/main.js
@@ -21,10 +21,13 @@ $(document).ready(function() {
       .addClass('panel panel-default')
       .removeClass('listingblock output')
       .each(function(_, el) {
-    var id = $(el).find('.content')
-        .addClass('collapse in panel-body')
-        .uniqueId().attr('id');
-    $(el).find('.title')
-        .replaceWith('<div class="panel-heading"><a class="title" data-toggle="collapse" href="#' + id + '">Output</a></div>');
-  });
+        var id = $(el)
+                     .find('.content')
+                     .addClass('collapse in panel-body')
+                     .uniqueId()
+                     .attr('id');
+        $(el).find('.title').replaceWith(
+            '<div class="panel-heading"><a class="title" data-toggle="collapse" href="#' +
+            id + '">Output</a></div>');
+      });
 });
diff --git a/kythe/web/site/sync_docs.sh b/kythe/web/site/sync_docs.sh
index 0667c32c5..fb1a14581 100755
--- a/kythe/web/site/sync_docs.sh
+++ b/kythe/web/site/sync_docs.sh
@@ -36,25 +36,26 @@ cd "$DIR/../../.."
 # Use the project's bazelrc so we pick up the default options that Kythe needs
 # to build correctly.
 bazel --bazelrc=.bazelrc build //kythe/docs/... \
-    //kythe/docs:schema-overview \
-    //kythe/docs/schema \
-    //kythe/docs/schema:callgraph \
-    //kythe/docs/schema:verifierstyle \
-    //kythe/docs/schema:writing-an-indexer \
-    //kythe/docs/schema:indexing-generated-code \
-    //kythe/docs/schema:marked-source
+  //kythe/docs:schema-overview \
+  //kythe/docs/schema \
+  //kythe/docs/schema:callgraph \
+  //kythe/docs/schema:verifierstyle \
+  //kythe/docs/schema:writing-an-indexer \
+  //kythe/docs/schema:indexing-generated-code \
+  //kythe/docs/schema:marked-source
 # Copy the zipped asciidoc outputs into the staging directory, unpack the
 # archives, then remove them. We do this to ensure the output retains the
 # directory structure of the source tree.
 rsync -Lr --chmod=a+w --delete "bazel-bin/kythe/docs/" "$DIR"/_docs
 find "$DIR"/_docs -type f -name '*.zip' -execdir unzip -q {} ';' -delete
 
-DOCS=($(bazel query 'kind("source file", deps(//kythe/docs/..., 1))' | \
-  grep -E '\.(txt|adoc|ad)$' | \
-  parallel --gnu -L1 'x() { file="$(tr : / <<<"$1")"; echo ${file#//kythe/docs/}; }; x'))
+# shellcheck disable=SC2016
+readarray -t DOCS < <(bazel query 'kind("source file", deps(//kythe/docs/..., 1))' |
+  grep -E '\.(txt|adoc|ad)$' |
+  parallel --gnu -L1 'x() { file="$(tr : / <<<"$1")"; echo ${file#//kythe/docs/}; }; x')
 
 asciidoc_query() {
-  bundle exec ruby -r asciidoctor -e 'puts Asciidoctor.load_file(ARGV[0]).'"$2" "$1" 2>/dev/null
+  bundle exec ruby -r asciidoctor -e 'puts Asciidoctor.load_file(ARGV[0]).'"$2" "$1" 2> /dev/null
 }
 
 asciidoc_attribute_presence() {
@@ -70,20 +71,22 @@ toclevels: $(asciidoc_query "$1" 'attributes["toclevels"]')"
   if asciidoc_attribute_presence "$1" toc || asciidoc_attribute_presence "$1" toc2; then
     echo "toc: true"
   fi
-echo "---"
+  echo "---"
 }
 
 TMP="$(mktemp)"
 trap 'rm -rf "$TMP"' EXIT ERR INT
 
 cd "$DIR"
-for doc in ${DOCS[@]}; do
+for doc in "${DOCS[@]}"; do
   html=${doc%%.*}.html
   echo "Processing $html"
   abs_path="../../../kythe/docs/$doc"
   cp "_docs/$html" "$TMP"
-  { doc_header "$abs_path";
-    cat "$TMP"; } >"_docs/$html"
+  {
+    doc_header "$abs_path"
+    cat "$TMP"
+  } > "_docs/$html"
 done
 
 mv _docs/schema/{schema,index}.html
diff --git a/tools/arc/.gitignore b/tools/arc/.gitignore
deleted file mode 100644
index 6b193c3e1..000000000
--- a/tools/arc/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-.phutil_module_cache
diff --git a/tools/arc/__phutil_library_init__.php b/tools/arc/__phutil_library_init__.php
deleted file mode 100644
index e1d86a465..000000000
--- a/tools/arc/__phutil_library_init__.php
+++ /dev/null
@@ -1,3 +0,0 @@
-<?php
-
-phutil_register_library('libbazel', __FILE__);
diff --git a/tools/arc/__phutil_library_map__.php b/tools/arc/__phutil_library_map__.php
deleted file mode 100644
index 522eb7782..000000000
--- a/tools/arc/__phutil_library_map__.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/**
- * This file is automatically generated. Use 'arc liberate' to rebuild it.
- *
- * @generated
- * @phutil-library-version 2
- */
-phutil_register_library_map(array(
-  '__library_version__' => 2,
-  'class' => array(
-    'BazelTestEngine' => 'unit/engine/BazelTestEngine.php'
-  ),
-  'function' => array(),
-  'xmap' => array(),
-));
diff --git a/tools/arc/linter.sh b/tools/arc/linter.sh
deleted file mode 100755
index 55c0a2d21..000000000
--- a/tools/arc/linter.sh
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/bin/bash -e
-
-# Copyright 2015 The Kythe Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Kythe's arcanist script-and-regex linter. Expected output format:
-#   /^((?P<name>.+?)::)?(?P<severity>warning|error|advice):(?P<line>\\d+)? (?P<message>.*)$/m
-#
-# Usage: linter.sh <file>
-#
-# Optionally uses shellcheck if it is found on PATH along with jq.
-#
-# Arcanist Documentation:
-#   https://secure.phabricator.com/book/phabricator/article/arcanist_lint_script_and_regex/
-
-readonly file="$1"
-readonly fullpath="$PWD/$file"
-readonly name="$(basename "$1")"
-readonly dir="$(dirname "$1")"
-
-case $file in
-  AUTHORS|CONTRIBUTORS|WORKSPACE|third_party/*|tools/*|*.md|*BUILD|*/testdata/*|*.yaml|*.json|*.html|*.pb.go|.arclint|.gitignore|*/.gitignore|.arcconfig|*/__phutil_*|*.bzl|.kythe|kythe/web/site/*|go.mod|go.sum|*bazelrc|*.yml|.bazel*version|*.lock)
-    ;; # skip copyright checks
-  *)
-    if ! grep -q 'Copyright 20[12][0-9] The Kythe Authors. All rights reserved.' "$file"; then
-      echo 'copyright header::error:1 File missing copyright header'
-    fi ;;
-esac
-
-# Ensure consistent code style
-case $file in
-  */testdata/*)
-    ;; # skip style checks over testdata
-  BUILD|*.BUILD|*.bzl)
-    if command -v buildifier &>/dev/null; then
-      buildifier --mode=check "$file" | sed 's/^/buildifier::error:1 /'
-    fi ;;
-  *.sh|*.bash)
-    if command -v shellcheck &>/dev/null && command -v jq &>/dev/null; then
-      shellcheck -f json "$file" | \
-        jq -r '.[] | "shellcheck::" + (if .level == "info" then "advice" else .level end) + ":" + (.line | tostring) + " " + .message'
-    fi ;;
-  *.java)
-    if command -v google-java-format &>/dev/null; then
-      google-java-format -n "$file" | sed 's/^/google-java-format::error:1 /'
-    fi ;;
-  *.go)
-    if command -v jq &>/dev/null && command -v staticcheck &>/dev/null; then
-      staticcheck -f json "./$dir" | jq -r --arg file "$fullpath" \
-        'select(.location.file == $file) | "staticcheck::" + .severity + ":" + (.location.line | tostring) + " " + (.message | gsub("\n"; " "))'
-    fi
-    if command -v gofmt &>/dev/null; then
-      gofmt -l "$file" | sed 's/^/gofmt::error:1 /'
-    fi ;;
-  *.h|*.cc|*.c|*.proto|*.js)
-    cf="$(command -v clang-format-11 clang-format 2>/dev/null | head -n1)"
-    if [[ -n "$cf" ]]; then
-      diff \
-        --unchanged-line-format='' \
-        --new-line-format='clang-format::error:%dn -%L' \
-        --old-line-format='clang-format::error:%dn +%L' \
-        <("$cf" --style=file "$file") "$file" || true
-    fi ;;
-esac
-
-# Ensure filenames/paths do not clash on case-insensitive file systems.
-if grep -q [A-Z] <<<"$dir"; then
-  echo "case-insensitivity::error:1 $dir directory contains an uppercase letter"
-fi
-if [[ $(find "$dir" -maxdepth 0 -iname "$name" | wc -l) -gt 1 ]]; then
-  echo "case-insensitivity::error:1 $name filename clashes on case-insensitive file systems"
-fi
diff --git a/tools/clang_tidy/clang_tidy.sh b/tools/clang_tidy/clang_tidy.sh
index 66e12994b..d4aa39223 100755
--- a/tools/clang_tidy/clang_tidy.sh
+++ b/tools/clang_tidy/clang_tidy.sh
@@ -1,12 +1,11 @@
 #!/bin/bash
 set -e
 
-if [ -z "${CLANG_TIDY:=$(which clang-tidy)}" ]; then
+if [ -z "${CLANG_TIDY:=$(command -v clang-tidy)}" ]; then
   echo "Unable to find clang-tidy" 1>&2
   exit 1
 fi
 
-$(dirname "${BASH_SOURCE[0]}")/../cpp/generate_compilation_database.sh
+"$(dirname "${BASH_SOURCE[0]}")/../cpp/generate_compilation_database.sh"
 
 "$CLANG_TIDY" -p "$(bazel info execution_root)" "$@"
-
diff --git a/tools/git/commit-msg.sh b/tools/git/commit-msg.sh
index 4ad28d0c1..49e23fbc1 100755
--- a/tools/git/commit-msg.sh
+++ b/tools/git/commit-msg.sh
@@ -15,4 +15,9 @@
 # limitations under the License.
 
 # Check commit message: https://www.conventionalcommits.org/en/v1.0.0-beta.2/
-bazel run --run_under "cd '$PWD' && " @io_kythe//tools/git:commitlint -- --edit "$1"
+bazel run \
+  --ui_event_filters=-info \
+  --show_result=0 \
+  --noshow_progress \
+  --noshow_loading_progress \
+  --run_under "cd '$PWD' && " @io_kythe//tools/git:commitlint -- --edit "$1"
diff --git a/tools/git/pre-push.sh b/tools/git/pre-push.sh
deleted file mode 100755
index 4abafee43..000000000
--- a/tools/git/pre-push.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/bash -e
-#
-# Copyright 2018 The Kythe Authors. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# pre-push hook documentation: https://git-scm.com/docs/githooks#_pre_push
-
-if [[ -n "$(git diff --name-only)" ]]; then
-  echo "ERROR: found locally modified files; please push from a clean branch" >&2
-  git diff --name-only >&2
-  exit 1
-elif [[ -n "$(git ls-files --others --exclude-standard )" ]]; then
-  echo "ERROR: found local untracked files; please push from a clean branch" >&2
-  git ls-files --others --exclude-standard >&2
-  exit 1
-fi
-
-export WAIT_FOR_BAZEL=1
-export USE_BAZELRC=1
-
-# Ensure that we end back where we started
-current_branch="$(git rev-parse --abbrev-ref HEAD)"
-trap 'git checkout -q "$current_branch" || true' EXIT ERR INT
-
-readonly z40=0000000000000000000000000000000000000000
-remote="$1"
-
-while read -r local_ref local_sha remote_ref remote_sha; do
-  : "$remote_ref" "$remote_sha" # unused
-  if [[ "$local_sha" == "$z40" ]]; then
-    # Skip deletions
-    continue
-  fi
-
-  branch="${local_ref#refs/heads/}"
-  echo "Pre-push for branch: $branch" >&2
-  if [[ $(git rev-parse --abbrev-ref HEAD) != "$branch" ]]; then
-    git checkout -q "$branch"
-  fi
-  # TODO(#3173): remove need for arcanist
-  arc lint --never-apply-patches --rev "$remote/master" < /dev/null
-  arc unit --rev "$remote/master"
-done
diff --git a/tools/git/run-staticcheck.sh b/tools/git/run-staticcheck.sh
new file mode 100755
index 000000000..7da5840d0
--- /dev/null
+++ b/tools/git/run-staticcheck.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Copyright 2021 The Kythe Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+for DIR in $(echo "$@"|xargs -n1 dirname|sort -u); do
+    staticcheck ./"$DIR"
+done
diff --git a/tools/git/setup_hooks.sh b/tools/git/setup_hooks.sh
index e78d478f7..d34db2328 100755
--- a/tools/git/setup_hooks.sh
+++ b/tools/git/setup_hooks.sh
@@ -35,5 +35,6 @@ setup_hook() {
   fi
 }
 
-setup_hook pre-push
 setup_hook commit-msg
+pre-commit install "$@" -t pre-commit
+pre-commit install "$@" -t pre-push


Test patch:
diff --git a/kythe/cxx/indexer/cxx/DESIGN b/kythe/cxx/indexer/cxx/DESIGN.md
similarity index 100%
rename from kythe/cxx/indexer/cxx/DESIGN
rename to kythe/cxx/indexer/cxx/DESIGN.md
diff --git a/kythe/cxx/indexer/cxx/claiming.ad b/kythe/cxx/indexer/cxx/claiming.md
similarity index 100%
rename from kythe/cxx/indexer/cxx/claiming.ad
rename to kythe/cxx/indexer/cxx/claiming.md
diff --git a/kythe/cxx/verifier/verifier_unit_test.cc b/kythe/cxx/verifier/verifier_unit_test.cc
index 41899b68b..c5300081f 100644
--- a/kythe/cxx/verifier/verifier_unit_test.cc
+++ b/kythe/cxx/verifier/verifier_unit_test.cc
@@ -934,7 +934,7 @@ fact_value: ""
 }
 #- @texx defines SomeNode
 ##texx (line 22 column 2 offset 384-388))
-  	 
+
 #-  SomeAnchor defines SomeNode)"));
   ASSERT_TRUE(v.PrepareDatabase());
   ASSERT_TRUE(v.VerifyAllGoals());
@@ -1424,7 +1424,7 @@ TEST(VerifierUnitTest, CompoundGroups) {
   Verifier v;
   ASSERT_TRUE(v.LoadInlineProtoFile(R"(entries {
 #- SomeNode.content 42
-#- !{ OtherNode.content 43 
+#- !{ OtherNode.content 43
 #-    AnotherNode.content 44 }
 #- !{ LastNode.content 45 }
 source { root:"1" }
@@ -1444,7 +1444,7 @@ fact_value: "43"
 TEST(VerifierUnitTest, ConjunctionInsideNegatedGroupPassFail) {
   Verifier v;
   ASSERT_TRUE(v.LoadInlineProtoFile(R"(entries {
-#- !{ SomeNode.content 42 
+#- !{ SomeNode.content 42
 #-    OtherNode.content 44 }
 source { root:"1" }
 fact_name: "/kythe/content"
@@ -1463,7 +1463,7 @@ fact_value: "43"
 TEST(VerifierUnitTest, ConjunctionInsideNegatedGroupPassPass) {
   Verifier v;
   ASSERT_TRUE(v.LoadInlineProtoFile(R"(entries {
-#- !{ SomeNode.content 42 
+#- !{ SomeNode.content 42
 #-    OtherNode.content 43 }
 source { root:"1" }
 fact_name: "/kythe/content"
diff --git a/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_empty_cu_test.sh b/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_empty_cu_test.sh
index b30f9bae7..73418d872 100755
--- a/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_empty_cu_test.sh
+++ b/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_empty_cu_test.sh
@@ -16,9 +16,9 @@ set -eo pipefail
 #
 # This script tests that the java indexer doesn't fall over on empty CUs.
 
-: ${indexer?:missing indexer}
-: ${entrystream?:missing entrystream}
+: "${indexer?:missing indexer}"
+: "${entrystream?:missing entrystream}"
 test_kindex="$PWD/kythe/testdata/java_empty.kindex"
 
 # Test indexing a .kindex file
-$indexer "$test_kindex" | $entrystream >/dev/null
+"$indexer" "$test_kindex" | "$entrystream" >/dev/null
diff --git a/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_source_root_test.sh b/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_source_root_test.sh
index b2396121b..04db707c0 100755
--- a/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_source_root_test.sh
+++ b/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_source_root_test.sh
@@ -16,8 +16,8 @@ set -eo pipefail
 #
 # This script checks for the existance of #818
 
-: ${indexer?:missing indexer}
-: ${entrystream?:missing entrystream}
+: "${indexer?:missing indexer}"
+: "${entrystream?:missing entrystream}"
 test_kindex="$PWD/kythe/javatests/com/google/devtools/kythe/analyzers/java/testdata/corner_case.kindex"
 
 # This line removes the precondition for #818 (allowing the test to pass).
diff --git a/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_test.sh b/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_test.sh
index fe73b1c02..74cd56f03 100755
--- a/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_test.sh
+++ b/kythe/javatests/com/google/devtools/kythe/analyzers/java/indexer_test.sh
@@ -16,9 +16,9 @@ set -eo pipefail
 #
 # This script tests the java indexer's CLI.
 
-: ${indexer?:missing indexer}
-: ${entrystream?:missing entrystream}
+: "${indexer?:missing indexer}"
+: "${entrystream?:missing entrystream}"
 test_kzip="$PWD/kythe/testdata/test.kzip"
 
 # Test indexing a .kzip file
-$indexer $test_kzip | $entrystream >/dev/null
+"$indexer" "$test_kzip" | "$entrystream" >/dev/null
diff --git a/tools/arc/unit/engine/BazelTestEngine.php b/tools/arc/unit/engine/BazelTestEngine.php
deleted file mode 100644
index 6e3e69d13..000000000
--- a/tools/arc/unit/engine/BazelTestEngine.php
+++ /dev/null
@@ -1,218 +0,0 @@
-<?php
-/*
- * Copyright 2015 The Kythe Authors. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * bazel wrapper.
- */
-final class BazelTestEngine extends ArcanistUnitTestEngine {
-  private static $omit_tags = ["manual", "broken", "arc-ignore", "docker"];
-  private $debug;
-  private $useConfig;
-  private $waitForBazel;
-
-  private $project_root;
-
-  public function run() {
-    if (getenv("DEBUG")) {
-      $this->debug = true;
-    }
-    $this->debugPrint("run");
-    if (getenv("USE_BAZELRC")) {
-      $this->useConfig = true;
-      print("WARNING: using default bazelrc\n");
-    }
-    if (getenv("WAIT_FOR_BAZEL")) {
-      $this->waitForBazel = true;
-    }
-
-    $this->project_root = $this->getWorkingCopy()->getProjectRoot();
-    $targets = $this->getTargets();
-    if (empty($targets)) {
-      return array();
-    }
-    $targets[] = "//kythe/go/util/tools/print_test_status";
-    return $this->runTests($targets);
-  }
-
-  protected function supportsRunAllTests() {
-    return true;
-  }
-
-  private function getTargets() {
-    $this->debugPrint("getTargets()");
-
-    if ($this->getRunAllTests()) {
-      return array('//...');
-    }
-
-    $files = $this->getFileTargets();
-    if (empty($files)) {
-      print("No files affected\n");
-      return array();
-    }
-    // Quote each file to make it safe in case it has special characters in it.
-    $files = array_map(function($s) { return '"'.$s.'"'; }, $files);
-    $files = join(" ", $files);
-    $this->debugPrint("files: " . $files);
-
-    $cmd = $this->bazelCommand("query", ["-k", "%s"]);
-    $tag_filter = join("|", self::$omit_tags);
-    $query = 'rdeps(//..., set('.$files.')) except attr(tags, "'.$tag_filter.'", //...)';
-    $this->debugPrint($query);
-    $future = new ExecFuture($cmd, $query);
-    $future->setCWD($this->project_root);
-    $status = $future->resolve();
-    if ($status[0] != 3 && $status[0] != 0) {
-      throw new Exception("Bazel query error (".$status[0]."): ".$status[2]);
-    }
-    $output = trim($status[1]);
-    if ($output === "") {
-      print("No targets affected\n");
-      return array();
-    }
-    // Quote each target to make it safe in case it has special characters in it.
-    return array_map(function($s) { return '"'.$s.'"'; }, explode("\n", $output));
-  }
-
-  private function getFileTargets() {
-    if (empty($this->getPaths())) {
-      return array();
-    }
-    $files = join(" ",
-                  array_map(array('BazelTestEngine', 'fileToTarget'), $this->getPaths()));
-    $future = new ExecFuture($this->bazelCommand("query", ["-k", "%s"]), 'set('.$files.')');
-    $future->setCWD($this->project_root);
-
-    $status = $future->resolve();
-    if ($status[0] != 3 && $status[0] != 0) {
-      throw new Exception("Bazel query error (".$status[0]."): ".$status[2]);
-    }
-
-    $output = trim($status[1]);
-    if ($output === "") {
-      return array();
-    }
-
-    return explode("\n", $output);
-  }
-
-  private function runTests($targets) {
-    $this->debugPrint("runTests(" . join(", ", $targets) . ")");
-
-    $tag_filters = join(",", array_map(function($s) { return "-$s"; }, self::$omit_tags));
-    $future = new ExecFuture($this->bazelCommand("test", array_merge([
-        "--config=prepush",
-        "--verbose_failures",
-        "--test_tag_filters=$tag_filters",
-        "--noshow_loading_progress",
-        "--noshow_progress"], $targets)));
-    $future->setCWD($this->project_root);
-    $status = $future->resolve();
-    return $this->parseTestResults($targets, $status);
-  }
-
-  private function parseTestResults($targets, $status) {
-    $this->debugPrint("parseTestResults");
-
-    $code = $status[0];
-    $output = $status[1];
-    $lines = explode("\n", $output);
-
-    if ($code == 4) {
-      print("No tests affected...\n");
-      return [];
-    } else if ($code == 1) {
-      throw new Exception($output . "\n" . $status[2]);
-    }
-
-    $cmd = $this->bazelCommand("query", ["-k", "%s"]);
-    $tag_filter = join("|", self::$omit_tags);
-    $query = 'tests(set('.join(" ", $targets).'))';
-    $this->debugPrint($query);
-    $future = new ExecFuture($cmd, $query);
-    $future->setCWD($this->project_root);
-    $testTargets = explode("\n", trim($future->resolvex()[0]));
-
-    $results = array();
-    foreach ($testTargets as $test) {
-      $result = new ArcanistUnitTestResult();
-      $result->setName($test);
-      try {
-        $data = $this->parseTestResultFile($test);
-        if (array_key_exists("test_case", $data)) {
-          $testCase = $data["test_case"];
-          if (array_key_exists("run_duration_millis", $testCase)) {
-            $result->setDuration($testCase["run_duration_millis"] / 1000);
-          }
-        }
-        if ($data["test_passed"]) {
-          $result->setResult(ArcanistUnitTestResult::RESULT_PASS);
-        } else if ($data["status"] == 4) {
-          $result->setResult(ArcanistUnitTestResult::RESULT_FAIL);
-        } else {
-          $result->setResult(ArcanistUnitTestResult::RESULT_BROKEN);
-        }
-      } catch (CommandException $exc) {
-        if ($code == 0) {
-          // bazel test exited successfully, therefore these are
-          // skipped tests, rather than broken.
-          $result->setResult(ArcanistUnitTestResult::RESULT_SKIP);
-        } else {
-          $result->setResult(ArcanistUnitTestResult::RESULT_BROKEN);
-        }
-      }
-
-      $results[] = $result;
-    }
-
-    return $results;
-  }
-
-  private function parseTestResultFile($target) {
-    $path = "bazel-testlogs/".str_replace(":", "/", substr($target, 2))."/test.cache_status";
-    $future = new ExecFuture("bazel-bin/kythe/go/util/tools/print_test_status/print_test_status %s", $path);
-    $future->setCWD($this->project_root);
-    return $future->resolveJSON();
-  }
-
-  private static function fileToTarget($file) {
-    if (dirname($file) == ".") {
-      return '//:' . $file;
-    }
-    return "'" . $file . "'";
-  }
-
-  private function bazelCommand($subcommand, $args) {
-    $cmd = "bazel ";
-    if (!$this->useConfig) {
-      $cmd = $cmd . "--bazelrc=/dev/null ";
-    }
-    if (!$this->waitForBazel) {
-      $cmd = $cmd . "--noblock_for_lock ";
-    }
-    $cmd = $cmd . $subcommand . " --tool_tag=arcanist ";
-    $cmd = $cmd . join(" ", $args);
-    $this->debugPrint($cmd);
-    return $cmd;
-  }
-
-  private function debugPrint($msg) {
-    if ($this->debug) {
-      print("DEBUG: " . $msg . "\n");
-    }
-  }
-}
diff --git a/tools/git/test-affected-targets.sh b/tools/git/test-affected-targets.sh
new file mode 100755
index 000000000..789e052b4
--- /dev/null
+++ b/tools/git/test-affected-targets.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+# Copyright 2021 The Kythe Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# A pre-commit hook which will determine and run affected test targets using the
+# files passed on the command line.
+# Usage: test-affected-targets.sh file...
+set -e
+OMIT_TAGS=(manual broken arc-ignore docker)
+
+function join_by {
+  local d="$1"
+  shift
+  local f="$1"
+  shift
+  printf %s "$f" "${@/#/$d}";
+}
+
+TAG_RE="\\b($(join_by '|' "${OMIT_TAGS[@]}"))\\b"
+
+readarray -t TARGETS < <(bazel query \
+  --keep_going \
+  --noshow_progress \
+  "rdeps(//..., set($*)) except attr('tags', '$TAG_RE', //...)")
+
+if [[ "${#TARGETS[@]}" -gt 0 ]]; then
+  echo "Building targets"
+  bazel build --config=prepush "${TARGETS[@]}"
+fi
+
+readarray -t TESTS < <(bazel query \
+  --keep_going \
+  --noshow_progress \
+  "tests(rdeps(//..., set($*))) except attr('tags', '$TAG_RE', //...)")
+
+if [[ "${#TESTS[@]}" -gt 0 ]]; then
+  echo "Running tests"
+  bazel test --config=prepush "${TESTS[@]}"
+fi