blob: dcc7b3107432ab5c7c80329cba84ab7b6bb36f57 [file] [log] [blame]
Nick Anthony593de4f2019-07-11 07:58:40 -04001/*
2 * Copyright 2018 The Android Open Source Project
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 */
16
17package androidx.build.gitclient
18
Nick Anthony593de4f2019-07-11 07:58:40 -040019import androidx.build.releasenotes.getBuganizerLink
Nick Anthonyb1753a92019-12-11 10:49:39 -050020import androidx.build.releasenotes.getChangeIdAOSPLink
Nick Anthony593de4f2019-07-11 07:58:40 -040021import java.io.File
Jeff Gastonc095a4b2022-01-14 12:25:49 -050022import org.gradle.api.GradleException
23import org.gradle.api.Project
24import org.gradle.api.provider.Provider
25import org.gradle.api.logging.Logger
Nick Anthony593de4f2019-07-11 07:58:40 -040026
27interface GitClient {
28 fun findChangedFilesSince(
29 sha: String,
30 top: String = "HEAD",
31 includeUncommitted: Boolean = false
32 ): List<String>
Jeff Gaston12cfc0c2021-05-06 11:03:57 -040033 fun findPreviousSubmittedChange(): String?
Nick Anthony593de4f2019-07-11 07:58:40 -040034
35 fun getGitLog(
Nick Anthonycc1b6982019-08-23 13:08:56 -070036 gitCommitRange: GitCommitRange,
Nick Anthony593de4f2019-07-11 07:58:40 -040037 keepMerges: Boolean,
38 fullProjectDir: File
39 ): List<Commit>
40
41 /**
42 * Abstraction for running execution commands for testability
43 */
44 interface CommandRunner {
45 /**
46 * Executes the given shell command and returns the stdout as a string.
47 */
48 fun execute(command: String): String
49 /**
50 * Executes the given shell command and returns the stdout by lines.
51 */
52 fun executeAndParse(command: String): List<String>
53 }
Jeff Gastonc095a4b2022-01-14 12:25:49 -050054
55 companion object {
56 fun getChangeInfoPath(project: Project): Provider<String> {
57 return project.providers.environmentVariable("CHANGE_INFO").orElse("")
58 }
59 fun getManifestPath(project: Project): Provider<String> {
60 return project.providers.environmentVariable("MANIFEST").orElse("")
61 }
62 fun create(
63 rootProjectDir: File,
64 logger: Logger,
65 changeInfoPath: String,
66 manifestPath: String
67 ): GitClient {
68 if (changeInfoPath != "") {
69 if (manifestPath == "") {
70 throw GradleException("Setting CHANGE_INFO requires also setting MANIFEST")
71 }
72 val changeInfoFile = File(changeInfoPath)
73 val manifestFile = File(manifestPath)
74 if (!changeInfoFile.exists()) {
75 throw GradleException("changeinfo file $changeInfoFile does not exist")
76 }
77 if (!manifestFile.exists()) {
78 throw GradleException("manifest $manifestFile does not exist")
79 }
80 val changeInfoText = changeInfoFile.readText()
81 val manifestText = manifestFile.readText()
82 logger.info("Using ChangeInfoGitClient with change info path $changeInfoPath, " +
83 "manifest $manifestPath")
84 return ChangeInfoGitClient(changeInfoText, manifestText)
85 }
86 logger.info("UsingGitRunnerGitClient")
87 return GitRunnerGitClient(rootProjectDir, logger)
88 }
89 }
Nick Anthony593de4f2019-07-11 07:58:40 -040090}
Nick Anthony593de4f2019-07-11 07:58:40 -040091
92enum class CommitType {
93 NEW_FEATURE, API_CHANGE, BUG_FIX, EXTERNAL_CONTRIBUTION;
94 companion object {
95 fun getTitle(commitType: CommitType): String {
96 return when (commitType) {
97 NEW_FEATURE -> "New Features"
98 API_CHANGE -> "API Changes"
99 BUG_FIX -> "Bug Fixes"
100 EXTERNAL_CONTRIBUTION -> "External Contribution"
101 }
102 }
103 }
104}
105
106/**
Nick Anthonycc1b6982019-08-23 13:08:56 -0700107 * Defines the parameters for a git log command
108 *
Nick Anthonyf94d6002019-08-28 14:15:46 -0700109 * @property fromExclusive the oldest SHA at which the git log starts. Set to an empty string to use
110 * [n]
111 * @property untilInclusive the latest SHA included in the git log. Defaults to HEAD
112 * @property n a count of how many commits to go back to. Only used when [fromExclusive] is an
113 * empty string
Nick Anthonycc1b6982019-08-23 13:08:56 -0700114 */
115data class GitCommitRange(
Nick Anthonyf94d6002019-08-28 14:15:46 -0700116 val fromExclusive: String = "",
117 val untilInclusive: String = "HEAD",
Nick Anthonycc1b6982019-08-23 13:08:56 -0700118 val n: Int = 0
119)
120
121/**
Nick Anthony593de4f2019-07-11 07:58:40 -0400122 * Class implementation of a git commit. It uses the input delimiters to parse the commit
123 *
Jeff Gastonda2df852022-01-24 11:54:34 -0500124 * @property formattedCommitText a string representation of a git commit
Nick Anthony593de4f2019-07-11 07:58:40 -0400125 * @property projectDir the project directory for which to parse file paths from a commit
126 * @property commitSHADelimiter the term to use to search for the commit SHA
127 * @property subjectDelimiter the term to use to search for the subject (aka commit summary)
128 * @property changeIdDelimiter the term to use to search for the change-id in the body of the commit
129 * message
130 * @property authorEmailDelimiter the term to use to search for the author email
131 */
132data class Commit(
Jeff Gastonda2df852022-01-24 11:54:34 -0500133 val formattedCommitText: String,
Nick Anthony593de4f2019-07-11 07:58:40 -0400134 val projectDir: String,
135 private val commitSHADelimiter: String = "_CommitSHA:",
136 private val subjectDelimiter: String = "_Subject:",
137 private val authorEmailDelimiter: String = "_Author:"
138) {
139 private val changeIdDelimiter: String = "Change-Id:"
140 var bugs: MutableList<Int> = mutableListOf()
141 var files: MutableList<String> = mutableListOf()
142 var sha: String = ""
143 var authorEmail: String = ""
144 var changeId: String = ""
145 var summary: String = ""
146 var type: CommitType = CommitType.BUG_FIX
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400147 var releaseNote: String = ""
148 private val releaseNoteDelimiters: List<String> = listOf(
Nick Anthonyb1753a92019-12-11 10:49:39 -0500149 "Relnote:"
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400150 )
Nick Anthony593de4f2019-07-11 07:58:40 -0400151
152 init {
Jeff Gastonda2df852022-01-24 11:54:34 -0500153 val listedCommit: List<String> = formattedCommitText.split('\n')
Nick Anthony593de4f2019-07-11 07:58:40 -0400154 listedCommit.filter { line -> line.trim() != "" }.forEach { line ->
Jeff Gaston14154dd2020-02-12 10:45:00 -0500155 processCommitLine(line)
Nick Anthony593de4f2019-07-11 07:58:40 -0400156 }
157 }
158
Jeff Gaston14154dd2020-02-12 10:45:00 -0500159 private fun processCommitLine(line: String) {
Sam Gilbert59601412020-02-19 14:32:15 -0500160 if (commitSHADelimiter in line) {
161 getSHAFromGitLine(line)
162 return
163 }
164 if (subjectDelimiter in line) {
165 getSummary(line)
166 return
167 }
168 if (changeIdDelimiter in line) {
169 getChangeIdFromGitLine(line)
170 return
171 }
172 if (authorEmailDelimiter in line) {
173 getAuthorEmailFromGitLine(line)
174 return
175 }
176 if ("Bug:" in line ||
177 "b/" in line ||
178 "bug:" in line ||
179 "Fixes:" in line ||
180 "fixes b/" in line
181 ) {
182 getBugsFromGitLine(line)
183 return
184 }
185 releaseNoteDelimiters.forEach { delimiter ->
186 if (delimiter in line) {
Jeff Gastonda2df852022-01-24 11:54:34 -0500187 getReleaseNotesFromGitLine(line, formattedCommitText)
Sam Gilbert59601412020-02-19 14:32:15 -0500188 return
189 }
190 }
191 if (projectDir.trim('/') in line) {
192 getFileFromGitLine(line)
193 return
194 }
Jeff Gaston14154dd2020-02-12 10:45:00 -0500195 }
196
Nick Anthony593de4f2019-07-11 07:58:40 -0400197 private fun isExternalAuthorEmail(authorEmail: String): Boolean {
198 return !(authorEmail.contains("@google.com"))
199 }
200
201 /**
202 * Parses SHAs from git commit line, with the format:
203 * [Commit.commitSHADelimiter] <commitSHA>
204 */
205 private fun getSHAFromGitLine(line: String) {
206 sha = line.substringAfter(commitSHADelimiter).trim()
207 }
208
209 /**
210 * Parses subject from git commit line, with the format:
211 * [Commit.subjectDelimiter]<commit subject>
212 */
213 private fun getSummary(line: String) {
214 summary = line.substringAfter(subjectDelimiter).trim()
215 }
216
217 /**
218 * Parses commit Change-Id lines, with the format:
219 * `commit.changeIdDelimiter` <changeId>
220 */
221 private fun getChangeIdFromGitLine(line: String) {
222 changeId = line.substringAfter(changeIdDelimiter).trim()
223 }
224
225 /**
226 * Parses commit author lines, with the format:
227 * [Commit.authorEmailDelimiter][email protected]
228 */
229 private fun getAuthorEmailFromGitLine(line: String) {
230 authorEmail = line.substringAfter(authorEmailDelimiter).trim()
231 if (isExternalAuthorEmail(authorEmail)) {
232 type = CommitType.EXTERNAL_CONTRIBUTION
233 }
234 }
235
236 /**
237 * Parses filepath to get changed files from commit, with the format:
238 * {project_directory}/{filepath}
239 */
240 private fun getFileFromGitLine(filepath: String) {
241 files.add(filepath.trim())
242 if (filepath.contains("current.txt") && type != CommitType.EXTERNAL_CONTRIBUTION) {
243 type = CommitType.API_CHANGE
244 }
245 }
246
247 /**
248 * Parses bugs from a git commit message line
249 */
250 private fun getBugsFromGitLine(line: String) {
251 var formattedLine = line.replace("b/", " ")
252 formattedLine = formattedLine.replace(":", " ")
253 formattedLine = formattedLine.replace(",", " ")
254 var words: List<String> = formattedLine.split(' ')
255 words.forEach { word ->
256 var possibleBug: Int? = word.toIntOrNull()
257 if (possibleBug != null && possibleBug > 1000) {
258 bugs.add(possibleBug)
259 }
260 }
261 }
262
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400263 /**
264 * Reads in the release notes field from the git commit message line
265 *
266 * They can have a couple valid formats:
267 *
268 * `Release notes: This is a one-line release note`
269 * `Release Notes: "This is a multi-line release note. This accounts for the use case where
270 * the commit cannot be explained in one line"
271 * `release notes: "This is a one-line release note. The quotes can be used this way too"`
272 */
Jeff Gastonda2df852022-01-24 11:54:34 -0500273 private fun getReleaseNotesFromGitLine(line: String, formattedCommitText: String) {
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400274 /* Account for the use of quotes in a release note line
275 * No quotes in the Release Note line means it's a one-line release note
276 * If there are quotes, assume it's a multi-line release note
277 */
278 var quoteCountInRelNoteLine: Int = 0
279 line.forEach { character ->
280 if (character == '"') { quoteCountInRelNoteLine++ }
281 }
282 if (quoteCountInRelNoteLine == 0) {
283 getOneLineReleaseNotesFromGitLine(line)
284 } else {
285 releaseNoteDelimiters.forEach { delimiter ->
286 if (delimiter in line) {
287 // Find the starting quote of the release notes quote block
Jeff Gastonda2df852022-01-24 11:54:34 -0500288 var releaseNoteStartIndex = formattedCommitText.lastIndexOf(delimiter)
289 + delimiter.length
290 releaseNoteStartIndex = formattedCommitText.indexOf('"', releaseNoteStartIndex)
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400291 // Move to the character after the first quote
Jeff Gastonda2df852022-01-24 11:54:34 -0500292 if (formattedCommitText[releaseNoteStartIndex] == '"') {
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400293 releaseNoteStartIndex++
294 }
295 // Find the ending quote of the release notes quote block
296 var releaseNoteEndIndex = releaseNoteStartIndex + 1
Jeff Gastonda2df852022-01-24 11:54:34 -0500297 releaseNoteEndIndex = formattedCommitText.indexOf('"', releaseNoteEndIndex)
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400298 // If there is no closing quote, just use the first line
299 if (releaseNoteEndIndex < 0) {
300 getOneLineReleaseNotesFromGitLine(line)
301 return
302 }
Jeff Gastonda2df852022-01-24 11:54:34 -0500303 releaseNote = formattedCommitText.substring(
Nick Anthonyb2fd2f32019-09-27 16:01:49 -0400304 startIndex = releaseNoteStartIndex,
305 endIndex = releaseNoteEndIndex
306 ).trim()
307 }
308 }
309 }
310 }
311
312 private fun getOneLineReleaseNotesFromGitLine(line: String) {
313 releaseNoteDelimiters.forEach { delimiter ->
314 if (delimiter in line) {
315 releaseNote = line.substringAfter(delimiter).trim(' ', '"')
316 return
317 }
318 }
319 }
320
Nick Anthonyb5425ae2019-10-04 10:40:12 -0400321 fun getReleaseNoteString(): String {
322 var releaseNoteString: String = releaseNote
Nick Anthonyb1753a92019-12-11 10:49:39 -0500323 releaseNoteString += " ${getChangeIdAOSPLink(changeId)}"
Nick Anthonyb5425ae2019-10-04 10:40:12 -0400324 bugs.forEach { bug ->
325 releaseNoteString += " ${getBuganizerLink(bug)}"
326 }
327 return releaseNoteString
328 }
329
Nick Anthony593de4f2019-07-11 07:58:40 -0400330 override fun toString(): String {
331 var commitString: String = summary
Nick Anthonyb1753a92019-12-11 10:49:39 -0500332 commitString += " ${getChangeIdAOSPLink(changeId)}"
Nick Anthony593de4f2019-07-11 07:58:40 -0400333 bugs.forEach { bug ->
334 commitString += " ${getBuganizerLink(bug)}"
335 }
336 return commitString
337 }
338}