Call webhook when pull request is merged (#2611)

This commit is contained in:
Naoki Takezoe
2020-12-28 10:54:48 +09:00
committed by GitHub
parent 1dfe76e21c
commit 736fdafea4
5 changed files with 264 additions and 53 deletions

View File

@@ -10,6 +10,7 @@ import gitbucket.core.util.{JGitUtil, LockUtil}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo}
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.service.WebHookService.WebHookPushPayload
import gitbucket.core.util.JGitUtil.CommitInfo
import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
import org.eclipse.jgit.api.Git
@@ -27,7 +28,8 @@ trait MergeService {
with IssuesService
with RepositoryService
with PullRequestService
with WebHookPullRequestService =>
with WebHookPullRequestService
with WebHookService =>
import MergeService._
@@ -61,40 +63,91 @@ trait MergeService {
/** merge the pull request with a merge commit */
def mergeWithMergeCommit(
git: Git,
userName: String,
repositoryName: String,
repository: RepositoryInfo,
branch: String,
issueId: Int,
message: String,
committer: PersonIdent
)(implicit s: Session): ObjectId = {
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).merge(message, committer)
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
val afterCommitId = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
afterCommitId
}
/** rebase to the head of the pull request branch */
def mergeWithRebase(
git: Git,
userName: String,
repositoryName: String,
repository: RepositoryInfo,
branch: String,
issueId: Int,
commits: Seq[RevCommit],
committer: PersonIdent
)(implicit s: Session): ObjectId = {
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).rebase(committer, commits)
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
val afterCommitId =
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.rebase(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), commits)
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
afterCommitId
}
/** squash commits in the pull request and append it */
def mergeWithSquash(
git: Git,
userName: String,
repositoryName: String,
repository: RepositoryInfo,
branch: String,
issueId: Int,
message: String,
committer: PersonIdent
)(implicit s: Session): ObjectId = {
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).squash(message, committer)
loginAccount: Account,
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): ObjectId = {
val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}")
val afterCommitId =
new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks())
.squash(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings)
afterCommitId
}
private def callWebHook(
git: Git,
repository: RepositoryInfo,
branch: String,
beforeCommitId: ObjectId,
afterCommitId: ObjectId,
loginAccount: Account,
settings: SystemSettings
)(
implicit s: Session,
c: JsonFormat.Context
): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Push, settings) {
getAccountByUserName(repository.owner).map { ownerAccount =>
WebHookPushPayload(
git,
loginAccount,
s"refs/heads/${branch}",
repository,
git
.log()
.addRange(beforeCommitId, afterCommitId)
.call()
.asScala
.map { commit =>
new JGitUtil.CommitInfo(commit)
}
.toList
.reverse,
ownerAccount,
oldId = beforeCommitId,
newId = afterCommitId
)
}
}
}
/** fetch remote branch to my repository refs/pull/{issueId}/head */
@@ -303,7 +356,8 @@ trait MergeService {
message,
strategy,
commits,
getReceiveHooks()
getReceiveHooks(),
settings
) match {
case Some(newCommitId) =>
// mark issue as merged and close.
@@ -428,8 +482,9 @@ trait MergeService {
message: String,
strategy: String,
commits: Seq[Seq[CommitInfo]],
receiveHooks: Seq[ReceiveHook]
)(implicit s: Session): Option[ObjectId] = {
receiveHooks: Seq[ReceiveHook],
settings: SystemSettings
)(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = {
val revCommits = Using
.resource(new RevWalk(git.getRepository)) { revWalk =>
commits.flatten.map { commit =>
@@ -443,36 +498,36 @@ trait MergeService {
Some(
mergeWithMergeCommit(
git,
repository.owner,
repository.name,
repository,
pullRequest.branch,
issue.issueId,
s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
loginAccount,
settings
)
)
case "rebase" =>
Some(
mergeWithRebase(
git,
repository.owner,
repository.name,
repository,
pullRequest.branch,
issue.issueId,
revCommits,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
loginAccount,
settings
)
)
case "squash" =>
Some(
mergeWithSquash(
git,
repository.owner,
repository.name,
repository,
pullRequest.branch,
issue.issueId,
s"${issue.title} (#${issue.issueId})\n\n" + message,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
loginAccount,
settings
)
)
case _ =>

View File

@@ -257,11 +257,11 @@ trait WebHookService {
)(implicit s: Session, c: JsonFormat.Context): Unit = {
val webHooks = getWebHooksByEvent(owner, repository, event)
if (webHooks.nonEmpty) {
makePayload.map(callWebHook(event, webHooks, _, settings))
makePayload.foreach(callWebHook(event, webHooks, _, settings))
}
val accountWebHooks = getAccountWebHooksByEvent(owner, event)
if (accountWebHooks.nonEmpty) {
makePayload.map(callWebHook(event, accountWebHooks, _, settings))
makePayload.foreach(callWebHook(event, accountWebHooks, _, settings))
}
}

View File

@@ -1,18 +1,34 @@
package gitbucket.core.service
import gitbucket.core.util.Directory._
import gitbucket.core.util.GitSpecUtil._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jetty.server.handler.AbstractHandler
import org.scalatest.funspec.AnyFunSpec
import java.io.File
import java.util.Date
import java.net.InetSocketAddress
import java.nio.charset.StandardCharsets
import gitbucket.core.plugin.ReceiveHook
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import scala.util.Using
import scala.jdk.CollectionConverters._
import gitbucket.core.controller.Context
import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.util.Directory._
import gitbucket.core.util.GitSpecUtil._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.WebHookService.WebHookPushPayload
import org.eclipse.jetty.webapp.WebAppContext
import org.eclipse.jetty.server.{Request, Server}
import org.json4s.jackson.JsonMethods._
import MergeServiceSpec._
import org.json4s.JsonAST.{JArray, JString}
class MergeServiceSpec extends AnyFunSpec {
class MergeServiceSpec extends AnyFunSpec with ServiceSpecBase {
val service = new MergeService with AccountService with ActivityService with IssuesService with LabelsService
with MilestonesService with RepositoryService with PrioritiesService with PullRequestService with CommitsService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with RequestCache {
@@ -115,22 +131,161 @@ class MergeServiceSpec extends AnyFunSpec {
}
describe("mergePullRequest") {
it("can merge") {
val repo8Dir = initRepository("user1", "repo8")
Using.resource(Git.open(repo8Dir)) { git =>
createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge2")
val committer = new PersonIdent("dummy2", "dummy2@example.com")
assert(getFile(git, branch, "test.txt").content.get == "hoge")
val requestBranchId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val masterId = git.getRepository.resolve(branch)
service.mergeWithMergeCommit(git, "user1", "repo8", branch, issueId, "merged", committer)(null)
val lastCommitId = git.getRepository.resolve(branch)
val commit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(lastCommitId))
assert(commit.getCommitterIdent() == committer)
assert(commit.getAuthorIdent() == committer)
assert(commit.getFullMessage() == "merged")
assert(commit.getParents.toSet == Set(requestBranchId, masterId))
assert(getFile(git, branch, "test.txt").content.get == "hoge2")
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
import gitbucket.core.util.Implicits._
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo8")
initRepository("user1", "repo8")
implicit val context = Context(
createSystemSettings(),
Some(createAccount("dummy2", "dummy2-fullname", "dummy2@example.com")),
request
)
Using.resource(Git.open(getRepositoryDir("user1", "repo8"))) { git =>
val commitId = createFile(git, s"refs/pull/${issueId}/head", "test.txt", "hoge2")
assert(getFile(git, branch, "test.txt").content.get == "hoge")
val requestBranchId = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val masterId = git.getRepository.resolve(branch)
val repository = createRepositoryInfo("user1", "repo8")
registerWebHook("user1", "repo8", "http://localhost:9999")
Using.resource(new TestServer()) { server =>
service.mergeWithMergeCommit(
git,
repository,
branch,
issueId,
"merged",
context.loginAccount.get,
context.settings
)
Thread.sleep(5000)
val json = parse(new String(server.lastRequestContent, StandardCharsets.UTF_8))
// 2 commits (create file + merge commit)
assert((json \ "commits").asInstanceOf[JArray].arr.length == 2)
// verify id of file creation commit
assert((json \ "commits" \ "id").asInstanceOf[JArray].arr(0).asInstanceOf[JString].s == commitId.getName)
}
val lastCommitId = git.getRepository.resolve(branch)
val commit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(lastCommitId))
assert(commit.getCommitterIdent().getName == "dummy2-fullname")
assert(commit.getCommitterIdent().getEmailAddress == "dummy2@example.com")
assert(commit.getAuthorIdent().getName == "dummy2-fullname")
assert(commit.getAuthorIdent().getEmailAddress == "dummy2@example.com")
assert(commit.getFullMessage() == "merged")
assert(commit.getParents.toSet == Set(requestBranchId, masterId))
assert(getFile(git, branch, "test.txt").content.get == "hoge2")
}
}
}
}
private def registerWebHook(userName: String, repositoryName: String, url: String)(implicit s: Session): Unit = {
RepositoryWebHooks insert RepositoryWebHook(
userName = userName,
repositoryName = repositoryName,
url = url,
ctype = WebHookContentType.JSON,
token = None
)
RepositoryWebHookEvents insert RepositoryWebHookEvent(userName, repositoryName, url, WebHook.Push)
}
private def createAccount(userName: String, fullName: String, mailAddress: String): Account =
Account(
userName = userName,
fullName = fullName,
mailAddress = mailAddress,
password = "password",
isAdmin = false,
url = None,
registeredDate = new Date(),
updatedDate = new Date(),
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = false,
description = None
)
private def createRepositoryInfo(userName: String, repositoryName: String): RepositoryInfo =
RepositoryInfo(
owner = userName,
name = repositoryName,
repository = gitbucket.core.model.Repository(
userName = userName,
repositoryName = repositoryName,
isPrivate = false,
description = None,
defaultBranch = "master",
registeredDate = new Date(),
updatedDate = new Date(),
lastActivityDate = new Date(),
originUserName = None,
originRepositoryName = None,
parentUserName = None,
parentRepositoryName = None,
options = RepositoryOptions(
issuesOption = "PUBLIC",
externalIssuesUrl = None,
wikiOption = "PUBLIC",
externalWikiUrl = None,
allowFork = true,
mergeOptions = "merge-commit,squash,rebase",
defaultMergeOption = "merge-commit"
)
),
issueCount = 0,
pullCount = 0,
forkedCount = 0,
milestoneCount = 0,
branchList = Nil,
tags = Nil,
managers = Nil
)
}
object MergeServiceSpec {
class TestServer extends AutoCloseable {
var lastRequestURI: String = null
var lastRequestHeaders: Map[String, String] = null
var lastRequestContent: Array[Byte] = null
val server = new Server(new InetSocketAddress(9999))
val context = new WebAppContext()
context.setServer(server)
server.setStopAtShutdown(true)
server.setStopTimeout(500)
server.setHandler(new AbstractHandler {
override def handle(
target: String,
baseRequest: Request,
request: HttpServletRequest,
response: HttpServletResponse
): Unit = {
lastRequestURI = request.getRequestURI
lastRequestHeaders = request.getHeaderNames.asScala.map { key =>
key -> request.getHeader(key)
}.toMap
val bytes = new Array[Byte](request.getContentLength)
if (bytes.length > 0) {
request.getInputStream.read(bytes)
lastRequestContent = bytes
}
}
})
server.start()
override def close(): Unit = {
server.stop()
}
}
}

View File

@@ -28,7 +28,7 @@ trait ServiceSpecBase {
when(request.getContextPath).thenReturn("")
when(request.getSession).thenReturn(session)
private def createSystemSettings() =
def createSystemSettings() =
SystemSettings(
baseUrl = None,
information = None,

View File

@@ -46,7 +46,7 @@ object GitSpecUtil {
authorName: String = "dummy",
authorEmail: String = "dummy@example.com",
message: String = "test commit"
): Unit = {
): ObjectId = {
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(branch + "^{commit}")
@@ -65,7 +65,7 @@ object GitSpecUtil {
)
)
builder.finish()
JGitUtil.createNewCommit(
val commitId = JGitUtil.createNewCommit(
git,
inserter,
headId,
@@ -77,6 +77,7 @@ object GitSpecUtil {
)
inserter.flush()
inserter.close()
commitId
}
def getFile(git: Git, branch: String, path: String) = {