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)
}
}