diff --git a/src/main/scala/gitbucket/core/service/MergeService.scala b/src/main/scala/gitbucket/core/service/MergeService.scala index 3e0d24d..7d2c7c7 100644 --- a/src/main/scala/gitbucket/core/service/MergeService.scala +++ b/src/main/scala/gitbucket/core/service/MergeService.scala @@ -10,6 +10,7 @@ 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 @@ with IssuesService with RepositoryService with PullRequestService - with WebHookPullRequestService => + with WebHookPullRequestService + with WebHookService => import MergeService._ @@ -61,40 +63,91 @@ /** 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 @@ message, strategy, commits, - getReceiveHooks() + getReceiveHooks(), + settings ) match { case Some(newCommitId) => // mark issue as merged and close. @@ -428,8 +482,9 @@ 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 @@ 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 _ => diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index e2ceef7..5c3a22f 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -257,11 +257,11 @@ )(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)) } } diff --git a/src/test/scala/gitbucket/core/service/MergeServiceSpec.scala b/src/test/scala/gitbucket/core/service/MergeServiceSpec.scala index e80d8dd..e3a10c5 100644 --- a/src/test/scala/gitbucket/core/service/MergeServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/MergeServiceSpec.scala @@ -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 @@ } 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() + } + } } diff --git a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala index 66f3b72..d3176bb 100644 --- a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala +++ b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala @@ -28,7 +28,7 @@ when(request.getContextPath).thenReturn("") when(request.getSession).thenReturn(session) - private def createSystemSettings() = + def createSystemSettings() = SystemSettings( baseUrl = None, information = None, diff --git a/src/test/scala/gitbucket/core/util/GitSpecUtil.scala b/src/test/scala/gitbucket/core/util/GitSpecUtil.scala index d43b7a5..d556817 100644 --- a/src/test/scala/gitbucket/core/util/GitSpecUtil.scala +++ b/src/test/scala/gitbucket/core/util/GitSpecUtil.scala @@ -46,7 +46,7 @@ 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 @@ ) ) builder.finish() - JGitUtil.createNewCommit( + val commitId = JGitUtil.createNewCommit( git, inserter, headId, @@ -77,6 +77,7 @@ ) inserter.flush() inserter.close() + commitId } def getFile(git: Git, branch: String, path: String) = {