Skip to content

Commit c2ea697

Browse files
authored
feat: get PR description from googleapis commits (#2531)
In this PR: - Add `generate_pr_description.py` to get commit messages from googleapis repository. - Parse configuration to get latest googleapis commit (C) and proto_path (P). - Combine commit message from C to a baseline commit (B), exclusively. Only commits that change files in P are considered. - Add unit tests for utility functions. - Add integration tests for `generate_pr_description.py`. This is [step 5](https://docs.google.com/document/d/1JiCcG3X7lnxaJErKe0ES_JkyU7ECb40nf2Xez3gWvuo/edit?pli=1&tab=t.0#bookmark=id.g8qkyq11ygpx) in milestone 2 of hermetic build project.
1 parent 5fd4d5e commit c2ea697

11 files changed

+527
-3
lines changed

.github/workflows/verify_library_generation.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
- name: Run python unit tests
9898
run: |
9999
set -x
100-
python -m unittest library_generation/test/unit_tests.py
100+
python -m unittest discover -s library_generation/test/ -p "*unit_tests.py"
101101
lint-shell:
102102
runs-on: ubuntu-22.04
103103
steps:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2024 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
import os
16+
import shutil
17+
from typing import Dict
18+
19+
import click
20+
from git import Commit, Repo
21+
from library_generation.model.generation_config import from_yaml
22+
from library_generation.utilities import find_versioned_proto_path
23+
from library_generation.utils.commit_message_formatter import format_commit_message
24+
from library_generation.utilities import get_file_paths
25+
from library_generation.utils.commit_message_formatter import wrap_nested_commit
26+
27+
28+
@click.group(invoke_without_command=False)
29+
@click.pass_context
30+
@click.version_option(message="%(version)s")
31+
def main(ctx):
32+
pass
33+
34+
35+
@main.command()
36+
@click.option(
37+
"--generation-config-yaml",
38+
required=True,
39+
type=str,
40+
help="""
41+
Path to generation_config.yaml that contains the metadata about
42+
library generation.
43+
The googleapis commit in the configuration is the latest commit,
44+
inclusively, from which the commit message is considered.
45+
""",
46+
)
47+
@click.option(
48+
"--baseline-commit",
49+
required=True,
50+
type=str,
51+
help="""
52+
The baseline (oldest) commit, exclusively, from which the commit message is
53+
considered.
54+
This commit should be an ancestor of googleapis commit in configuration.
55+
""",
56+
)
57+
@click.option(
58+
"--repo-url",
59+
type=str,
60+
default="https://github.com/googleapis/googleapis.git",
61+
show_default=True,
62+
help="""
63+
GitHub repository URL.
64+
""",
65+
)
66+
def generate(
67+
generation_config_yaml: str,
68+
repo_url: str,
69+
baseline_commit: str,
70+
) -> str:
71+
return generate_pr_descriptions(
72+
generation_config_yaml=generation_config_yaml,
73+
repo_url=repo_url,
74+
baseline_commit=baseline_commit,
75+
)
76+
77+
78+
def generate_pr_descriptions(
79+
generation_config_yaml: str,
80+
repo_url: str,
81+
baseline_commit: str,
82+
) -> str:
83+
config = from_yaml(generation_config_yaml)
84+
paths = get_file_paths(config)
85+
return __get_commit_messages(
86+
repo_url=repo_url,
87+
latest_commit=config.googleapis_commitish,
88+
baseline_commit=baseline_commit,
89+
paths=paths,
90+
generator_version=config.gapic_generator_version,
91+
is_monorepo=config.is_monorepo,
92+
)
93+
94+
95+
def __get_commit_messages(
96+
repo_url: str,
97+
latest_commit: str,
98+
baseline_commit: str,
99+
paths: Dict[str, str],
100+
generator_version: str,
101+
is_monorepo: bool,
102+
) -> str:
103+
"""
104+
Combine commit messages of a repository from latest_commit to
105+
baseline_commit. Only commits which change files in a pre-defined
106+
file paths will be considered.
107+
Note that baseline_commit should be an ancestor of latest_commit.
108+
109+
:param repo_url: the url of the repository.
110+
:param latest_commit: the newest commit to be considered in
111+
selecting commit message.
112+
:param baseline_commit: the oldest commit to be considered in
113+
selecting commit message. This commit should be an ancestor of
114+
:param paths: a mapping from file paths to library_name.
115+
:param generator_version: the version of the generator.
116+
:param is_monorepo: whether to generate commit messages in a monorepo.
117+
:return: commit messages.
118+
"""
119+
tmp_dir = "/tmp/repo"
120+
shutil.rmtree(tmp_dir, ignore_errors=True)
121+
os.mkdir(tmp_dir)
122+
repo = Repo.clone_from(repo_url, tmp_dir)
123+
commit = repo.commit(latest_commit)
124+
qualified_commits = {}
125+
while str(commit.hexsha) != baseline_commit:
126+
commit_and_name = __filter_qualified_commit(paths=paths, commit=commit)
127+
if commit_and_name != ():
128+
qualified_commits[commit_and_name[0]] = commit_and_name[1]
129+
commit_parents = commit.parents
130+
if len(commit_parents) == 0:
131+
break
132+
commit = commit_parents[0]
133+
shutil.rmtree(tmp_dir, ignore_errors=True)
134+
return __combine_commit_messages(
135+
latest_commit=latest_commit,
136+
baseline_commit=baseline_commit,
137+
commits=qualified_commits,
138+
generator_version=generator_version,
139+
is_monorepo=is_monorepo,
140+
)
141+
142+
143+
def __filter_qualified_commit(paths: Dict[str, str], commit: Commit) -> (Commit, str):
144+
"""
145+
Returns a tuple of a commit and libray_name.
146+
A qualified commit means at least one file changes in that commit is
147+
within the versioned proto_path in paths.
148+
149+
:param paths: a mapping from versioned proto_path to library_name.
150+
:param commit: a commit under consideration.
151+
:return: a tuple of a commit and library_name if the commit is
152+
qualified; otherwise an empty tuple.
153+
"""
154+
for file in commit.stats.files.keys():
155+
versioned_proto_path = find_versioned_proto_path(file)
156+
if versioned_proto_path in paths:
157+
return commit, paths[versioned_proto_path]
158+
return ()
159+
160+
161+
def __combine_commit_messages(
162+
latest_commit: str,
163+
baseline_commit: str,
164+
commits: Dict[Commit, str],
165+
generator_version: str,
166+
is_monorepo: bool,
167+
) -> str:
168+
messages = [
169+
f"This pull request is generated with proto changes between googleapis commit {baseline_commit} (exclusive) and {latest_commit} (inclusive).",
170+
"Qualified commits are:",
171+
]
172+
for commit in commits:
173+
short_sha = commit.hexsha[:7]
174+
messages.append(
175+
f"[googleapis/googleapis@{short_sha}](https://github.com/googleapis/googleapis/commit/{commit.hexsha})"
176+
)
177+
178+
messages.extend(format_commit_message(commits=commits, is_monorepo=is_monorepo))
179+
messages.extend(
180+
wrap_nested_commit(
181+
[
182+
f"feat: Regenerate with the Java code generator (gapic-generator-java) v{generator_version}"
183+
]
184+
)
185+
)
186+
187+
return "\n".join(messages)
188+
189+
190+
if __name__ == "__main__":
191+
main()

library_generation/test/integration_tests.py

+35-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import unittest
1818
from distutils.dir_util import copy_tree
1919
from distutils.file_util import copy_file
20+
from filecmp import cmp
2021
from filecmp import dircmp
2122

2223
from git import Repo
2324
from pathlib import Path
2425
from typing import List
25-
from typing import Dict
26+
27+
from library_generation.generate_pr_description import generate_pr_descriptions
2628
from library_generation.generate_repo import generate_from_yaml
2729
from library_generation.model.generation_config import from_yaml, GenerationConfig
2830
from library_generation.test.compare_poms import compare_xml
@@ -49,6 +51,35 @@
4951

5052

5153
class IntegrationTest(unittest.TestCase):
54+
def test_get_commit_message_success(self):
55+
repo_url = "https://github.com/googleapis/googleapis.git"
56+
config_files = self.__get_config_files(config_dir)
57+
monorepo_baseline_commit = "a17d4caf184b050d50cacf2b0d579ce72c31ce74"
58+
split_repo_baseline_commit = "679060c64136e85b52838f53cfe612ce51e60d1d"
59+
for repo, config_file in config_files:
60+
baseline_commit = (
61+
monorepo_baseline_commit
62+
if repo == "google-cloud-java"
63+
else split_repo_baseline_commit
64+
)
65+
description = generate_pr_descriptions(
66+
generation_config_yaml=config_file,
67+
repo_url=repo_url,
68+
baseline_commit=baseline_commit,
69+
)
70+
description_file = f"{config_dir}/{repo}/pr-description.txt"
71+
if os.path.isfile(f"{description_file}"):
72+
os.remove(f"{description_file}")
73+
with open(f"{description_file}", "w+") as f:
74+
f.write(description)
75+
self.assertTrue(
76+
cmp(
77+
f"{config_dir}/{repo}/pr-description-golden.txt",
78+
f"{description_file}",
79+
)
80+
)
81+
os.remove(f"{description_file}")
82+
5283
def test_generate_repo(self):
5384
shutil.rmtree(f"{golden_dir}", ignore_errors=True)
5485
os.makedirs(f"{golden_dir}", exist_ok=True)
@@ -150,7 +181,7 @@ def __pull_repo_to(cls, default_dest: Path, repo: str, committish: str) -> str:
150181
repo = Repo(dest)
151182
else:
152183
dest = default_dest
153-
repo_dest = f"{golden_dir}/{repo}"
184+
shutil.rmtree(dest, ignore_errors=True)
154185
repo_url = f"{repo_prefix}/{repo}"
155186
print(f"Cloning repository {repo_url}")
156187
repo = Repo.clone_from(repo_url, dest)
@@ -169,6 +200,8 @@ def __get_library_names_from_config(cls, config: GenerationConfig) -> List[str]:
169200
def __get_config_files(cls, path: str) -> List[tuple[str, str]]:
170201
config_files = []
171202
for sub_dir in Path(path).resolve().iterdir():
203+
if sub_dir.is_file():
204+
continue
172205
repo = sub_dir.name
173206
if repo == "golden":
174207
continue

library_generation/test/resources/integration/google-cloud-java/generation_config.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,17 @@ libraries:
4848
- proto_path: google/cloud/alloydb/connectors/v1
4949
- proto_path: google/cloud/alloydb/connectors/v1alpha
5050
- proto_path: google/cloud/alloydb/connectors/v1beta
51+
52+
- api_shortname: documentai
53+
name_pretty: Document AI
54+
product_documentation: https://cloud.google.com/compute/docs/documentai/
55+
api_description: allows developers to unlock insights from your documents with machine
56+
learning.
57+
library_name: document-ai
58+
release_level: stable
59+
issue_tracker: https://issuetracker.google.com/savedsearches/559755
60+
GAPICs:
61+
- proto_path: google/cloud/documentai/v1
62+
- proto_path: google/cloud/documentai/v1beta1
63+
- proto_path: google/cloud/documentai/v1beta2
64+
- proto_path: google/cloud/documentai/v1beta3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
This pull request is generated with proto changes between googleapis commit a17d4caf184b050d50cacf2b0d579ce72c31ce74 (exclusive) and 1a45bf7393b52407188c82e63101db7dc9c72026 (inclusive).
2+
Qualified commits are:
3+
[googleapis/googleapis@7a9a855](https://github.com/googleapis/googleapis/commit/7a9a855287b5042410c93e5a510f40efd4ce6cb1)
4+
[googleapis/googleapis@c7fd8bd](https://github.com/googleapis/googleapis/commit/c7fd8bd652ac690ca84f485014f70b52eef7cb9e)
5+
BEGIN_NESTED_COMMIT
6+
feat: [document-ai] expose model_type in v1 processor, so that user can see the model_type after get or list processor version
7+
8+
PiperOrigin-RevId: 603727585
9+
10+
END_NESTED_COMMIT
11+
BEGIN_NESTED_COMMIT
12+
feat: [document-ai] add model_type in v1beta3 processor proto
13+
14+
PiperOrigin-RevId: 603726122
15+
16+
END_NESTED_COMMIT
17+
BEGIN_NESTED_COMMIT
18+
feat: Regenerate with the Java code generator (gapic-generator-java) v2.34.0
19+
END_NESTED_COMMIT
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
This pull request is generated with proto changes between googleapis commit 679060c64136e85b52838f53cfe612ce51e60d1d (exclusive) and fc3043ebe12fb6bc1729c175e1526c859ce751d8 (inclusive).
2+
Qualified commits are:
3+
[googleapis/googleapis@fbcfef0](https://github.com/googleapis/googleapis/commit/fbcfef09510b842774530989889ed1584a8b5acb)
4+
[googleapis/googleapis@63d2a60](https://github.com/googleapis/googleapis/commit/63d2a60056ad5b156c05c7fb13138fc886c3b739)
5+
BEGIN_NESTED_COMMIT
6+
fix: extend timeouts for deleting snapshots, backups and tables
7+
8+
PiperOrigin-RevId: 605388988
9+
10+
END_NESTED_COMMIT
11+
BEGIN_NESTED_COMMIT
12+
chore: update retry settings for backup rpcs
13+
14+
PiperOrigin-RevId: 605367937
15+
16+
END_NESTED_COMMIT
17+
BEGIN_NESTED_COMMIT
18+
feat: Regenerate with the Java code generator (gapic-generator-java) v2.35.0
19+
END_NESTED_COMMIT

library_generation/test/unit_tests.py

+33
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import contextlib
2323
from pathlib import Path
2424
from difflib import unified_diff
25+
2526
from typing import List
2627
from parameterized import parameterized
2728
from library_generation import utilities as util
@@ -30,6 +31,8 @@
3031
from library_generation.model.gapic_inputs import parse as parse_build_file
3132
from library_generation.model.generation_config import from_yaml
3233
from library_generation.model.library_config import LibraryConfig
34+
from library_generation.utilities import find_versioned_proto_path
35+
from library_generation.utilities import get_file_paths
3336

3437
script_dir = os.path.dirname(os.path.realpath(__file__))
3538
resources_dir = os.path.join(script_dir, "resources")
@@ -214,6 +217,36 @@ def test_from_yaml_succeeds(self):
214217
self.assertEqual("google/cloud/asset/v1p5beta1", gapics[3].proto_path)
215218
self.assertEqual("google/cloud/asset/v1p7beta1", gapics[4].proto_path)
216219

220+
def test_get_file_paths_from_yaml_success(self):
221+
paths = get_file_paths(from_yaml(f"{test_config_dir}/generation_config.yaml"))
222+
self.assertEqual(
223+
{
224+
"google/cloud/asset/v1": "asset",
225+
"google/cloud/asset/v1p1beta1": "asset",
226+
"google/cloud/asset/v1p2beta1": "asset",
227+
"google/cloud/asset/v1p5beta1": "asset",
228+
"google/cloud/asset/v1p7beta1": "asset",
229+
},
230+
paths,
231+
)
232+
233+
@parameterized.expand(
234+
[
235+
(
236+
"google/cloud/aiplatform/v1/schema/predict/params/image_classification.proto",
237+
"google/cloud/aiplatform/v1",
238+
),
239+
(
240+
"google/cloud/asset/v1p2beta1/assets.proto",
241+
"google/cloud/asset/v1p2beta1",
242+
),
243+
("google/type/color.proto", "google/type/color.proto"),
244+
]
245+
)
246+
def test_find_versioned_proto_path(self, file_path, expected):
247+
proto_path = find_versioned_proto_path(file_path)
248+
self.assertEqual(expected, proto_path)
249+
217250
@parameterized.expand(
218251
[
219252
("BUILD_no_additional_protos.bazel", " "),

library_generation/test/utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)