Newer
Older
gitbucket_jkp / src / main / scala / app / PullRequestsController.scala
package app

import util.{CollaboratorsAuthenticator, FileUtil, JGitUtil, ReferrerAuthenticator}
import util.Directory._
import service._
import org.eclipse.jgit.treewalk.CanonicalTreeParser
import util.JGitUtil.{DiffInfo, CommitInfo}
import scala.collection.mutable.ArrayBuffer
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import util.JGitUtil.DiffInfo
import scala.Some
import util.JGitUtil.CommitInfo
import org.eclipse.jgit.transport.RefSpec
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.lib.PersonIdent

class PullRequestsController extends PullRequestsControllerBase
  with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with ActivityService
  with ReferrerAuthenticator with CollaboratorsAuthenticator

trait PullRequestsControllerBase extends ControllerBase {
  self: RepositoryService with IssuesService with MilestonesService with ActivityService with PullRequestService
    with ReferrerAuthenticator with CollaboratorsAuthenticator =>

  val pullRequestForm = mapping(
    "title"           -> trim(label("Title"  , text(required, maxlength(100)))),
    "content"         -> trim(label("Content", optional(text()))),
    "branch"          -> trim(text(required, maxlength(100))),
    "requestUserName" -> trim(text(required, maxlength(100))),
    "requestBranch"   -> trim(text(required, maxlength(100)))
  )(PullRequestForm.apply)

  val mergeForm = mapping(
    "message" -> trim(label("Message", text(required)))
  )(MergeForm.apply)

  case class PullRequestForm(title: String, content: Option[String], branch: String,
                             requestUserName: String, requestBranch: String)

  case class MergeForm(message: String)

  get("/:owner/:repository/pulls")(referrersOnly { repository =>
    pulls.html.list(repository)
  })

  get("/:owner/:repository/pulls/:id")(referrersOnly { repository =>
    val owner   = repository.owner
    val name    = repository.name
    val issueId = params("id").toInt

    getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
      pulls.html.pullreq(
        issue, pullreq,
        getComments(owner, name, issueId.toInt),
        (getCollaborators(owner, name) :+ owner).sorted,
        getMilestones(owner, name),
        hasWritePermission(owner, name, context.loginAccount),
        repository)
    } getOrElse NotFound
  })

  get("/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
    val owner   = repository.owner
    val name    = repository.name
    val issueId = params("id").toInt

    getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
      pulls.html.commits(
        issue, pullreq,
        getCompareInfo(owner, name, pullreq.branch, pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)._1,
        hasWritePermission(owner, name, context.loginAccount),
        repository)
    } getOrElse NotFound
  })

  get("/:owner/:repository/pulls/:id/files")(referrersOnly { repository =>
    val owner   = repository.owner
    val name    = repository.name
    val issueId = params("id").toInt

    getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
      JGitUtil.withGit(getRepositoryDir(owner, name)){ git =>
        val newId = git.getRepository.resolve(pullreq.requestBranch)

        pulls.html.files(
          issue, pullreq,
          getCompareInfo(owner, name, pullreq.branch, pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch)._2,
          newId.getName,
          hasWritePermission(owner, name, context.loginAccount),
          repository)
      }
    } getOrElse NotFound
  })


  post("/:owner/:repository/pulls/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
    val issueId = params("id").toInt

    getPullRequest(repository.owner, repository.name, issueId).map { case (issue, pullreq) =>
      val remote = getRepositoryDir(repository.owner, repository.name)
      val tmpdir = new java.io.File(getTemporaryDir(repository.owner, repository.name), s"merge-${issueId}")
      val git = Git.cloneRepository.setDirectory(tmpdir).setURI(remote.toURI.toString).call

      try {
        // TODO merge and close issue
        val loginAccount = context.loginAccount.get
        recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, form.message)

        git.checkout.setName(pullreq.branch).call

        git.fetch
          .setRemote(getRepositoryDir(pullreq.requestUserName, pullreq.requestRepositoryName).toURI.toString)
          .setRefSpecs(new RefSpec(s"refs/heads/${pullreq.branch}:refs/heads/${pullreq.requestBranch}")).call

        git.merge.include(git.getRepository.resolve("FETCH_HEAD")).setCommit(false).call

        git.commit
          .setCommitter(new PersonIdent(loginAccount.userName, loginAccount.mailAddress))
          .setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestRepositoryName}\n"
                     + form.message).call
        git.push.call

      } finally {
        git.getRepository.close
        FileUtils.deleteDirectory(tmpdir)
      }
    } getOrElse NotFound
  })

  get("/:owner/:repository/pulls/compare")(collaboratorsOnly { newRepo =>
    (newRepo.repository.originUserName, newRepo.repository.originRepositoryName) match {
      case (None,_)|(_, None) => NotFound // TODO BadRequest?
      case (Some(originUserName), Some(originRepositoryName)) => {
        getRepository(originUserName, originRepositoryName, baseUrl).map { oldRepo =>
          withGit(
            getRepositoryDir(originUserName, originRepositoryName),
            getRepositoryDir(params("owner"), params("repository"))
          ){ (oldGit, newGit) =>
            val oldBranch = JGitUtil.getDefaultBranch(oldGit, oldRepo).get._2
            val newBranch = JGitUtil.getDefaultBranch(newGit, newRepo).get._2

            redirect(s"${context.path}/${newRepo.owner}/${newRepo.name}/pulls/compare/${originUserName}:${oldBranch}...${newBranch}")
          }
        } getOrElse NotFound
      }
    }
  })

  get("/:owner/:repository/pulls/compare/*:*...*")(collaboratorsOnly { repository =>
    if(repository.repository.originUserName.isEmpty || repository.repository.originRepositoryName.isEmpty){
      NotFound // TODO BadRequest?
    } else {
      getRepository(
        repository.repository.originUserName.get,
        repository.repository.originRepositoryName.get, baseUrl
      ).map{ originRepository =>
        val Seq(origin, originId, forkedId) = multiParams("splat")
        val userName       = params("owner")
        val repositoryName = params("repository")

        JGitUtil.withGit(getRepositoryDir(userName, repositoryName)){ git =>
          val newId = git.getRepository.resolve(forkedId)

          val pullreq = getCompareInfo(
            origin, repository.repository.originRepositoryName.get, originId,
            params("owner"), params("repository"), forkedId)

          pulls.html.compare(pullreq._1, pullreq._2, origin, originId, forkedId, newId.getName, repository, originRepository)
        }
      } getOrElse NotFound
    }
  })

  post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
    val loginUserName = context.loginAccount.get.userName

    val issueId = createIssue(
      repository.owner,
      repository.name,
      loginUserName,
      form.title,
      form.content,
      None, None)

    createPullRequest(
      repository.owner,
      repository.name,
      issueId,
      form.branch,
      form.requestUserName,
      repository.name,
      form.requestBranch)

    recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)

    redirect(s"/${repository.owner}/${repository.name}/pulls/${issueId}")
  })

  private def withGit[T](oldDir: java.io.File, newDir: java.io.File)(action: (Git, Git) => T): T = {
    val oldGit = Git.open(oldDir)
    val newGit = Git.open(newDir)
    try {
      action(oldGit, newGit)
    } finally {
      oldGit.getRepository.close
      newGit.getRepository.close
    }
  }

  /**
   * Returns the commits and diffs between specified repository and revision.
   */
  private def getCompareInfo(userName: String, repositoryName: String, branch: String,
      requestUserName: String, requestRepositoryName: String, requestBranch: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) = {
    withGit(
      getRepositoryDir(userName, repositoryName),
      getRepositoryDir(requestUserName, requestRepositoryName)
    ){ (oldGit, newGit) =>
      val oldReader = oldGit.getRepository.newObjectReader
      val oldTreeIter = new CanonicalTreeParser
      oldTreeIter.reset(oldReader, oldGit.getRepository.resolve(s"${branch}^{tree}"))

      val newReader = newGit.getRepository.newObjectReader
      val newTreeIter = new CanonicalTreeParser
      newTreeIter.reset(newReader, newGit.getRepository.resolve(s"${requestBranch}^{tree}"))

      import scala.collection.JavaConverters._
      import util.Implicits._

      val oldId = oldGit.getRepository.resolve(branch)
      val newId = newGit.getRepository.resolve(requestBranch)
      val i = newGit.log.addRange(oldId, newId).call.iterator.asScala

      val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
        new CommitInfo(revCommit)
      }.toSeq.splitWith{ (commit1, commit2) =>
        view.helpers.date(commit1.time) == view.helpers.date(commit2.time)
      }

      val diffs = newGit.diff.setOldTree(oldTreeIter).setNewTree(newTreeIter).call.asScala.map { diff =>
        if(FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
          DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
        } else {
          DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
            JGitUtil.getContent(oldGit, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")),
            JGitUtil.getContent(newGit, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(new String(_, "UTF-8")))
        }
      }.toSeq

      (commits, diffs)
    }
  }

}