Skip to content

Commit 8494fc5

Browse files
committed
Implement posting initial comment
1 parent 4263907 commit 8494fc5

File tree

3 files changed

+120
-21
lines changed

3 files changed

+120
-21
lines changed

bot/src/dotty/tools/bot/PullRequestService.scala

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package dotty.tools.bot
33
import org.http4s.{ Status => _, _ }
44
import org.http4s.client.blaze._
55
import org.http4s.client.Client
6-
import org.http4s.headers.Authorization
6+
import org.http4s.headers.{ Accept, Authorization }
77

88
import cats.syntax.applicative._
99
import scalaz.concurrent.Task
@@ -47,6 +47,10 @@ trait PullRequestService {
4747
new Authorization(creds)
4848
}
4949

50+
private[this] lazy val previewAcceptHeader =
51+
Accept.parse("application/vnd.github.black-cat-preview+json")
52+
.getOrElse(throw new Exception("Couldn't initialize accept header"))
53+
5054
private final case class CLASignature(
5155
user: String,
5256
signed: Boolean,
@@ -68,14 +72,20 @@ trait PullRequestService {
6872
def issueCommentsUrl(issueNbr: Int): String =
6973
s"$githubUrl/repos/lampepfl/dotty/issues/$issueNbr/comments"
7074

75+
def reviewUrl(issueNbr: Int): String =
76+
s"$githubUrl/repos/lampepfl/dotty/pulls/$issueNbr/reviews"
77+
7178
def toUri(url: String): Task[Uri] =
7279
Uri.fromString(url).fold(Task.fail, Task.now)
7380

7481
def getRequest(endpoint: Uri): Task[Request] =
75-
Request(uri = endpoint, method = Method.GET).putHeaders(authHeader).pure[Task]
82+
Request(uri = endpoint, method = Method.GET).putHeaders(authHeader)
83+
.pure[Task]
7684

7785
def postRequest(endpoint: Uri): Task[Request] =
78-
Request(uri = endpoint, method = Method.POST).putHeaders(authHeader).pure[Task]
86+
Request(uri = endpoint, method = Method.POST)
87+
.putHeaders(authHeader, previewAcceptHeader)
88+
.pure[Task]
7989

8090
def shutdownClient(client: Client): Task[Unit] =
8191
client.shutdownNow().pure[Task]
@@ -191,11 +201,87 @@ trait PullRequestService {
191201
private def usersFromInvalid(xs: List[CommitStatus]) =
192202
xs.collect { case Invalid(user, _) => user }
193203

194-
def sendInitialComment(invalidUsers: List[String]): Task[Unit] = ???
204+
def hasBadCommitMessages(commits: List[Commit]): Boolean =
205+
commits.exists { cm =>
206+
val firstLine = cm.commit.message.takeWhile(_ != '\n').trim.toLowerCase
207+
val firstWord = firstLine.takeWhile(x => x != ':' && x != ' ')
208+
val containsColon = firstLine.contains(':')
195209

196-
def checkFreshPR(issue: Issue): Task[Response] = {
210+
val wrongTense = firstWord match {
211+
case "added" | "fixed" | "removed" | "moved" | "changed" |
212+
"finalized" | "re-added"
213+
=> true
214+
215+
case "adds" | "fixes" | "removes" | "moves" | "changes" |
216+
"finalizes" | "re-adds"
217+
=> true
218+
219+
case _
220+
=> false
221+
}
222+
223+
wrongTense || firstLine.last == '.' || firstLine.length > 100
224+
}
197225

226+
def sendInitialComment(issueNbr: Int, invalidUsers: List[String], commits: List[Commit], client: Client): Task[ReviewResponse] = {
227+
228+
val cla = if (invalidUsers.nonEmpty) {
229+
s"""|## CLA ##
230+
|In order for us to be able to accept your contribution, all users
231+
|must sign the Scala CLA.
232+
|
233+
|Users:
234+
|${ invalidUsers.map("@" + _).mkString("- ", "\n- ", "") }
235+
|
236+
|Could you folks please sign the CLA? :pray:
237+
|
238+
|Please do this here: https://www.lightbend.com/contribute/cla/scala
239+
|""".stripMargin
240+
} else "All contributors have signed the CLA, thank you! :heart:"
241+
242+
val commitRules = if (hasBadCommitMessages(commits)) {
243+
"""|## Commit Messages ##
244+
|We want to keep history, but for that to actually be useful we have
245+
|some rules on how to format our commit messages ([relevant xkcd](https://xkcd.com/1296/)).
246+
|
247+
|Please stick to these guidelines for commit messages:
248+
|
249+
|> 1. Separate subject from body with a blank line
250+
|> 1. When fixing an issue, start your commit message with `Fix #<ISSUE-NBR>: `
251+
|> 1. Limit the subject line to 80 characters
252+
|> 1. Capitalize the subject line
253+
|> 1. Do not end the subject line with a period
254+
|> 1. Use the imperative mood in the subject line ("Added" instead of "Add")
255+
|> 1. Wrap the body at 80 characters
256+
|> 1. Use the body to explain what and why vs. how
257+
|>
258+
|> adapted from https://chris.beams.io/posts/git-commit""".stripMargin
259+
} else ""
260+
261+
val body =
262+
s"""|Hello, and thank you for opening this PR! :tada:
263+
|
264+
|If you haven't already, please request a review from one of our
265+
|collaborators (have no fear, we don't bite)!
266+
|
267+
|$cla
268+
|
269+
|$commitRules
270+
|
271+
|Have an awesome day! :sunny:""".stripMargin
272+
273+
val review = Review.comment(body)
274+
275+
for {
276+
endpoint <- toUri(reviewUrl(issueNbr))
277+
req <- postRequest(endpoint).withBody(review.asJson)
278+
res <- client.expect(req)(jsonOf[ReviewResponse])
279+
} yield res
280+
}
281+
282+
def checkFreshPR(issue: Issue): Task[Response] = {
198283
val httpClient = PooledHttp1Client()
284+
199285
for {
200286
commits <- getCommits(issue.number, httpClient)
201287
statuses <- checkCLA(commits, httpClient)
@@ -215,7 +301,7 @@ trait PullRequestService {
215301
}
216302

217303
// Send positive comment:
218-
_ <- sendInitialComment(invalidUsers)
304+
_ <- sendInitialComment(issue.number, invalidUsers, commits, httpClient)
219305
_ <- shutdownClient(httpClient)
220306
resp <- Ok("Fresh PR checked")
221307
} yield resp

bot/src/dotty/tools/bot/model/Github.scala

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,15 @@ package dotty.tools.bot
22
package model
33

44
object Github {
5-
case class PullRequest(
6-
url: String,
7-
id: Long,
8-
commits_url: String
9-
)
10-
115
case class Issue(
126
action: String, // "opened", "reopened", "closed", "synchronize"
137
number: Int,
148
pull_request: Option[PullRequest]
159
)
1610

17-
case class CommitInfo(
18-
message: String
19-
)
11+
case class PullRequest(url: String, id: Long, commits_url: String)
12+
13+
case class CommitInfo(message: String)
2014

2115
case class Commit(
2216
sha: String,
@@ -36,14 +30,20 @@ object Github {
3630
context: String = "CLA"
3731
)
3832

39-
// TODO: Can we get a `Commit` from `StatusResponse`?
40-
case class StatusResponse(
41-
url: String,
42-
id: Long,
43-
state: String
44-
) {
33+
case class StatusResponse(url: String, id: Long, state: String) {
4534
def sha: String = url.split('/').last
4635
}
4736

4837
case class Comment(user: Author)
38+
39+
/** A PR review */
40+
case class Review (body: String, event: String)
41+
42+
object Review {
43+
def approve(body: String) = Review(body, "APPROVE")
44+
def requestChanges(body: String) = Review(body, "REQUEST_CHANGES")
45+
def comment(body: String) = Review(body, "COMMENT")
46+
}
47+
48+
case class ReviewResponse(body: String, state: String, id: Long)
4949
}

bot/test/PRServiceTests.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,17 @@ class PRServiceTests extends PullRequestService {
121121

122122
assert(rechecked.forall(cs => cs.isValid), s"Should have set all statuses to valid, but got: $rechecked")
123123
}
124+
125+
@Test def canPostReview = {
126+
val invalidUsers = "felixmulder" :: "smarter" :: Nil
127+
val commit = Commit("", Author(Some("smarter")), Author(Some("smarter")), CommitInfo("Added stuff"))
128+
val res = withClient(sendInitialComment(2281, invalidUsers, commit :: Nil, _))
129+
130+
assert(
131+
res.body.contains("We want to keep history") &&
132+
res.body.contains("Could you folks please sign the CLA?") &&
133+
res.body.contains("Have an awesome day!"),
134+
s"Body of review was not as expected:\n${res.body}"
135+
)
136+
}
124137
}

0 commit comments

Comments
 (0)