|  | # Copyright 2022 The Bazel 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. | 
|  |  | 
|  | """Script to generate release notes.""" | 
|  |  | 
|  | import re | 
|  | import subprocess | 
|  |  | 
|  | import requests | 
|  |  | 
|  |  | 
|  | def get_last_release(): | 
|  | """Discovers the last stable release name from GitHub.""" | 
|  | response = requests.get("https://github.com/bazelbuild/bazel/releases/latest") | 
|  | return response.url.split("/")[-1] | 
|  |  | 
|  |  | 
|  | def git(*args): | 
|  | """Runs git as a subprocess, and returns its stdout as a list of lines.""" | 
|  | return subprocess.check_output(["git"] + | 
|  | list(args)).decode("utf-8").strip().split("\n") | 
|  |  | 
|  |  | 
|  | def extract_relnotes(commit_message_lines): | 
|  | """Extracts relnotes from a commit message (passed in as a list of lines).""" | 
|  | relnote_lines = [] | 
|  | in_relnote = False | 
|  | for line in commit_message_lines: | 
|  | if not line or line.startswith("PiperOrigin-RevId:"): | 
|  | in_relnote = False | 
|  | m = re.match(r"^RELNOTES(?:\[(INC|NEW)\])?:", line) | 
|  | if m is not None: | 
|  | in_relnote = True | 
|  | line = line[len(m[0]):] | 
|  | if m[1] == "INC": | 
|  | line = "**[Incompatible]** " + line.strip() | 
|  | line = line.strip() | 
|  | if in_relnote and line: | 
|  | relnote_lines.append(line) | 
|  | relnote = " ".join(relnote_lines) | 
|  | relnote_lower = relnote.strip().lower().rstrip(".") | 
|  | if relnote_lower == "n/a" or relnote_lower == "none": | 
|  | return None | 
|  | return relnote | 
|  |  | 
|  |  | 
|  | def get_relnotes_between(base, head): | 
|  | """Gets all relnotes for commits between `base` and `head`.""" | 
|  | commits = git("rev-list", f"{base}..{head}", "--grep=RELNOTES") | 
|  | relnotes = [] | 
|  | rolled_back_commits = set() | 
|  | # We go in reverse-chronological order, so that we can identify rollback | 
|  | # commits and ignore the rolled-back commits. | 
|  | for commit in commits: | 
|  | if commit in rolled_back_commits: | 
|  | continue | 
|  | lines = git("show", "-s", commit, "--pretty=format:%B") | 
|  | m = re.match(r"^Automated rollback of commit ([\dA-Fa-f]+)", lines[0]) | 
|  | if m is not None: | 
|  | rolled_back_commits.add(m[1]) | 
|  | # The rollback commit itself is also skipped. | 
|  | continue | 
|  | relnote = extract_relnotes(lines) | 
|  | if relnote is not None: | 
|  | relnotes.append(relnote) | 
|  | return relnotes | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | # Get the last stable release. | 
|  | last_release = get_last_release() | 
|  | print("last_release is", last_release) | 
|  | git("fetch", "origin", f"refs/tags/{last_release}:refs/tags/{last_release}") | 
|  |  | 
|  | # Assuming HEAD is on the current (to-be-released) release, find the merge | 
|  | # base with the last release so that we know which commits to generate notes | 
|  | # for. | 
|  | merge_base = git("merge-base", "HEAD", last_release)[0] | 
|  | print("merge base with", last_release, "is", merge_base) | 
|  |  | 
|  | # Generate notes for all commits from last branch cut to HEAD, but filter out | 
|  | # any identical notes from the previous release branch. | 
|  | cur_release_relnotes = get_relnotes_between(merge_base, "HEAD") | 
|  | last_release_relnotes = set(get_relnotes_between(merge_base, last_release)) | 
|  | filtered_relnotes = [ | 
|  | note for note in cur_release_relnotes if note not in last_release_relnotes | 
|  | ] | 
|  |  | 
|  | # Reverse so that the notes are in chronological order. | 
|  | filtered_relnotes.reverse() | 
|  | print() | 
|  | print() | 
|  | for note in filtered_relnotes: | 
|  | print("*", note) |