blob: 985e3757b259ffdfa5186a9850e0298e92289e31 [file] [log] [blame]
Jeff Gaston7ce6a572021-12-20 16:30:12 -05001/*
2 * Copyright 2022 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
19import org.gradle.api.logging.Logger
20import java.io.File
21import java.util.concurrent.TimeUnit
22
23/**
24 * A simple git client that uses system process commands to communicate with the git setup in the
25 * given working directory.
26 */
27class GitRunnerGitClient(
28 /**
29 * The root location for git
30 */
31 private val workingDir: File,
32 private val logger: Logger?,
33 private val commandRunner: GitClient.CommandRunner = RealCommandRunner(
34 workingDir = workingDir,
35 logger = logger
36 )
37) : GitClient {
38
39 private val gitRoot: File = findGitDirInParentFilepath(workingDir) ?: workingDir
40
41 /**
42 * Finds changed file paths since the given sha
43 */
44 override fun findChangedFilesSince(
45 sha: String,
46 top: String,
47 includeUncommitted: Boolean
48 ): List<String> {
49 // use this if we don't want local changes
50 return commandRunner.executeAndParse(
51 if (includeUncommitted) {
52 "$CHANGED_FILES_CMD_PREFIX HEAD..$sha"
53 } else {
54 "$CHANGED_FILES_CMD_PREFIX $top $sha"
55 }
56 )
57 }
58
59 /**
60 * checks the history to find the first merge CL.
61 */
62 override fun findPreviousSubmittedChange(): String? {
63 return commandRunner.executeAndParse(PREVIOUS_SUBMITTED_CMD)
64 .firstOrNull()
65 ?.split(" ")
66 ?.firstOrNull()
67 }
68
69 private fun findGitDirInParentFilepath(filepath: File): File? {
70 var curDirectory: File = filepath
71 while (curDirectory.path != "/") {
72 if (File("$curDirectory/.git").exists()) {
73 return curDirectory
74 }
75 curDirectory = curDirectory.parentFile
76 }
77 return null
78 }
79
80 private fun parseCommitLogString(
81 commitLogString: String,
82 commitStartDelimiter: String,
83 commitSHADelimiter: String,
84 subjectDelimiter: String,
85 authorEmailDelimiter: String,
86 localProjectDir: String
87 ): List<Commit> {
88 // Split commits string out into individual commits (note: this removes the deliminter)
89 val gitLogStringList: List<String>? = commitLogString.split(commitStartDelimiter)
90 var commitLog: MutableList<Commit> = mutableListOf()
91 gitLogStringList?.filter { gitCommit ->
92 gitCommit.trim() != ""
93 }?.forEach { gitCommit ->
94 commitLog.add(
95 Commit(
96 gitCommit,
97 localProjectDir,
98 commitSHADelimiter = commitSHADelimiter,
99 subjectDelimiter = subjectDelimiter,
100 authorEmailDelimiter = authorEmailDelimiter
101 )
102 )
103 }
104 return commitLog.toList()
105 }
106
107 /**
108 * Converts a diff log command into a [List<Commit>]
109 *
110 * @param gitCommitRange the [GitCommitRange] that defines the parameters of the git log command
111 * @param keepMerges boolean for whether or not to add merges to the return [List<Commit>].
112 * @param fullProjectDir a [File] object that represents the full project directory.
113 */
114 override fun getGitLog(
115 gitCommitRange: GitCommitRange,
116 keepMerges: Boolean,
117 fullProjectDir: File
118 ): List<Commit> {
119 val commitStartDelimiter: String = "_CommitStart"
120 val commitSHADelimiter: String = "_CommitSHA:"
121 val subjectDelimiter: String = "_Subject:"
122 val authorEmailDelimiter: String = "_Author:"
123 val dateDelimiter: String = "_Date:"
124 val bodyDelimiter: String = "_Body:"
125 val localProjectDir: String = fullProjectDir.relativeTo(gitRoot).toString()
126 val relativeProjectDir: String = fullProjectDir.relativeTo(workingDir).toString()
127
128 var gitLogOptions: String =
129 "--pretty=format:$commitStartDelimiter%n" +
130 "$commitSHADelimiter%H%n" +
131 "$authorEmailDelimiter%ae%n" +
132 "$dateDelimiter%ad%n" +
133 "$subjectDelimiter%s%n" +
134 "$bodyDelimiter%b" +
135 if (!keepMerges) {
136 " --no-merges"
137 } else {
138 ""
139 }
140 var gitLogCmd: String
141 if (gitCommitRange.fromExclusive != "") {
142 gitLogCmd = "$GIT_LOG_CMD_PREFIX $gitLogOptions " +
143 "${gitCommitRange.fromExclusive}..${gitCommitRange.untilInclusive}" +
144 " -- ./$relativeProjectDir"
145 } else {
146 gitLogCmd = "$GIT_LOG_CMD_PREFIX $gitLogOptions ${gitCommitRange.untilInclusive} -n " +
147 "${gitCommitRange.n} -- ./$relativeProjectDir"
148 }
149 val gitLogString: String = commandRunner.execute(gitLogCmd)
150 val commits = parseCommitLogString(
151 gitLogString,
152 commitStartDelimiter,
153 commitSHADelimiter,
154 subjectDelimiter,
155 authorEmailDelimiter,
156 localProjectDir
157 )
158 if (commits.isEmpty()) {
159 // Probably an error; log this
160 logger?.warn(
161 "No git commits found! Ran this command: '" +
162 gitLogCmd + "' and received this output: '" + gitLogString + "'"
163 )
164 }
165 return commits
166 }
167
168 private class RealCommandRunner(
169 private val workingDir: File,
170 private val logger: Logger?
171 ) : GitClient.CommandRunner {
172 override fun execute(command: String): String {
173 val parts = command.split("\\s".toRegex())
174 logger?.info("running command $command in $workingDir")
175 val proc = ProcessBuilder(*parts.toTypedArray())
176 .directory(workingDir)
177 .redirectOutput(ProcessBuilder.Redirect.PIPE)
178 .redirectError(ProcessBuilder.Redirect.PIPE)
179 .start()
180
181 // Read output, waiting for process to finish, as needed
182 val stdout = proc
183 .inputStream
184 .bufferedReader()
185 .readText()
186 val stderr = proc
187 .errorStream
188 .bufferedReader()
189 .readText()
190 val message = stdout + stderr
191 // wait potentially a little bit longer in case Git was waiting for us to
192 // read its response before it exited
193 proc.waitFor(10, TimeUnit.SECONDS)
194 logger?.info("Response: $message")
195 check(proc.exitValue() == 0) {
196 "Nonzero exit value running git command. Response: $message"
197 }
198 return stdout
199 }
200 override fun executeAndParse(command: String): List<String> {
201 val response = execute(command)
202 .split(System.lineSeparator())
203 .filterNot {
204 it.isEmpty()
205 }
206 return response
207 }
208 }
209
210 companion object {
211 const val PREVIOUS_SUBMITTED_CMD =
212 "git log -1 --merges --oneline --invert-grep --author=android-build-server"
213 const val CHANGED_FILES_CMD_PREFIX = "git diff --name-only"
214 const val GIT_LOG_CMD_PREFIX = "git log --name-only"
215 }
216}