All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gitbucket.core.service.ProtectedBranchService.scala Maven / Gradle / Ivy

The newest version!
package gitbucket.core.service

import gitbucket.core.model.{Session => _, _}
import gitbucket.core.plugin.ReceiveHook
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile.blockingApi._
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}

trait ProtectedBranchService {
  import ProtectedBranchService._
  private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit
    session: Session
  ): Option[ProtectedBranchInfo] =
    ProtectedBranches
      .joinLeft(ProtectedBranchContexts)
      .on { case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
      .map { case (pb, c) => pb -> c.map(_.context) }
      .filter(_._1.byPrimaryKey(owner, repository, branch))
      .list
      .groupBy(_._1)
      .headOption
      .map { p =>
        p._1 -> p._2.flatMap(_._2)
      }
      .map { case (t1, contexts) =>
        new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin)
      }

  def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit
    session: Session
  ): ProtectedBranchInfo =
    getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(
      ProtectedBranchInfo.disabled(owner, repository, branch)
    )

  def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] =
    ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list

  def enableBranchProtection(
    owner: String,
    repository: String,
    branch: String,
    includeAdministrators: Boolean,
    contexts: Seq[String]
  )(implicit session: Session): Unit = {
    disableBranchProtection(owner, repository, branch)
    ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty))
    contexts.map { context =>
      ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context))
    }
  }

  def disableBranchProtection(owner: String, repository: String, branch: String)(implicit session: Session): Unit =
    ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete

}

object ProtectedBranchService {

  class ProtectedBranchReceiveHook
      extends ReceiveHook
      with ProtectedBranchService
      with RepositoryService
      with AccountService {
    override def preReceive(
      owner: String,
      repository: String,
      receivePack: ReceivePack,
      command: ReceiveCommand,
      pusher: String,
      mergePullRequest: Boolean
    )(implicit session: Session): Option[String] = {
      if (mergePullRequest) {
        None
      } else {
        checkBranchProtection(owner, repository, receivePack, command, pusher)
      }
    }

    private def checkBranchProtection(
      owner: String,
      repository: String,
      receivePack: ReceivePack,
      command: ReceiveCommand,
      pusher: String,
    )(implicit session: Session): Option[String] = {
      val branch = command.getRefName.stripPrefix("refs/heads/")
      if (branch != command.getRefName) {
        val repositoryInfo = getRepository(owner, repository)
        if (
          command.getType == ReceiveCommand.Type.DELETE && repositoryInfo.exists(
            _.repository.defaultBranch == branch
          )
        ) {
          Some(s"refusing to delete the branch: ${command.getRefName}.")
        } else {
          getProtectedBranchInfo(owner, repository, branch).getStopReason(
            receivePack.isAllowNonFastForwards,
            command,
            pusher
          )
        }
      } else {
        None
      }
    }
  }

  case class ProtectedBranchInfo(
    owner: String,
    repository: String,
    branch: String,
    enabled: Boolean,
    /**
     * Require status checks to pass before merging
     * Choose which status checks must pass before branches can be merged into test.
     * When enabled, commits must first be pushed to another branch,
     * then merged or pushed directly to test after status checks have passed.
     */
    contexts: Seq[String],
    /**
     * Include administrators
     * Enforce required status checks for repository administrators.
     */
    includeAdministrators: Boolean
  ) extends AccountService
      with RepositoryService
      with CommitStatusService {

    def isAdministrator(pusher: String)(implicit session: Session): Boolean =
      pusher == owner || getGroupMembers(owner).exists(gm => gm.userName == pusher && gm.isManager) ||
        getCollaborators(owner, repository).exists { case (collaborator, isGroup) =>
          if (collaborator.role == Role.ADMIN.name) {
            if (isGroup) {
              getGroupMembers(collaborator.collaboratorName).exists(gm => gm.userName == pusher)
            } else {
              collaborator.collaboratorName == pusher
            }
          } else false
        }

    /**
     * Can't be force pushed
     * Can't be deleted
     * Can't have changes merged into them until required status checks pass
     */
    def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit
      session: Session
    ): Option[String] = {
      if (enabled) {
        command.getType() match {
          case ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
            Some("Cannot force-push to a protected branch")
          case ReceiveCommand.Type.UPDATE | ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
            unSuccessedContexts(command.getNewId.name) match {
              case s if s.sizeIs == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
              case s if s.sizeIs >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
              case _                  => None
            }
          case ReceiveCommand.Type.DELETE =>
            Some("Cannot delete a protected branch")
          case _ => None
        }
      } else {
        None
      }
    }
    def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] =
      if (contexts.isEmpty) {
        Set.empty
      } else {
        contexts.toSet -- getCommitStatuses(owner, repository, sha1)
          .filter(_.state == CommitState.SUCCESS)
          .map(_.context)
          .toSet
      }
    def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
      case _ if !enabled              => false
      case _ if contexts.isEmpty      => false
      case _ if includeAdministrators => true
      case p if isAdministrator(p)    => false
      case _                          => true
    }
  }
  object ProtectedBranchInfo {
    def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo =
      ProtectedBranchInfo(owner, repository, branch, false, Nil, false)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy