tokendb_check: Initial commit

This recipe checks that token database CLs do not remove entries, but
provides a way to bypass if necessary. This should prevent the tokendb
updater from occasionally removing entries when it shouldn't. (Still
working on tracking that down.)

Also move common code from tokendb_check and doc_check to new util
module.

Change-Id: I01dd4d1b529ca5def2a113cd939ae3c0321128f8
Reviewed-on: https://pigweed-review.googlesource.com/c/infra/recipes/+/49780
Commit-Queue: Rob Mohr <mohrr@google.com>
Pigweed-Auto-Submit: Rob Mohr <mohrr@google.com>
Reviewed-by: Marc-Antoine Ruel <maruel@google.com>
diff --git a/recipe_modules/util/api.py b/recipe_modules/util/api.py
new file mode 100644
index 0000000..2f78f35
--- /dev/null
+++ b/recipe_modules/util/api.py
@@ -0,0 +1,82 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+#     https://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.
+"""Utility functions common to multiple recipes that don't fit elsewhere."""
+
+import re
+
+import attr
+from recipe_engine import recipe_api
+
+
+@attr.s
+class ChangeWithComments(object):
+    change = attr.ib()
+    comments = attr.ib()
+
+
+class UtilApi(recipe_api.RecipeApi):
+    def get_change_with_comments(self):
+        input_ = self.m.buildbucket.build.input
+        change_id = str(input_.gerrit_changes[0].change)
+        change = self.m.gerrit.change_details(
+            'change details',
+            change_id=change_id,
+            host=input_.gerrit_changes[0].host,
+            query_params=['CURRENT_COMMIT', 'CURRENT_REVISION', 'ALL_FILES'],
+            test_data=self.m.json.test_api.output(
+                {
+                    'owner': {'email': 'coder@example.com',},
+                    'current_revision': 'a' * 40,
+                    'revisions': {
+                        'a' * 40: {'files': [], 'commit': {'message': '',},}
+                    },
+                    'revert_of': 0,
+                }
+            ),
+        ).json.output
+
+        current_revision = change['revisions'][change['current_revision']]
+        comments = [current_revision['commit']['message']]
+
+        comments_result = self.m.gerrit.list_change_comments(
+            "list change comments",
+            change_id,
+            test_data=self.m.json.test_api.output(
+                {'/PATCHSET_LEVEL': [{'message': ''}],}
+            ),
+        ).json.output
+
+        for file in comments_result:
+            for comment_data in comments_result[file]:
+                comments.append(comment_data['message'])
+
+        return ChangeWithComments(change, comments)
+
+    def find_matching_comment(self, rx, comments):
+        """Find a comment in comments that matches regex object rx."""
+        result = None
+        with self.m.step.nest('checking comments'):
+            for i, comment in enumerate(comments):
+                with self.m.step.nest('comment ({})'.format(i)) as pres:
+                    pres.step_summary_text = comment
+                    match = re.search(rx, comment)
+                    if match:
+                        result = match
+                        break
+
+            if result:
+                with self.m.step.nest('found'):
+                    pass
+
+        return result