Skip to content

Commit e508ae6

Browse files
authoredJun 10, 2024··
feat: generate pr description from configuration comparison result (#2841)
In this PR: - Generate pull request description from configuration comparison result. - The pull request description contains repo-level changes, if applicable, and googleapis commit from baseline config (exclusive) to current config (inclusive). An example of pr description with repo-level change: ``` This pull request is generated with proto changes between [googleapis/googleapis@3b6f144](googleapis/googleapis@3b6f144) (exclusive) and [googleapis/googleapis@0cea717](googleapis/googleapis@0cea717) (inclusive). BEGIN_COMMIT_OVERRIDE BEGIN_NESTED_COMMIT fix(deps): update the Java code generator (gapic-generator-java) to 1.2.3 END_NESTED_COMMIT BEGIN_NESTED_COMMIT chore: update the libraries_bom version to 2.3.4 END_NESTED_COMMIT BEGIN_NESTED_COMMIT feat: Make Layout Parser generally available in V1 PiperOrigin-RevId: 638924855 Source Link: [googleapis/googleapis@0cea717](googleapis/googleapis@0cea717) END_NESTED_COMMIT END_COMMIT_OVERRIDE ```
1 parent 6c5d6ce commit e508ae6

8 files changed

+262
-85
lines changed
 

‎.github/workflows/hermetic_library_generation.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
-f .cloudbuild/library_generation/library_generation.Dockerfile \
3535
-t gcr.io/cloud-devrel-public-resources/java-library-generation:latest \
3636
.
37-
- name: Install gapic-generator-java-pom-parent
37+
- name: Install all modules
3838
shell: bash
3939
run: |
4040
mvn -V -B -ntp clean install -DskipTests

‎library_generation/cli/entry_point.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,7 @@ def generate(
135135
target_library_names=config_change.get_changed_libraries(),
136136
)
137137
generate_pr_descriptions(
138-
config=config_change.current_config,
139-
baseline_commit=config_change.baseline_config.googleapis_commitish,
138+
config_change=config_change,
140139
description_path=repository_path,
141140
)
142141

‎library_generation/generate_pr_description.py

+35-33
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
import shutil
1818
from typing import Dict
1919
from git import Commit, Repo
20-
from library_generation.model.generation_config import GenerationConfig
20+
21+
from library_generation.model.config_change import ConfigChange
2122
from library_generation.utils.proto_path_utils import find_versioned_proto_path
2223
from library_generation.utils.commit_message_formatter import (
2324
format_commit_message,
25+
format_repo_level_change,
2426
commit_link,
2527
)
2628
from library_generation.utils.commit_message_formatter import wrap_override_commit
@@ -29,42 +31,38 @@
2931

3032

3133
def generate_pr_descriptions(
32-
config: GenerationConfig,
33-
baseline_commit: str,
34+
config_change: ConfigChange,
3435
description_path: str,
3536
repo_url: str = "https://github.com/googleapis/googleapis.git",
3637
) -> None:
3738
"""
38-
Generate pull request description from baseline_commit (exclusive) to the
39-
googleapis commit (inclusive) in the given generation config.
39+
Generate pull request description from configuration comparison result.
40+
41+
The pull request description contains repo-level changes, if applicable,
42+
and googleapis commit from baseline config (exclusive) to current config
43+
(inclusive).
4044
4145
The pull request description will be generated into
4246
description_path/pr_description.txt.
4347
44-
If baseline_commit is the same as googleapis commit in the given generation
45-
config, no pr_description.txt will be generated.
48+
No pr_description.txt will be generated if no changes in the configurations.
4649
47-
:param config: a GenerationConfig object. The googleapis commit in this
48-
configuration is the latest commit, inclusively, from which the commit
49-
message is considered.
50-
:param baseline_commit: The baseline (oldest) commit, exclusively, from
51-
which the commit message is considered. This commit should be an ancestor
52-
of googleapis commit in configuration.
50+
:param config_change: a ConfigChange object, containing changes in baseline
51+
and current generation configurations.
5352
:param description_path: the path to which the pull request description
5453
file goes.
5554
:param repo_url: the GitHub repository from which retrieves the commit
5655
history.
5756
"""
58-
if baseline_commit == config.googleapis_commitish:
59-
return
60-
61-
paths = config.get_proto_path_to_library_name()
62-
description = get_commit_messages(
57+
repo_level_message = format_repo_level_change(config_change)
58+
paths = config_change.current_config.get_proto_path_to_library_name()
59+
description = get_repo_level_commit_messages(
6360
repo_url=repo_url,
64-
current_commit_sha=config.googleapis_commitish,
65-
baseline_commit_sha=baseline_commit,
61+
current_commit_sha=config_change.current_config.googleapis_commitish,
62+
baseline_commit_sha=config_change.baseline_config.googleapis_commitish,
6663
paths=paths,
67-
is_monorepo=config.is_monorepo(),
64+
is_monorepo=config_change.current_config.is_monorepo(),
65+
repo_level_message=repo_level_message,
6866
)
6967

7068
if description == EMPTY_MESSAGE:
@@ -77,12 +75,13 @@ def generate_pr_descriptions(
7775
f.write(description)
7876

7977

80-
def get_commit_messages(
78+
def get_repo_level_commit_messages(
8179
repo_url: str,
8280
current_commit_sha: str,
8381
baseline_commit_sha: str,
8482
paths: Dict[str, str],
8583
is_monorepo: bool,
84+
repo_level_message: list[str] = None,
8685
) -> str:
8786
"""
8887
Combine commit messages of a repository from latest_commit to
@@ -97,10 +96,13 @@ def get_commit_messages(
9796
selecting commit message. This commit should be an ancestor of
9897
:param paths: a mapping from file paths to library_name.
9998
:param is_monorepo: whether to generate commit messages in a monorepo.
99+
:param repo_level_message: commit messages regarding repo-level changes.
100100
:return: commit messages.
101101
:raise ValueError: if current_commit is older than or equal to
102102
baseline_commit.
103103
"""
104+
if current_commit_sha == baseline_commit_sha:
105+
return EMPTY_MESSAGE
104106
tmp_dir = "/tmp/repo"
105107
shutil.rmtree(tmp_dir, ignore_errors=True)
106108
os.mkdir(tmp_dir)
@@ -134,6 +136,7 @@ def get_commit_messages(
134136
baseline_commit=baseline_commit,
135137
commits=qualified_commits,
136138
is_monorepo=is_monorepo,
139+
repo_level_message=repo_level_message,
137140
)
138141

139142

@@ -160,20 +163,19 @@ def __combine_commit_messages(
160163
baseline_commit: Commit,
161164
commits: Dict[Commit, str],
162165
is_monorepo: bool,
166+
repo_level_message: list[str],
163167
) -> str:
164-
messages = [
165-
f"This pull request is generated with proto changes between {commit_link(baseline_commit)} (exclusive) "
166-
f"and {commit_link(current_commit)} (inclusive).",
167-
"",
168+
description = [
169+
f"This pull request is generated with proto changes between "
170+
f"{commit_link(baseline_commit)} (exclusive) "
171+
f"and {commit_link(current_commit)} (inclusive).\n",
168172
]
169-
170-
messages.extend(
171-
wrap_override_commit(
172-
format_commit_message(commits=commits, is_monorepo=is_monorepo)
173-
)
173+
commit_message = repo_level_message
174+
commit_message.extend(
175+
format_commit_message(commits=commits, is_monorepo=is_monorepo)
174176
)
175-
176-
return "\n".join(messages)
177+
description.extend(wrap_override_commit(commit_message))
178+
return "\n".join(description)
177179

178180

179181
def __get_commit_timestamp(commit: Commit) -> int:

‎library_generation/model/generation_config.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
LIBRARY_LEVEL_PARAMETER = "Library level parameter"
2222
GAPIC_LEVEL_PARAMETER = "GAPIC level parameter"
2323
COMMON_PROTOS_LIBRARY_NAME = "common-protos"
24+
GAPIC_GENERATOR_VERSION = "gapic_generator_version"
25+
LIBRARIES_BOM_VERSION = "libraries_bom_version"
2426

2527

2628
class GenerationConfig:
@@ -144,14 +146,14 @@ def from_yaml(path_to_yaml: str) -> GenerationConfig:
144146

145147
parsed_config = GenerationConfig(
146148
gapic_generator_version=__required(
147-
config, "gapic_generator_version", REPO_LEVEL_PARAMETER
149+
config, GAPIC_GENERATOR_VERSION, REPO_LEVEL_PARAMETER
148150
),
149151
googleapis_commitish=__required(
150152
config, "googleapis_commitish", REPO_LEVEL_PARAMETER
151153
),
152154
grpc_version=__optional(config, "grpc_version", None),
153155
protoc_version=__optional(config, "protoc_version", None),
154-
libraries_bom_version=__optional(config, "libraries_bom_version", None),
156+
libraries_bom_version=__optional(config, LIBRARIES_BOM_VERSION, None),
155157
libraries=parsed_libraries,
156158
)
157159

‎library_generation/test/generate_pr_description_unit_tests.py

+110-34
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@
1313
# limitations under the License.
1414
import os
1515
import unittest
16+
from filecmp import cmp
1617

1718
from library_generation.generate_pr_description import (
18-
get_commit_messages,
19+
get_repo_level_commit_messages,
1920
generate_pr_descriptions,
2021
)
22+
from library_generation.model.config_change import (
23+
ConfigChange,
24+
ChangeType,
25+
LibraryChange,
26+
)
27+
from library_generation.model.gapic_config import GapicConfig
2128
from library_generation.model.generation_config import GenerationConfig
29+
from library_generation.model.library_config import LibraryConfig
30+
31+
script_dir = os.path.dirname(os.path.realpath(__file__))
32+
resources_dir = os.path.join(script_dir, "resources", "goldens")
2233

2334

2435
class GeneratePrDescriptionTest(unittest.TestCase):
@@ -30,44 +41,51 @@ def test_get_commit_messages_current_is_older_raise_exception(self):
3041
self.assertRaisesRegex(
3142
ValueError,
3243
"newer than",
33-
get_commit_messages,
44+
get_repo_level_commit_messages,
3445
"https://github.com/googleapis/googleapis.git",
3546
current_commit,
3647
baseline_commit,
3748
{},
3849
True,
3950
)
4051

41-
def test_get_commit_messages_current_and_baseline_are_same_raise_exception(self):
52+
def test_get_commit_messages_with_same_current_and_baseline_returns_empty_message(
53+
self,
54+
):
4255
# committed on April 1st, 2024
4356
current_commit = "36441693dddaf0ed73951ad3a15c215a332756aa"
4457
baseline_commit = "36441693dddaf0ed73951ad3a15c215a332756aa"
45-
self.assertRaisesRegex(
46-
ValueError,
47-
"newer than",
48-
get_commit_messages,
49-
"https://github.com/googleapis/googleapis.git",
50-
current_commit,
51-
baseline_commit,
52-
{},
53-
True,
58+
self.assertEqual(
59+
"",
60+
get_repo_level_commit_messages(
61+
"https://github.com/googleapis/googleapis.git",
62+
current_commit,
63+
baseline_commit,
64+
{},
65+
True,
66+
),
5467
)
5568

56-
def test_generate_pr_description_with_same_googleapis_commits(self):
69+
def test_generate_pr_description_with_no_change_in_config(self):
5770
commit_sha = "36441693dddaf0ed73951ad3a15c215a332756aa"
58-
cwd = os.getcwd()
71+
config = GenerationConfig(
72+
gapic_generator_version="",
73+
googleapis_commitish=commit_sha,
74+
libraries_bom_version="",
75+
# use empty libraries to make sure no qualified commit between
76+
# two commit sha.
77+
libraries=[],
78+
)
79+
pr_description_path = os.path.join(os.getcwd(), "no_config_change")
5980
generate_pr_descriptions(
60-
config=GenerationConfig(
61-
gapic_generator_version="",
62-
googleapis_commitish=commit_sha,
63-
grpc_version="",
64-
protoc_version="",
65-
libraries=[],
81+
config_change=ConfigChange(
82+
change_to_libraries={},
83+
baseline_config=config,
84+
current_config=config,
6685
),
67-
baseline_commit=commit_sha,
68-
description_path=cwd,
86+
description_path=pr_description_path,
6987
)
70-
self.assertFalse(os.path.isfile(f"{cwd}/pr_description.txt"))
88+
self.assertFalse(os.path.isfile(f"{pr_description_path}/pr_description.txt"))
7189

7290
def test_generate_pr_description_does_not_create_pr_description_without_qualified_commit(
7391
self,
@@ -76,19 +94,77 @@ def test_generate_pr_description_does_not_create_pr_description_without_qualifie
7694
old_commit_sha = "30717c0b0c9966906880703208a4c820411565c4"
7795
# committed on May 23rd, 2024
7896
new_commit_sha = "eeed69d446a90eb4a4a2d1762c49d637075390c1"
97+
pr_description_path = os.path.join(os.getcwd(), "no_qualified_commit")
98+
generate_pr_descriptions(
99+
config_change=ConfigChange(
100+
change_to_libraries={},
101+
baseline_config=GenerationConfig(
102+
gapic_generator_version="",
103+
googleapis_commitish=old_commit_sha,
104+
# use empty libraries to make sure no qualified commit between
105+
# two commit sha.
106+
libraries=[],
107+
),
108+
current_config=GenerationConfig(
109+
gapic_generator_version="",
110+
googleapis_commitish=new_commit_sha,
111+
# use empty libraries to make sure no qualified commit between
112+
# two commit sha.
113+
libraries=[],
114+
),
115+
),
116+
description_path=pr_description_path,
117+
)
118+
self.assertFalse(os.path.isfile(f"{pr_description_path}/pr_description.txt"))
119+
120+
def test_generate_pr_description_with_combined_message(
121+
self,
122+
):
123+
# no other commits between these two commits.
124+
baseline_commit_sha = "3b6f144d47b0a1d2115ab2445ec06e80cc324a44"
125+
documentai_commit_sha = "0cea7170404bec3d994f43db4fa292f5034cbe9a"
79126
cwd = os.getcwd()
127+
library = LibraryConfig(
128+
api_shortname="documentai",
129+
api_description="",
130+
name_pretty="",
131+
product_documentation="",
132+
gapic_configs=[GapicConfig(proto_path="google/cloud/documentai/v1")],
133+
)
80134
generate_pr_descriptions(
81-
config=GenerationConfig(
82-
gapic_generator_version="",
83-
googleapis_commitish=new_commit_sha,
84-
libraries_bom_version="",
85-
grpc_version="",
86-
protoc_version="",
87-
# use empty libraries to make sure no qualified commit between
88-
# two commit sha.
89-
libraries=[],
135+
config_change=ConfigChange(
136+
change_to_libraries={
137+
ChangeType.REPO_LEVEL_CHANGE: [
138+
LibraryChange(
139+
changed_param="gapic_generator_version",
140+
current_value="1.2.3",
141+
),
142+
LibraryChange(
143+
changed_param="libraries_bom_version", current_value="2.3.4"
144+
),
145+
],
146+
ChangeType.GOOGLEAPIS_COMMIT: [],
147+
},
148+
baseline_config=GenerationConfig(
149+
gapic_generator_version="",
150+
googleapis_commitish=baseline_commit_sha,
151+
libraries=[library],
152+
),
153+
current_config=GenerationConfig(
154+
gapic_generator_version="1.2.3",
155+
googleapis_commitish=documentai_commit_sha,
156+
libraries_bom_version="2.3.4",
157+
libraries=[library],
158+
),
90159
),
91-
baseline_commit=old_commit_sha,
92160
description_path=cwd,
93161
)
94-
self.assertFalse(os.path.isfile(f"{cwd}/pr_description.txt"))
162+
self.assertTrue(os.path.isfile(f"{cwd}/pr_description.txt"))
163+
self.assertTrue(
164+
cmp(
165+
f"{resources_dir}/pr_description-golden.txt",
166+
f"{cwd}/pr_description.txt",
167+
),
168+
"The generated PR description does not match the expected golden file",
169+
)
170+
os.remove(f"{cwd}/pr_description.txt")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
This pull request is generated with proto changes between [googleapis/googleapis@3b6f144](https://github.com/googleapis/googleapis/commit/3b6f144d47b0a1d2115ab2445ec06e80cc324a44) (exclusive) and [googleapis/googleapis@0cea717](https://github.com/googleapis/googleapis/commit/0cea7170404bec3d994f43db4fa292f5034cbe9a) (inclusive).
2+
3+
BEGIN_COMMIT_OVERRIDE
4+
BEGIN_NESTED_COMMIT
5+
fix(deps): update the Java code generator (gapic-generator-java) to 1.2.3
6+
END_NESTED_COMMIT
7+
BEGIN_NESTED_COMMIT
8+
chore: update the libraries_bom version to 2.3.4
9+
END_NESTED_COMMIT
10+
BEGIN_NESTED_COMMIT
11+
feat: Make Layout Parser generally available in V1
12+
13+
PiperOrigin-RevId: 638924855
14+
15+
Source Link: [googleapis/googleapis@0cea717](https://github.com/googleapis/googleapis/commit/0cea7170404bec3d994f43db4fa292f5034cbe9a)
16+
END_NESTED_COMMIT
17+
END_COMMIT_OVERRIDE

‎library_generation/test/utils/commit_message_formatter_unit_tests.py

+46-2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,24 @@
1414
import unittest
1515
from unittest.mock import patch
1616

17+
from library_generation.model.config_change import (
18+
ConfigChange,
19+
ChangeType,
20+
LibraryChange,
21+
)
22+
from library_generation.model.generation_config import GenerationConfig
1723
from library_generation.utils.commit_message_formatter import (
1824
format_commit_message,
1925
commit_link,
26+
format_repo_level_change,
2027
)
21-
from library_generation.utils.commit_message_formatter import wrap_nested_commit
28+
from library_generation.utils.commit_message_formatter import wrap_googleapis_commit
2229
from library_generation.utils.commit_message_formatter import wrap_override_commit
2330

31+
gen_config = GenerationConfig(
32+
gapic_generator_version="1.2.3", googleapis_commitish="123abc", libraries=[]
33+
)
34+
2435

2536
class CommitMessageFormatterTest(unittest.TestCase):
2637
def test_format_commit_message_should_add_library_name_for_conventional_commit(
@@ -130,7 +141,7 @@ def test_wrap_nested_commit_success(self):
130141
"Source Link: [googleapis/googleapis@1234567](https://github.com/googleapis/googleapis/commit/1234567abcdefg)",
131142
"END_NESTED_COMMIT",
132143
],
133-
wrap_nested_commit(commit, messages),
144+
wrap_googleapis_commit(commit, messages),
134145
)
135146

136147
def test_wrap_override_commit_success(self):
@@ -153,3 +164,36 @@ def test_commit_link_success(self):
153164
"[googleapis/googleapis@1234567](https://github.com/googleapis/googleapis/commit/1234567abcdefg)",
154165
commit_link(commit),
155166
)
167+
168+
def test_format_repo_level_change_success(self):
169+
config_change = ConfigChange(
170+
change_to_libraries={
171+
ChangeType.REPO_LEVEL_CHANGE: [
172+
LibraryChange(
173+
changed_param="gapic_generator_version", current_value="1.2.3"
174+
),
175+
LibraryChange(
176+
changed_param="libraries_bom_version", current_value="2.3.4"
177+
),
178+
LibraryChange(
179+
changed_param="protoc_version", current_value="3.4.5"
180+
),
181+
]
182+
},
183+
baseline_config=gen_config,
184+
current_config=gen_config,
185+
)
186+
self.assertEqual(
187+
[
188+
"BEGIN_NESTED_COMMIT",
189+
"fix(deps): update the Java code generator (gapic-generator-java) to 1.2.3",
190+
"END_NESTED_COMMIT",
191+
"BEGIN_NESTED_COMMIT",
192+
"chore: update the libraries_bom version to 2.3.4",
193+
"END_NESTED_COMMIT",
194+
"BEGIN_NESTED_COMMIT",
195+
"chore: update repo-level parameter protoc_version to 3.4.5",
196+
"END_NESTED_COMMIT",
197+
],
198+
format_repo_level_change(config_change),
199+
)

‎library_generation/utils/commit_message_formatter.py

+48-11
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import re
15-
from typing import List
16-
from typing import Dict
1715
from git import Commit
1816

17+
from library_generation.model.config_change import ConfigChange, ChangeType
18+
from library_generation.model.generation_config import (
19+
GAPIC_GENERATOR_VERSION,
20+
LIBRARIES_BOM_VERSION,
21+
)
1922

20-
def format_commit_message(commits: Dict[Commit, str], is_monorepo: bool) -> List[str]:
23+
PARAM_TO_COMMIT_MESSAGE = {
24+
GAPIC_GENERATOR_VERSION: "fix(deps): update the Java code generator (gapic-generator-java) to",
25+
LIBRARIES_BOM_VERSION: "chore: update the libraries_bom version to",
26+
}
27+
28+
29+
def format_commit_message(commits: dict[Commit, str], is_monorepo: bool) -> list[str]:
2130
"""
2231
Format commit messages. Add library_name to conventional commit messages if
2332
is_monorepo is True; otherwise no op.
@@ -47,26 +56,41 @@ def format_commit_message(commits: Dict[Commit, str], is_monorepo: bool) -> List
4756
messages.append(formatted_message)
4857
else:
4958
messages.append(message_line)
50-
all_commits.extend(wrap_nested_commit(commit, messages))
59+
all_commits.extend(wrap_googleapis_commit(commit, messages))
5160
return all_commits
5261

5362

54-
def wrap_nested_commit(commit: Commit, messages: List[str]) -> List[str]:
63+
def format_repo_level_change(config_change: ConfigChange) -> list[str]:
64+
"""
65+
Format commit messages regarding repo-level changes.
66+
67+
:param config_change:
68+
:return: commit messages regarding repo-level changes.
69+
"""
70+
messages = []
71+
for repo_level_change in config_change.change_to_libraries.get(
72+
ChangeType.REPO_LEVEL_CHANGE, []
73+
):
74+
message = f"chore: update repo-level parameter {repo_level_change.changed_param} to {repo_level_change.current_value}"
75+
if repo_level_change.changed_param in PARAM_TO_COMMIT_MESSAGE:
76+
message = f"{PARAM_TO_COMMIT_MESSAGE.get(repo_level_change.changed_param)} {repo_level_change.current_value}"
77+
messages.extend(__wrap_nested_commit([message]))
78+
return messages
79+
80+
81+
def wrap_googleapis_commit(commit: Commit, messages: list[str]) -> list[str]:
5582
"""
5683
Wrap message between `BEGIN_NESTED_COMMIT` and `BEGIN_NESTED_COMMIT`.
5784
5885
:param commit: a GitHub commit.
5986
:param messages: a (multi-line) commit message, one line per item.
6087
:return: wrapped messages.
6188
"""
62-
result = ["BEGIN_NESTED_COMMIT"]
63-
result.extend(messages)
64-
result.append(f"Source Link: {commit_link(commit)}")
65-
result.append("END_NESTED_COMMIT")
66-
return result
89+
messages.append(f"Source Link: {commit_link(commit)}")
90+
return __wrap_nested_commit(messages)
6791

6892

69-
def wrap_override_commit(messages: List[str]) -> List[str]:
93+
def wrap_override_commit(messages: list[str]) -> list[str]:
7094
"""
7195
Wrap message between `BEGIN_COMMIT_OVERRIDE` and `END_COMMIT_OVERRIDE`.
7296
@@ -88,3 +112,16 @@ def commit_link(commit: Commit) -> str:
88112
"""
89113
short_sha = commit.hexsha[:7]
90114
return f"[googleapis/googleapis@{short_sha}](https://github.com/googleapis/googleapis/commit/{commit.hexsha})"
115+
116+
117+
def __wrap_nested_commit(messages: list[str]) -> list[str]:
118+
"""
119+
Wrap message between `BEGIN_NESTED_COMMIT` and `BEGIN_NESTED_COMMIT`.
120+
121+
:param messages: a (multi-line) commit message, one line per item.
122+
:return: wrapped messages.
123+
"""
124+
result = ["BEGIN_NESTED_COMMIT"]
125+
result.extend(messages)
126+
result.append("END_NESTED_COMMIT")
127+
return result

0 commit comments

Comments
 (0)
Please sign in to comment.