Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 1 | #!/usr/bin/python3 |
| 2 | # |
| 3 | # Copyright (C) 2020 The Android Open Source Project |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
| 16 | # |
| 17 | |
| 18 | from ReleaseNoteMarkdown import * |
Nick Anthony | 2d9b73c | 2020-06-29 21:24:55 -0400 | [diff] [blame] | 19 | from GitClient import CommitType, getTitleFromCommitType, getChangeIdAOSPLink, getBuganizerLink |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 20 | |
| 21 | class CommitMarkdownList: |
| 22 | """Generates the markdown list of commits with sections defined by enum [CommitType], in the format: |
| 23 | |
| 24 | **New Features** |
| 25 | - <[Commit.summary]> <[getChangeIdAOSPLink]> <[getBuganizerLink] 1> <[getBuganizerLink] 2>... |
| 26 | **API Changes** |
| 27 | - <[Commit.summary]> <[getChangeIdAOSPLink]> <[getBuganizerLink] 1> <[getBuganizerLink] 2>... |
| 28 | **Bug Fixes** |
| 29 | - <[Commit.summary]> <[getChangeIdAOSPLink]> <[getBuganizerLink] 1> <[getBuganizerLink] 2>... |
| 30 | **External Contribution** |
| 31 | - <[Commit.summary]> <[getChangeIdAOSPLink]> <[getBuganizerLink] 1> <[getBuganizerLink] 2>... |
| 32 | """ |
| 33 | def __init__(self, commits=[], forceIncludeAllCommits=False): |
| 34 | self.forceIncludeAllCommits = forceIncludeAllCommits |
| 35 | self.commits = commits |
| 36 | |
| 37 | def add(self, commit): |
| 38 | self.commits.append(commit) |
| 39 | |
| 40 | def getListItemStr(self): |
| 41 | return "- " |
| 42 | |
Nick Anthony | 2d9b73c | 2020-06-29 21:24:55 -0400 | [diff] [blame] | 43 | def formatReleaseNoteString(self, commit): |
Nick Anthony | 4c1cad7 | 2020-06-07 18:26:37 -0400 | [diff] [blame] | 44 | if commit.releaseNote != "": |
| 45 | releaseNoteString = commit.releaseNote |
| 46 | else: |
| 47 | releaseNoteString = self.getListItemStr() + commit.summary |
| 48 | newLineCharCount = releaseNoteString.count("\n") |
| 49 | if releaseNoteString[-1] == "\n": |
| 50 | newLineCharCount = newLineCharCount - 1 |
| 51 | releaseNoteString = releaseNoteString.replace("\n", "\n ", newLineCharCount) |
| 52 | releaseNoteString += " (" + str(getChangeIdAOSPLink(commit.changeId)) |
| 53 | for bug in commit.bugs: |
| 54 | releaseNoteString += ", " + str(getBuganizerLink(bug)) |
| 55 | releaseNoteString += ")" |
| 56 | return self.getListItemStr() + releaseNoteString |
| 57 | |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 58 | def makeReleaseNotesSection(self, sectionCommitType): |
| 59 | sectionHeader = MarkdownBoldText(getTitleFromCommitType(sectionCommitType)) |
| 60 | markdownStringSection = "" |
| 61 | for commit in self.commits: |
| 62 | if commit.changeType != sectionCommitType: continue |
Nick Anthony | 4c1cad7 | 2020-06-07 18:26:37 -0400 | [diff] [blame] | 63 | commitString = self.formatReleaseNoteString(commit) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 64 | if self.forceIncludeAllCommits or commit.releaseNote != "": |
| 65 | markdownStringSection = markdownStringSection + commitString |
| 66 | if markdownStringSection[-1] != '\n': |
| 67 | markdownStringSection += '\n' |
Nick Anthony | 8f18f3d | 2020-05-28 20:15:56 -0400 | [diff] [blame] | 68 | markdownStringSection = "\n%s\n\n%s" % (sectionHeader, markdownStringSection) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 69 | return markdownStringSection |
| 70 | |
| 71 | def __str__(self): |
| 72 | markdownString = "" |
| 73 | for commitType in CommitType: |
| 74 | markdownString += self.makeReleaseNotesSection(commitType) |
| 75 | return markdownString |
| 76 | |
| 77 | class GitilesDiffLogLink(MarkdownLink): |
| 78 | def __str__(self): |
| 79 | return "[%s](%s)" % (self.linkText, self.linkUrl) |
| 80 | |
| 81 | def getGitilesDiffLogLink(version, startSHA, endSHA, projectDir): |
| 82 | """ |
| 83 | @param startSHA the SHA at which to start the diff log (exclusive) |
| 84 | @param endSHA the last SHA to include in the diff log (inclusive) |
| 85 | @param projectDir the local directory of the project, in relation to frameworks/support |
| 86 | |
| 87 | @return A [MarkdownLink] to the public Gitiles diff log |
| 88 | """ |
| 89 | baseGitilesUrl = "https://android.googlesource.com/platform/frameworks/support/+log/" |
| 90 | # The root project directory is already existent in the url path, so the directory here |
| 91 | # should be relative to frameworks/support/. |
| 92 | if ("frameworks/support" in projectDir): |
| 93 | print_e("Gitiles directory should be relative to frameworks/support; received incorrect directory: $projectDir") |
| 94 | exit(1) |
Nick Anthony | 917014b | 2020-05-08 08:24:09 -0400 | [diff] [blame] | 95 | if startSHA != "": |
| 96 | return GitilesDiffLogLink("Version %s contains these commits." % version, "%s%s..%s/%s" % (baseGitilesUrl, startSHA, endSHA, projectDir)) |
| 97 | else: |
| 98 | return GitilesDiffLogLink("Version %s contains these commits." % version, "%s%s/%s" % (baseGitilesUrl, endSHA, projectDir)) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 99 | |
| 100 | class LibraryHeader(MarkdownHeader): |
| 101 | """ |
| 102 | Markdown class for a Library Header in the format: |
| 103 | |
| 104 | ### Version <version> {:#<artifactIdTag-version>} |
| 105 | |
| 106 | An artifactId tag is required because artifactIds may be can be grouped by version, in which case the tag is not obvious |
| 107 | """ |
| 108 | def __init__(self, groupId="", version="", artifactIdTag=""): |
| 109 | self.markdownType = HeaderType.H3 |
| 110 | self.text = "%s Version %s {:#%s%s}" % (groupId, version, artifactIdTag, version) |
| 111 | |
Nick Anthony | 6526d3b | 2020-07-09 16:41:41 -0400 | [diff] [blame] | 112 | class ChannelSummaryItem: |
| 113 | """Generates the summary list item in the channel.md pages, which take the format: |
| 114 | |
| 115 | * [<Title> Version <version>](/jetpack/androidx/releases/<groupid>#<version>) |
| 116 | |
| 117 | where <header> is usually the GroupId. |
| 118 | """ |
| 119 | def __init__(self, formattedTitle, groupId, version, artifactIdTag=""): |
| 120 | self.text = "* [%s Version %s](/jetpack/androidx/releases/%s#%s%s)\n" % (formattedTitle, version, groupId.lower(), artifactIdTag, version) |
| 121 | |
| 122 | def __str__(self): |
| 123 | return self.text |
| 124 | |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 125 | class LibraryReleaseNotes: |
| 126 | """ Structured release notes class, that connects all parts of the release notes. Creates release |
| 127 | notes in the format: |
| 128 | <pre> |
| 129 | <[LibraryHeader]> |
| 130 | <Date> |
| 131 | |
| 132 | `androidx.<groupId>:<artifactId>:<version>` is released. The commits included in this version |
| 133 | can be found <[MarkdownLink]>. |
| 134 | |
| 135 | <[CommitMarkdownList]> |
| 136 | </pre> |
| 137 | """ |
| 138 | def __init__(self, groupId, artifactIds, version, releaseDate, fromSHA, untilSHA, projectDir, requiresSameVersion, commitList=[], forceIncludeAllCommits=False): |
| 139 | """ |
| 140 | @property groupId Library GroupId. |
| 141 | @property artifactIds List of ArtifactIds included in these release notes. |
| 142 | @property version Version of the library, assuming all artifactIds have the same version. |
| 143 | @property releaseDate Date the release will go live. Defaults to the current date. |
| 144 | @property fromSHA The oldest SHA to which to query for release notes. It will be |
| 145 | excluded from release notes, but the next newer SHA will be included. |
| 146 | @property untilSHA The newest SHA to be included in the release notes. |
| 147 | @property projectDir The filepath relative to the parent directory of the .git directory. |
| 148 | @property requiresSameVersion True if the groupId of this module requires the same version for |
| 149 | all artifactIds in the groupId. When true, uses the GroupId for the release notes |
| 150 | header. When false, uses the list of artifactIds for the header. |
| 151 | @property commitList The initial list of Commits to include in these release notes. Defaults to an |
| 152 | empty list. Users can always add more commits with [LibraryReleaseNotes.addCommit] |
| 153 | @param forceIncludeAllCommits Set to true to include all commits, both with and without a |
| 154 | release note field in the commit message. Defaults to false, which means only commits |
| 155 | with a release note field are included in the release notes. |
| 156 | """ |
| 157 | self.groupId = groupId |
| 158 | self.artifactIds = artifactIds |
| 159 | self.version = version |
Nick Anthony | c687a4b | 2020-04-23 07:59:13 -0400 | [diff] [blame] | 160 | self.releaseDate = MarkdownDate(releaseDate) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 161 | self.fromSHA = fromSHA |
| 162 | self.untilSHA = untilSHA |
| 163 | self.projectDir = projectDir |
| 164 | self.commitList = commitList |
| 165 | self.requiresSameVersion = requiresSameVersion |
| 166 | self.forceIncludeAllCommits = forceIncludeAllCommits |
| 167 | self.diffLogLink = MarkdownLink() |
| 168 | self.commits = commitList |
| 169 | self.commitMarkdownList = CommitMarkdownList(commitList, forceIncludeAllCommits) |
| 170 | self.summary = "" |
| 171 | self.bugsFixed = [] |
Nick Anthony | 6526d3b | 2020-07-09 16:41:41 -0400 | [diff] [blame] | 172 | self.channelSummary = None |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 173 | |
| 174 | if version == "" or groupId == "": |
| 175 | raise RuntimeError("Tried to create Library Release Notes Header without setting " + |
| 176 | "the groupId or version!") |
| 177 | if requiresSameVersion: |
Nick Anthony | 8f18f3d | 2020-05-28 20:15:56 -0400 | [diff] [blame] | 178 | formattedGroupId = groupId.replace("androidx.", "") |
| 179 | formattedGroupId = self.capitalizeTitle(formattedGroupId) |
| 180 | self.header = LibraryHeader(formattedGroupId, version) |
Nick Anthony | 6526d3b | 2020-07-09 16:41:41 -0400 | [diff] [blame] | 181 | self.channelSummary = ChannelSummaryItem(formattedGroupId, formattedGroupId, version) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 182 | else: |
Nick Anthony | 8f18f3d | 2020-05-28 20:15:56 -0400 | [diff] [blame] | 183 | artifactIdTag = artifactIds[0] + "-" if len(artifactIds) == 1 else "" |
| 184 | formattedArtifactIds = (" ".join(artifactIds)) |
| 185 | if len(artifactIds) > 3: |
| 186 | formattedArtifactIds = groupId.replace("androidx.", "") |
| 187 | formattedArtifactIds = self.capitalizeTitle(formattedArtifactIds) |
| 188 | self.header = LibraryHeader(formattedArtifactIds, version, artifactIdTag) |
Nick Anthony | 6526d3b | 2020-07-09 16:41:41 -0400 | [diff] [blame] | 189 | formattedGroupId = groupId.replace("androidx.", "") |
| 190 | self.channelSummary = ChannelSummaryItem(formattedArtifactIds, formattedGroupId, version, artifactIdTag) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 191 | self.diffLogLink = getGitilesDiffLogLink(version, fromSHA, untilSHA, projectDir) |
| 192 | |
| 193 | def getFormattedReleaseSummary(self): |
| 194 | numberArtifacts = len(self.artifactIds) |
| 195 | for i in range(0, numberArtifacts): |
| 196 | currentArtifactId = self.artifactIds[i] |
| 197 | if numberArtifacts == 1: |
| 198 | self.summary = "`%s:%s:%s` is released. " % (self.groupId, currentArtifactId, self.version) |
| 199 | elif numberArtifacts == 2: |
| 200 | if i == 0: self.summary = "`%s:%s:%s` and " % (self.groupId, currentArtifactId, self.version) |
| 201 | if i == 1: self.summary += "`%s:%s:%s` are released. " % (self.groupId, currentArtifactId, self.version) |
Nick Anthony | 8f18f3d | 2020-05-28 20:15:56 -0400 | [diff] [blame] | 202 | elif numberArtifacts == 3: |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 203 | if (i < numberArtifacts - 1): |
| 204 | self.summary += "`%s:%s:%s`, " % (self.groupId, currentArtifactId, self.version) |
| 205 | else: |
| 206 | self.summary += "and `%s:%s:%s` are released. " % (self.groupId, currentArtifactId, self.version) |
Nick Anthony | 8f18f3d | 2020-05-28 20:15:56 -0400 | [diff] [blame] | 207 | else: |
| 208 | commonArtifactIdSubstring = self.artifactIds[0].split('-')[0] |
| 209 | self.summary = "`%s:%s-*:%s` is released. " % ( |
| 210 | self.groupId, |
| 211 | commonArtifactIdSubstring, |
| 212 | self.version |
| 213 | ) |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 214 | self.summary += "%s\n" % self.diffLogLink |
| 215 | return self.summary |
| 216 | |
Nick Anthony | 8f18f3d | 2020-05-28 20:15:56 -0400 | [diff] [blame] | 217 | def capitalizeTitle(self, artifactWord): |
| 218 | artifactWord = artifactWord.title() |
| 219 | keywords = ["Animated", "Animation", "Callback", "Compat", "Drawable", "File", "Layout", |
| 220 | "Pager", "Pane", "Parcelable", "Provider", "Refresh", "SQL", "State", "TV", |
| 221 | "Target", "View", "Inflater"] |
| 222 | for keyword in keywords: |
| 223 | artifactWord = artifactWord.replace(keyword.lower(), keyword) |
| 224 | return artifactWord |
| 225 | |
Nick Anthony | 7ea8ca9 | 2020-02-11 11:11:37 -0500 | [diff] [blame] | 226 | def addCommit(self, newCommit): |
| 227 | for bug in newCommit.bugs: |
| 228 | bugsFixed.append(bug) |
| 229 | commits.append(newCommit) |
| 230 | commitMarkdownList.add(newCommit) |
| 231 | |
| 232 | def __str__(self): |
| 233 | return "%s\n%s\n\n%s%s" % (self.header, self.releaseDate, self.getFormattedReleaseSummary(), self.commitMarkdownList) |