Reject direct push to branch if branch protection is enabled (#3791)

This commit is contained in:
Naoki Takezoe
2025-08-03 11:58:28 +09:00
committed by GitHub
parent fda67a32e2
commit 0c1e8b932b
21 changed files with 726 additions and 350 deletions

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<!--================================================================================================-->
<!-- PROTECTED_BRANCH -->
<!--================================================================================================-->
<addColumn tableName="PROTECTED_BRANCH">
<column name="REQUIRED_STATUS_CHECK" type="boolean" nullable="false" defaultValue="false"/>
<column name="RESTRICTIONS" type="boolean" nullable="false" defaultValue="false"/>
</addColumn>
<sql>
UPDATE PROTECTED_BRANCH SET REQUIRED_STATUS_CHECK = TRUE
WHERE EXISTS (SELECT * FROM PROTECTED_BRANCH_REQUIRE_CONTEXT
WHERE PROTECTED_BRANCH.USER_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.USER_NAME
AND PROTECTED_BRANCH.REPOSITORY_NAME = PROTECTED_BRANCH_REQUIRE_CONTEXT.REPOSITORY_NAME
AND PROTECTED_BRANCH.BRANCH = PROTECTED_BRANCH_REQUIRE_CONTEXT.BRANCH)
</sql>
<!--================================================================================================-->
<!-- PROTECTED_BRANCH_RESTRICTIONS_USER -->
<!--================================================================================================-->
<createTable tableName="PROTECTED_BRANCH_RESTRICTION">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="BRANCH" type="varchar(100)" nullable="false"/>
<column name="ALLOWED_USER" type="varchar(255)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_PK" tableName="PROTECTED_BRANCH_RESTRICTION" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, ALLOWED_USER"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK0" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_RESTRICTION_FK1" baseTableName="PROTECTED_BRANCH_RESTRICTION" baseColumnNames="ALLOWED_USER" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
</changeSet>

View File

@@ -120,7 +120,8 @@ object GitBucketCoreModule
new Version("4.41.0"),
new Version("4.42.0", new LiquibaseMigration("update/gitbucket-core_4.42.xml")),
new Version("4.42.1"),
new Version("4.43.0")
new Version("4.43.0"),
new Version("4.44.0", new LiquibaseMigration("update/gitbucket-core_4.44.xml"))
) {
java.util.logging.Logger.getLogger("liquibase").setLevel(Level.SEVERE)
}

View File

@@ -6,7 +6,7 @@ import gitbucket.core.util.RepositoryName
* https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtection)(
case class ApiBranch(name: String, commit: ApiBranchCommit, protection: ApiBranchProtectionResponse)(
repositoryName: RepositoryName
) extends FieldSerializable {
val _links =

View File

@@ -0,0 +1,21 @@
package gitbucket.core.api
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtectionRequest(
enabled: Boolean,
required_status_checks: Option[ApiBranchProtectionRequest.Status],
restrictions: Option[ApiBranchProtectionRequest.Restrictions],
enforce_admins: Option[Boolean]
)
object ApiBranchProtectionRequest {
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtectionRequest)
case class Status(
contexts: Seq[String]
)
case class Restrictions(users: Seq[String])
}

View File

@@ -4,55 +4,68 @@ import gitbucket.core.service.ProtectedBranchService
import org.json4s._
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtection(
case class ApiBranchProtectionResponse(
url: Option[ApiPath], // for output
enabled: Boolean,
required_status_checks: Option[ApiBranchProtection.Status]
required_status_checks: Option[ApiBranchProtectionResponse.Status],
restrictions: Option[ApiBranchProtectionResponse.Restrictions],
enforce_admins: Option[ApiBranchProtectionResponse.EnforceAdmins]
) {
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
def status: ApiBranchProtectionResponse.Status =
required_status_checks.getOrElse(ApiBranchProtectionResponse.statusNone)
}
object ApiBranchProtection {
object ApiBranchProtectionResponse {
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtection)
case class EnforceAdmins(enabled: Boolean)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection =
ApiBranchProtection(
// /** form for enabling-and-disabling-branch-protection */
// case class EnablingAndDisabling(protection: ApiBranchProtectionResponse)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtectionResponse =
ApiBranchProtectionResponse(
url = Some(
ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection"
)
),
enabled = info.enabled,
required_status_checks = Some(
required_status_checks = info.contexts.map { contexts =>
Status(
Some(
ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks"
)
),
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators),
info.contexts,
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.enforceAdmins),
contexts,
Some(
ApiPath(
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts"
)
)
)
)
},
restrictions = info.restrictionsUsers.map { restrictionsUsers =>
Restrictions(restrictionsUsers)
},
enforce_admins = if (info.enabled) Some(EnforceAdmins(info.enforceAdmins)) else None
)
val statusNone = Status(None, Off, Seq.empty, None)
val statusNone: Status = Status(None, Off, Seq.empty, None)
case class Status(
url: Option[ApiPath], // for output
enforcement_level: EnforcementLevel,
contexts: Seq[String],
contexts_url: Option[ApiPath] // for output
)
sealed class EnforcementLevel(val name: String)
case object Off extends EnforcementLevel("off")
case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel =
if (enabled) {
@@ -66,6 +79,8 @@ object ApiBranchProtection {
}
}
case class Restrictions(users: Seq[String])
implicit val enforcementLevelSerializer: CustomSerializer[EnforcementLevel] =
new CustomSerializer[EnforcementLevel](format =>
(

View File

@@ -44,7 +44,7 @@ object JsonFormat {
FieldSerializer[ApiCommits.File]() +
FieldSerializer[ApiRelease]() +
FieldSerializer[ApiReleaseAsset]() +
ApiBranchProtection.enforcementLevelSerializer
ApiBranchProtectionResponse.enforcementLevelSerializer
def apiPathSerializer(c: Context) =
new CustomSerializer[ApiPath](_ =>

View File

@@ -261,11 +261,22 @@ trait IndexControllerBase extends ControllerBase {
/**
* JSON API for checking user or group existence.
*
* Returns a single string which is any of "group", "user" or "".
* Additionally, check whether the user is writable to the repository
* if "owner" and "repository" are given,
*/
post("/_user/existence")(usersOnly {
getAccountByUserNameIgnoreCase(params("userName")).map { account =>
if (account.isGroupAccount) "group" else "user"
if (!account.isGroupAccount && params.get("repository").isDefined && params.get("owner").isDefined) {
getRepository(params("owner"), params("repository"))
.collect {
case repository if isWritable(repository.repository, Some(account)) => "user"
}
.getOrElse("")
} else {
if (account.isGroupAccount) "group" else "user"
}
} getOrElse ""
})

View File

@@ -203,7 +203,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
if (!repository.branchList.contains(branch)) {
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val protection = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatusContexts(
repository.owner,
repository.name,

View File

@@ -5,7 +5,7 @@ import gitbucket.core.service.{AccountService, ProtectedBranchService, Repositor
import gitbucket.core.util.*
import gitbucket.core.util.Directory.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.JGitUtil.getBranchesNoMergeInfo
import gitbucket.core.util.JGitUtil.{getBranchesNoMergeInfo, processTree}
import org.eclipse.jgit.api.Git
import org.scalatra.NoContent
@@ -43,7 +43,9 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
} yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat(
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtection(protection))(RepositoryName(repository))
ApiBranch(branch, ApiBranchCommit(br.commitId), ApiBranchProtectionResponse(protection))(
RepositoryName(repository)
)
)
}) getOrElse NotFound()
}
@@ -58,7 +60,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat(
ApiBranchProtection(protection)
ApiBranchProtectionResponse(protection)
)
} else { NotFound() }
})
@@ -138,7 +140,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
if (repository.branchList.contains(branch)) {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
JsonFormat(
ApiBranchProtection(protection).required_status_checks
ApiBranchProtectionResponse(protection).required_status_checks
)
} else { NotFound() }
})
@@ -262,7 +264,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
(for {
branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
protection <- extractFromJsonBody[ApiBranchProtectionRequest.EnablingAndDisabling].map(_.protection)
br <- getBranchesNoMergeInfo(git).find(_.name == branch)
} yield {
if (protection.enabled) {
@@ -270,13 +272,17 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
repository.owner,
repository.name,
branch,
protection.status.enforcement_level == ApiBranchProtection.Everyone,
protection.status.contexts
protection.enforce_admins.getOrElse(false),
protection.required_status_checks.isDefined,
protection.required_status_checks.map(_.contexts).getOrElse(Nil),
protection.restrictions.isDefined,
protection.restrictions.map(_.users).getOrElse(Nil)
)
} else {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), protection)(RepositoryName(repository)))
val response = ApiBranchProtectionResponse(getProtectedBranchInfo(repository.owner, repository.name, branch))
JsonFormat(ApiBranch(branch, ApiBranchCommit(br.commitId), response)(RepositoryName(repository)))
}) getOrElse NotFound()
}
})

View File

@@ -1,16 +1,18 @@
package gitbucket.core.model
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
import profile.api.*
lazy val ProtectedBranches = TableQuery[ProtectedBranches]
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
def * = (userName, repositoryName, branch, statusCheckAdmin).mapTo[ProtectedBranch]
def byPrimaryKey(userName: String, repositoryName: String, branch: String) =
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") // enforceAdmins
val requiredStatusCheck = column[Boolean]("REQUIRED_STATUS_CHECK")
val restrictions = column[Boolean]("RESTRICTIONS")
def * =
(userName, repositoryName, branch, statusCheckAdmin, requiredStatusCheck, restrictions).mapTo[ProtectedBranch]
def byPrimaryKey(userName: String, repositoryName: String, branch: String): Rep[Boolean] =
byBranch(userName, repositoryName, branch)
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) =
def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]): Rep[Boolean] =
byBranch(userName, repositoryName, branch)
}
@@ -22,8 +24,27 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
def * =
(userName, repositoryName, branch, context).mapTo[ProtectedBranchContext]
}
lazy val ProtectedBranchRestrictions = TableQuery[ProtectedBranchRestrictions]
class ProtectedBranchRestrictions(tag: Tag)
extends Table[ProtectedBranchRestriction](tag, "PROTECTED_BRANCH_RESTRICTION")
with BranchTemplate {
val allowedUser = column[String]("ALLOWED_USER")
def * = (userName, repositoryName, branch, allowedUser).mapTo[ProtectedBranchRestriction]
def byPrimaryKey(userName: String, repositoryName: String, branch: String, allowedUser: String): Rep[Boolean] =
this.userName === userName.bind && this.repositoryName === repositoryName.bind && this.branch === branch.bind && this.allowedUser === allowedUser.bind
}
}
case class ProtectedBranch(userName: String, repositoryName: String, branch: String, statusCheckAdmin: Boolean)
case class ProtectedBranch(
userName: String,
repositoryName: String,
branch: String,
enforceAdmins: Boolean,
requiredStatusCheck: Boolean,
restrictions: Boolean
)
case class ProtectedBranchContext(userName: String, repositoryName: String, branch: String, context: String)
case class ProtectedBranchRestriction(userName: String, repositoryName: String, branch: String, allowedUser: String)

View File

@@ -1,9 +1,10 @@
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 gitbucket.core.model.Profile.*
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.{CommitState, ProtectedBranch, ProtectedBranchContext, ProtectedBranchRestriction, Role}
import gitbucket.core.util.SyntaxSugars.*
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
trait ProtectedBranchService {
@@ -13,17 +14,27 @@ trait ProtectedBranchService {
): 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) }
.on { case pb ~ c => pb.byBranch(c.userName, c.repositoryName, c.branch) }
.joinLeft(ProtectedBranchRestrictions)
.on { case pb ~ c ~ r => pb.byBranch(r.userName, r.repositoryName, r.branch) }
.map { case pb ~ c ~ r => pb -> (c.map(_.context), r.map(_.allowedUser)) }
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
.headOption
.map { p =>
p._1 -> p._2.flatMap(_._2)
.map { (p: (ProtectedBranch, List[(ProtectedBranch, (Option[String], Option[String]))])) =>
p._1 -> (p._2.flatMap(_._2._1), p._2.flatMap(_._2._2))
}
.map { case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin)
.map { case (t1, (contexts, users)) =>
new ProtectedBranchInfo(
t1.userName,
t1.repositoryName,
t1.branch,
true,
if (t1.requiredStatusCheck) Some(contexts) else None,
t1.enforceAdmins,
if (t1.restrictions) Some(users) else None
)
}
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit
@@ -40,19 +51,32 @@ trait ProtectedBranchService {
owner: String,
repository: String,
branch: String,
includeAdministrators: Boolean,
contexts: Seq[String]
enforceAdmins: Boolean,
requiredStatusCheck: Boolean,
contexts: Seq[String],
restrictions: Boolean,
restrictionsUsers: 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))
ProtectedBranches.insert(
ProtectedBranch(owner, repository, branch, enforceAdmins, requiredStatusCheck, restrictions)
)
if (restrictions) {
restrictionsUsers.foreach { user =>
ProtectedBranchRestrictions.insert(ProtectedBranchRestriction(owner, repository, branch, user))
}
}
if (requiredStatusCheck) {
contexts.foreach { context =>
ProtectedBranchContexts.insert(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 {
@@ -101,6 +125,7 @@ object ProtectedBranchService {
)
}
} else {
println("-> else")
None
}
}
@@ -117,12 +142,16 @@ object ProtectedBranchService {
* 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],
contexts: Option[Seq[String]],
/**
* Include administrators
* Enforce required status checks for repository administrators.
*/
includeAdministrators: Boolean
enforceAdmins: Boolean,
/**
* Users who can push to the branch.
*/
restrictionsUsers: Option[Seq[String]]
) extends AccountService
with RepositoryService
with CommitStatusService {
@@ -148,42 +177,66 @@ object ProtectedBranchService {
session: Session
): Option[String] = {
if (enabled) {
command.getType() match {
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 !isPushAllowed(pusher) =>
Some("You do not have permission to push to this 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 s if s.sizeIs == 1 => Some(s"""Required status check "${s.head}" is expected""")
case s if s.sizeIs >= 1 =>
Some(s"${s.size} of ${contexts.map(_.size).getOrElse(0)} required status checks are expected")
case _ => None
}
case ReceiveCommand.Type.DELETE =>
Some("Cannot delete a protected branch")
Some("You do not have permission to push to this 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 unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = {
contexts match {
case None => Set.empty
case Some(x) if x.isEmpty => Set.empty
case Some(x) =>
x.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
case _ if !enabled => false
case _ if contexts.isEmpty => false
case _ if enforceAdmins => true
case p if isAdministrator(p) => false
case _ => true
}
def isPushAllowed(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled || restrictionsUsers.isEmpty => true
case _ if restrictionsUsers.get.contains(pusher) => true
case p if isAdministrator(p) && enforceAdmins => false
case _ => false
}
}
object ProtectedBranchInfo {
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo =
ProtectedBranchInfo(owner, repository, branch, false, Nil, false)
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo = {
ProtectedBranchInfo(
owner,
repository,
branch,
enabled = false,
contexts = None,
enforceAdmins = false,
restrictionsUsers = None
)
}
}
}

View File

@@ -653,18 +653,18 @@ object PullRequestService {
commitIdTo: String
) {
val hasConflict = conflictMessage.isDefined
val hasConflict: Boolean = conflictMessage.isDefined
val statuses: List[CommitStatus] =
commitStatuses ++ (branchProtection.contexts.toSet -- commitStatuses.map(_.context).toSet)
commitStatuses ++ (branchProtection.contexts.getOrElse(Nil).toSet -- commitStatuses.map(_.context).toSet)
.map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context =>
statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS)
)
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
val hasRequiredStatusProblem: Boolean = needStatusCheck && branchProtection.contexts
.getOrElse(Nil)
.exists(context => !statuses.find(_.context == context).map(_.state).contains(CommitState.SUCCESS))
val hasProblem: Boolean = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(
statuses.map(_.state).toSet
) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
val canUpdate: Boolean = branchIsOutOfDate && !hasConflict
val canMerge: Boolean = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary: (CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
@@ -672,8 +672,8 @@ object PullRequestService {
state -> summary
}
lazy val statusesAndRequired: List[(CommitStatus, Boolean)] = statuses.map { s =>
s -> branchProtection.contexts.contains(s.context)
s -> branchProtection.contexts.getOrElse(Nil).contains(s.context)
}
lazy val isAllSuccess = commitStateSummary._1 == CommitState.SUCCESS
lazy val isAllSuccess: Boolean = commitStateSummary._1 == CommitState.SUCCESS
}
}

View File

@@ -1,7 +1,7 @@
package gitbucket.core.service
import gitbucket.core.api.JsonFormat
import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService.SystemSettings
@@ -11,14 +11,14 @@ import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.{JGitUtil, LockUtil}
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
import org.eclipse.jgit.lib._
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
import scala.util.Using
trait RepositoryCommitFileService {
self: AccountService & ActivityService & IssuesService & PullRequestService & WebHookPullRequestService &
RepositoryService =>
RepositoryService & ProtectedBranchService =>
/**
* Create multiple files by callback function.
@@ -92,10 +92,10 @@ trait RepositoryCommitFileService {
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, Option[ObjectId])] = {
val newPath = newFileName.map { newFileName =>
if (path.length == 0) newFileName else s"${path}/${newFileName}"
if (path.isEmpty) newFileName else s"${path}/${newFileName}"
}
val oldPath = oldFileName.map { oldFileName =>
if (path.length == 0) oldFileName else s"${path}/${oldFileName}"
if (path.isEmpty) oldFileName else s"${path}/${oldFileName}"
}
_createFiles(repository, branch, message, pusherAccount, committerName, committerMailAddress, settings) {
@@ -139,7 +139,6 @@ trait RepositoryCommitFileService {
)(
f: (Git, ObjectId, DirCacheBuilder, ObjectInserter) => R
)(implicit s: Session, c: JsonFormat.Context): Either[String, (ObjectId, R)] = {
LockUtil.lock(s"${repository.owner}/${repository.name}") {
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
val builder = DirCache.newInCore.builder()
@@ -168,7 +167,14 @@ trait RepositoryCommitFileService {
// call pre-commit hook
val error = PluginRegistry().getReceiveHooks.flatMap { hook =>
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, pusherAccount.userName, false)
hook.preReceive(
repository.owner,
repository.name,
receivePack,
receiveCommand,
pusherAccount.userName,
mergePullRequest = false
)
}.headOption
error match {
@@ -194,7 +200,8 @@ trait RepositoryCommitFileService {
// record activity
updateLastActivityDate(repository.owner, repository.name)
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
val pushInfo = PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
val pushInfo =
PushInfo(repository.owner, repository.name, pusherAccount.userName, branch, List(commitInfo))
recordActivity(pushInfo)
// create issue comment by commit message
@@ -221,7 +228,14 @@ trait RepositoryCommitFileService {
// call post-commit hook
PluginRegistry().getReceiveHooks.foreach { hook =>
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false)
hook.postReceive(
repository.owner,
repository.name,
receivePack,
receiveCommand,
committerName,
mergePullRequest = false
)
}
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))

View File

@@ -654,11 +654,11 @@ trait RepositoryService {
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if (a.isAdmin) => true
case Some(a) if (a.userName == owner) => true
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
case _ => false
case Some(a) if a.isAdmin => true
case Some(a) if a.userName == owner => true
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a) if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName) => true
case _ => false
}
}
@@ -666,11 +666,11 @@ trait RepositoryService {
s: Session
): Boolean = {
loginAccount match {
case Some(a) if (a.isAdmin) => true
case Some(a) if (a.userName == owner) => true
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if a.isAdmin => true
case Some(a) if a.userName == owner => true
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a)
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName)) =>
if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER)).contains(a.userName) =>
true
case _ => false
}
@@ -678,12 +678,12 @@ trait RepositoryService {
def hasGuestRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if (a.isAdmin) => true
case Some(a) if (a.userName == owner) => true
case Some(a) if (getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if a.isAdmin => true
case Some(a) if a.userName == owner => true
case Some(a) if getGroupMembers(owner).exists(_.userName == a.userName) => true
case Some(a)
if (getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
.contains(a.userName)) =>
if getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN, Role.DEVELOPER, Role.GUEST))
.contains(a.userName) =>
true
case _ => false
}
@@ -694,17 +694,29 @@ trait RepositoryService {
true
} else {
loginAccount match {
case Some(x) if (x.isAdmin) => true
case Some(x) if (repository.userName == x.userName) => true
case Some(x) if (getGroupMembers(repository.userName).exists(_.userName == x.userName)) => true
case Some(x)
if (getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName)) =>
case Some(x) if x.isAdmin => true
case Some(x) if repository.userName == x.userName => true
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
case Some(x) if getCollaboratorUserNames(repository.userName, repository.repositoryName).contains(x.userName) =>
true
case _ => false
}
}
}
def isWritable(repository: Repository, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(x) if x.isAdmin => true
case Some(x) if repository.userName == x.userName => true
case Some(x) if getGroupMembers(repository.userName).exists(_.userName == x.userName) => true
case Some(x)
if getCollaboratorUserNames(repository.userName, repository.repositoryName, Seq(Role.ADMIN, Role.DEVELOPER))
.contains(x.userName) =>
true
case _ => false
}
}
private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int =
Query(Repositories.filter { t =>
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)

View File

@@ -9,11 +9,11 @@ import gitbucket.core.api.JsonFormat.Context
import gitbucket.core.model.WebHook
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.WebHookService.*
import gitbucket.core.service.*
import gitbucket.core.util.Implicits.*
import gitbucket.core.util.*
import gitbucket.core.model.Profile.profile.blockingApi.*
import gitbucket.core.model.activity.{
BaseActivityInfo,
CloseIssueInfo,
@@ -33,9 +33,9 @@ import gitbucket.core.servlet.Database
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.http.server.GitServlet
import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport._
import org.eclipse.jgit.transport.resolver._
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.transport.*
import org.eclipse.jgit.transport.resolver.*
import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
@@ -43,7 +43,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.internal.storage.file.FileRepository
import org.json4s.Formats
import org.json4s.convertToJsonInput
import org.json4s.jackson.Serialization._
import org.json4s.jackson.Serialization.*
/**
* Provides Git repository via HTTP.
@@ -117,7 +117,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
GitLfs.BatchResponseObject(
requestObject.oid,
requestObject.size,
true,
authenticated = true,
GitLfs.Actions(
upload = Some(
GitLfs.Action(
@@ -138,7 +138,7 @@ class GitRepositoryServlet extends GitServlet with SystemSettingsService {
GitLfs.BatchResponseObject(
requestObject.oid,
requestObject.size,
true,
authenticated = true,
GitLfs.Actions(
download = Some(
GitLfs.Action(
@@ -223,7 +223,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
}
}
import scala.jdk.CollectionConverters._
import scala.jdk.CollectionConverters.*
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook
@@ -242,6 +242,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
with WebHookPullRequestReviewCommentService
with CommitsService
with SystemSettingsService
with ProtectedBranchService
with RequestCache {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
@@ -253,7 +254,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
commands.asScala.foreach { command =>
// call pre-commit hook
PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, false))
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
.headOption
.foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
@@ -428,8 +429,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
repositoryInfo,
newCommits,
ownerAccount,
newId = command.getNewId(),
oldId = command.getOldId()
newId = command.getNewId,
oldId = command.getOldId
)
}
}
@@ -453,7 +454,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// call post-commit hook
PluginRegistry().getReceiveHooks
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, false))
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, mergePullRequest = false))
}
}
// update repository last modified time.

View File

@@ -15,9 +15,9 @@ trait OneselfAuthenticator { self: ControllerBase =>
private def authenticate(action: => Any) = {
context.loginAccount match {
case Some(x) if (x.isAdmin) => action
case Some(x) if (request.paths(0) == x.userName) => action
case _ => Unauthorized()
case Some(x) if x.isAdmin => action
case Some(x) if request.paths(0) == x.userName => action
case _ => Unauthorized()
}
}
}
@@ -26,7 +26,7 @@ trait OneselfAuthenticator { self: ControllerBase =>
* Allows only the repository owner and administrators.
*/
trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def ownerOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def ownerOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def ownerOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = {
@@ -34,14 +34,14 @@ trait OwnerAuthenticator { self: ControllerBase & RepositoryService & AccountSer
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
context.loginAccount match {
case Some(x) if (x.isAdmin) => action(repository)
case Some(x) if (repository.owner == x.userName) => action(repository)
case Some(x) if x.isAdmin => action(repository)
case Some(x) if repository.owner == x.userName => action(repository)
// TODO Repository management is allowed for only group managers?
case Some(x) if (getGroupMembers(repository.owner).exists { m =>
case Some(x) if getGroupMembers(repository.owner).exists { m =>
m.userName == x.userName && m.isManager
}) =>
} =>
action(repository)
case Some(x) if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName)) =>
case Some(x) if getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN)).contains(x.userName) =>
action(repository)
case _ => Unauthorized()
}
@@ -83,10 +83,10 @@ trait AdminAuthenticator { self: ControllerBase =>
* Allows only guests and signed in users who can access the repository.
*/
trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def referrersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def referrersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def referrersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => { authenticate(action(form, _)) }
private def authenticate(action: (RepositoryInfo) => Any) = {
private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner")
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
@@ -103,12 +103,12 @@ trait ReferrerAuthenticator { self: ControllerBase & RepositoryService & Account
* Allows only signed in users who have read permission for the repository.
*/
trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def readableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def readableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def readableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
authenticate(action(form, _))
}
private def authenticate(action: (RepositoryInfo) => Any) = {
private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner")
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
@@ -125,24 +125,19 @@ trait ReadableUsersAuthenticator { self: ControllerBase & RepositoryService & Ac
* Allows only signed in users who have write permission for the repository.
*/
trait WritableUsersAuthenticator { self: ControllerBase & RepositoryService & AccountService =>
protected def writableUsersOnly(action: (RepositoryInfo) => Any) = { authenticate(action) }
protected def writableUsersOnly(action: RepositoryInfo => Any) = { authenticate(action) }
protected def writableUsersOnly[T](action: (T, RepositoryInfo) => Any) = (form: T) => {
authenticate(action(form, _))
}
private def authenticate(action: (RepositoryInfo) => Any) = {
private def authenticate(action: RepositoryInfo => Any) = {
val userName = params("owner")
val repoName = params("repository")
getRepository(userName, repoName).map { repository =>
context.loginAccount match {
case Some(x) if (x.isAdmin) => action(repository)
case Some(x) if (userName == x.userName) => action(repository)
case Some(x) if (getGroupMembers(repository.owner).exists(_.userName == x.userName)) => action(repository)
case Some(x)
if (getCollaboratorUserNames(userName, repoName, Seq(Role.ADMIN, Role.DEVELOPER))
.contains(x.userName)) =>
action(repository)
case _ => Unauthorized()
if (isWritable(repository.repository, context.loginAccount)) {
action(repository)
} else {
Unauthorized()
}
} getOrElse NotFound()
}
@@ -159,9 +154,9 @@ trait GroupManagerAuthenticator { self: ControllerBase & AccountService =>
context.loginAccount match {
case Some(x) if x.isAdmin => action
case Some(x) if x.userName == request.paths(0) => action
case Some(x) if (getGroupMembers(request.paths(0)).exists { member =>
case Some(x) if getGroupMembers(request.paths(0)).exists { member =>
member.userName == x.userName && member.isManager
}) =>
} =>
action
case _ => Unauthorized()
}

View File

@@ -1,6 +1,6 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
branch: String,
protection: gitbucket.core.api.ApiBranchProtection,
protection: gitbucket.core.api.ApiBranchProtectionResponse,
knownContexts: Seq[String],
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@@ -22,9 +22,44 @@
</label>
<p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p>
</div>
<!--====================================================================-->
<!-- Enforce administrators -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off") @if(knownContexts.isEmpty){disabled }>
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.enforce_admins.exists(_.enabled))>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce restrictions even for repository administrators.</p>
</div>
<!--====================================================================-->
<!-- Push restrictions -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="restrictions" onclick="update()" @check(protection.restrictions.isDefined)>
<span class="strong">Restrict users for push</span>
</label>
<p class="help-block">Restrict users who can push to this branch</p>
<div class="js-restrictions_enabled" style="display: none;">
<ul id="restrictions-user-list">
</ul>
@gitbucket.core.helper.html.account("userName-restrictions-user", 200, true, false)
<input type="button" class="btn btn-default add-restrictions-user" value="Add"/>
<div>
<span class="error" id="error-restrictions-user"></span>
</div>
</div>
</div>
<!--====================================================================-->
<!-- Status check -->
<!--====================================================================-->
<div class="checkbox js-enabled" style="display:none">
<label>
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.required_status_checks.isDefined) @if(knownContexts.isEmpty){disabled }>
<span class="strong">Require status checks to pass before merging</span>
</label>
<p class="help-block">When enabled, commits must first be pushed to another branch, then merged or pushed directly to <b>@branch</b> after status checks have passed.</p>
@@ -48,14 +83,6 @@
}
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>
<span class="strong">Include administrators</span>
</label>
<p class="help-block">Enforce required status checks for repository administrators.</p>
</div>
</div>
}
</div>
@@ -68,59 +95,114 @@
}
<script>
function getValue(){
var v = {}, contexts=[];
const v = {}, contexts = [];
let restrictions = undefined;
$("input[type=checkbox]:checked").each(function(){
if(this.name === 'contexts'){
if(this.name === 'contexts') {
contexts.push(this.value);
} else if (this.name === 'restrictions') {
restrictions = $('#restrictions-user-list li').map(function(i, e){
return $(e).data('name');
}).get();
} else {
v[this.name] = true;
}
});
if(v.enabled){
return {
enabled: true,
required_status_checks: {
enforcement_level: v.has_required_statuses ? ((v.enforce_for_admins ? 'everyone' : 'non_admins')) : 'off',
contexts: v.has_required_statuses ? contexts : []
}
};
enforce_admins: v.enforce_for_admins,
required_status_checks: v.has_required_statuses ? { contexts: contexts } : undefined,
restrictions: restrictions ? { users: restrictions } : undefined
};
} else {
return {
enabled: false,
required_status_checks: {
enforcement_level: "off",
contexts: []
}
enabled: false
};
}
}
function updateView(protection){
$('.js-enabled').toggle(protection.enabled);
$('.js-has_required_statuses').toggle(protection.required_status_checks.enforcement_level != 'off');
$('.js-submit-btn').attr('disabled',protection.required_status_checks.enforcement_level != 'off' && protection.required_status_checks.contexts.length == 0);
$('.js-restrictions_enabled').toggle(protection.restrictions !== undefined);
$('.js-has_required_statuses').toggle(protection.required_status_checks !== undefined);
}
function update(){
var protection = getValue();
const protection = getValue();
updateView(protection);
}
$(update);
function submitForm(e){
e.stopPropagation();
e.preventDefault();
var protection = getValue();
const protection = getValue();
$.ajax({
method:'PATCH',
url:'@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
method: 'PATCH',
url: '@context.path/api/v3/repos/@repository.owner/@repository.name/branches/@helpers.urlEncode(branch)',
contentType: 'application/json',
dataType: 'json',
data:JSON.stringify({protection:protection}),
success:function(r){
data: JSON.stringify({protection: protection}),
success: function(r){
$('#saved-info').show();
},
error:function(err){
error: function(err){
console.log(err);
alert('update error');
}
});
}
function addUserToListHTML(userName, id){
$(id).append($('<li>').data('name', userName)
.append(' ')
.append(userName)
.append($('<a href="#" onclick="$(this).parent().remove();" class="remove">(remove)</a>')));
}
$(function() {
// Initialize
update();
@protection.restrictions.map(_.users).map { users =>
@users.map { user =>
addUserToListHTML('@user', '#restrictions-user-list');
}
}
$('.add-restrictions-user').click(function(){
$('#error-restrictions-user').text('');
const userName = $('#userName-restrictions-user').val();
// check empty
if($.trim(userName) === ''){
return false;
}
// check duplication
const exists = $('#restrictions-user-list li').filter(function(){
return $(this).data('name') === userName;
}).length > 0;
if(exists){
$('#error-restrictions-user').text('User has been already added.');
return false;
}
// check existence
$.post('@context.path/_user/existence', {
'userName': userName,
'owner': '@repository.owner',
'repository': '@repository.name'
},
function(data, status){
if(data !== ''){
addUserToListHTML(userName, '#restrictions-user-list');
$('#userName-restrictions-user').val('');
} else {
$('#error-restrictions-user').text("User does not exist or isn't writable to this repository.");
}
});
});
})
</script>

View File

@@ -1,8 +1,9 @@
package gitbucket.core.api
import java.util.{Calendar, Date, TimeZone}
import gitbucket.core.api.ApiBranchProtectionResponse.Restrictions
import gitbucket.core.model._
import java.util.{Calendar, Date, TimeZone}
import gitbucket.core.model.*
import gitbucket.core.plugin.PluginInfo
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchInfo
import gitbucket.core.service.RepositoryService.RepositoryInfo
@@ -15,7 +16,7 @@ object ApiSpecModels {
implicit val context: JsonFormat.Context = JsonFormat.Context("http://gitbucket.exmple.com", None)
val date1 = {
val date1: Date = {
val d = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
d.set(2011, 3, 14, 16, 0, 49)
d.getTime
@@ -29,7 +30,7 @@ object ApiSpecModels {
// Models
val account = Account(
val account: Account = Account(
userName = "octocat",
fullName = "octocat",
mailAddress = "octocat@example.com",
@@ -45,10 +46,10 @@ object ApiSpecModels {
description = None
)
val sha1 = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name = RepositoryName("octocat/Hello-World")
val sha1: String = "6dcb09b5b57875f334f61aebed695e2e4193db5e"
val repo1Name: RepositoryName = RepositoryName("octocat/Hello-World")
val repository = Repository(
val repository: Repository = Repository(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
isPrivate = false,
@@ -73,7 +74,7 @@ object ApiSpecModels {
)
)
val repositoryInfo = RepositoryInfo(
val repositoryInfo: RepositoryInfo = RepositoryInfo(
owner = repo1Name.owner,
name = repo1Name.name,
repository = repository,
@@ -101,7 +102,7 @@ object ApiSpecModels {
managers = Seq("myboss")
)
val label = Label(
val label: Label = Label(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
labelId = 10,
@@ -109,7 +110,7 @@ object ApiSpecModels {
color = "f29513"
)
val issue = Issue(
val issue: Issue = Issue(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
issueId = 1347,
@@ -124,14 +125,14 @@ object ApiSpecModels {
isPullRequest = false
)
val issuePR = issue.copy(
val issuePR: Issue = issue.copy(
title = "new-feature",
content = Some("Please pull these awesome changes"),
closed = true,
isPullRequest = true
)
val issueComment = IssueComment(
val issueComment: IssueComment = IssueComment(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
issueId = issue.issueId,
@@ -143,7 +144,7 @@ object ApiSpecModels {
updatedDate = date1
)
val pullRequest = PullRequest(
val pullRequest: PullRequest = PullRequest(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
issueId = issuePR.issueId,
@@ -156,7 +157,7 @@ object ApiSpecModels {
isDraft = true
)
val commitComment = CommitComment(
val commitComment: CommitComment = CommitComment(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
commitId = sha1,
@@ -174,7 +175,7 @@ object ApiSpecModels {
originalNewLine = None
)
val commitStatus = CommitStatus(
val commitStatus: CommitStatus = CommitStatus(
commitStatusId = 1,
userName = repo1Name.owner,
repositoryName = repo1Name.name,
@@ -188,7 +189,7 @@ object ApiSpecModels {
updatedDate = date1
)
val milestone = Milestone(
val milestone: Milestone = Milestone(
userName = repo1Name.owner,
repositoryName = repo1Name.name,
milestoneId = 1,
@@ -200,28 +201,28 @@ object ApiSpecModels {
// APIs
val apiUser = ApiUser(account)
val apiUser: ApiUser = ApiUser(account)
val apiRepository = ApiRepository(
val apiRepository: ApiRepository = ApiRepository(
repository = repository,
owner = apiUser,
forkedCount = repositoryInfo.forkedCount,
watchers = 0
)
val apiLabel = ApiLabel(
val apiLabel: ApiLabel = ApiLabel(
label = label,
repositoryName = repo1Name
)
val apiMilestone = ApiMilestone(
val apiMilestone: ApiMilestone = ApiMilestone(
repository = repository,
milestone = milestone,
open_issue_count = 1,
closed_issue_count = 1
)
val apiIssue = ApiIssue(
val apiIssue: ApiIssue = ApiIssue(
issue = issue,
repositoryName = repo1Name,
user = apiUser,
@@ -230,7 +231,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone)
)
val apiNotAssignedIssue = ApiIssue(
val apiNotAssignedIssue: ApiIssue = ApiIssue(
issue = issue,
repositoryName = repo1Name,
user = apiUser,
@@ -239,7 +240,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone)
)
val apiIssuePR = ApiIssue(
val apiIssuePR: ApiIssue = ApiIssue(
issue = issuePR,
repositoryName = repo1Name,
user = apiUser,
@@ -248,7 +249,7 @@ object ApiSpecModels {
milestone = Some(apiMilestone)
)
val apiComment = ApiComment(
val apiComment: ApiComment = ApiComment(
comment = issueComment,
repositoryName = repo1Name,
issueId = issueComment.issueId,
@@ -256,7 +257,7 @@ object ApiSpecModels {
isPullRequest = false
)
val apiCommentPR = ApiComment(
val apiCommentPR: ApiComment = ApiComment(
comment = issueComment,
repositoryName = repo1Name,
issueId = issueComment.issueId,
@@ -264,7 +265,7 @@ object ApiSpecModels {
isPullRequest = true
)
val apiPullRequest = ApiPullRequest(
val apiPullRequest: ApiPullRequest = ApiPullRequest(
issue = issuePR,
pullRequest = pullRequest,
headRepo = apiRepository,
@@ -275,15 +276,14 @@ object ApiSpecModels {
mergedComment = Some((issueComment, account))
)
// https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
val apiPullRequestReviewComment = ApiPullRequestReviewComment(
val apiPullRequestReviewComment: ApiPullRequestReviewComment = ApiPullRequestReviewComment(
comment = commitComment,
commentedUser = apiUser,
repositoryName = repo1Name,
issueId = commitComment.issueId.get
)
val commitInfo = (id: String) =>
val commitInfo: String => CommitInfo = (id: String) =>
CommitInfo(
id = id,
shortMessage = "short message",
@@ -299,12 +299,12 @@ object ApiSpecModels {
None
)
val apiCommitListItem = ApiCommitListItem(
val apiCommitListItem: ApiCommitListItem = ApiCommitListItem(
commit = commitInfo(sha1),
repositoryName = repo1Name
)
val apiCommit = {
val apiCommit: ApiCommit = {
val commit = commitInfo(sha1)
ApiCommit(
id = commit.id,
@@ -318,7 +318,7 @@ object ApiSpecModels {
)(repo1Name)
}
val apiCommits = ApiCommits(
val apiCommits: ApiCommits = ApiCommits(
repositoryName = repo1Name,
commitInfo = commitInfo(sha1),
diffs = Seq(
@@ -348,42 +348,45 @@ object ApiSpecModels {
commentCount = 2
)
val apiCommitStatus = ApiCommitStatus(
val apiCommitStatus: ApiCommitStatus = ApiCommitStatus(
status = commitStatus,
creator = apiUser
)
val apiCombinedCommitStatus = ApiCombinedCommitStatus(
val apiCombinedCommitStatus: ApiCombinedCommitStatus = ApiCombinedCommitStatus(
sha = sha1,
statuses = Iterable((commitStatus, account)),
repository = apiRepository
)
val apiBranchProtectionOutput = ApiBranchProtection(
val apiBranchProtectionOutput: ApiBranchProtectionResponse = ApiBranchProtectionResponse(
info = ProtectedBranchInfo(
owner = repo1Name.owner,
repository = repo1Name.name,
branch = "main",
enabled = true,
contexts = Seq("continuous-integration/travis-ci"),
includeAdministrators = true
contexts = Some(Seq("continuous-integration/travis-ci")),
enforceAdmins = true,
restrictionsUsers = Some(Seq("admin"))
)
)
val apiBranchProtectionInput = new ApiBranchProtection(
val apiBranchProtectionInput: ApiBranchProtectionResponse = new ApiBranchProtectionResponse(
url = None,
enabled = true,
required_status_checks = Some(
ApiBranchProtection.Status(
ApiBranchProtectionResponse.Status(
url = None,
enforcement_level = ApiBranchProtection.Everyone,
enforcement_level = ApiBranchProtectionResponse.Everyone,
contexts = Seq("continuous-integration/travis-ci"),
contexts_url = None
)
)
),
restrictions = Some(Restrictions(users = Seq("admin"))),
enforce_admins = None
)
val apiBranch = ApiBranch(
val apiBranch: ApiBranch = ApiBranch(
name = "main",
commit = ApiBranchCommit(sha1),
protection = apiBranchProtectionOutput
@@ -391,12 +394,12 @@ object ApiSpecModels {
repositoryName = repo1Name
)
val apiBranchForList = ApiBranchForList(
val apiBranchForList: ApiBranchForList = ApiBranchForList(
name = "main",
commit = ApiBranchCommit(sha1)
)
val apiContents = ApiContents(
val apiContents: ApiContents = ApiContents(
fileInfo = FileInfo(
id = ObjectId.fromString(sha1),
isDirectory = false,
@@ -413,14 +416,14 @@ object ApiSpecModels {
content = Some("README".getBytes("UTF-8"))
)
val apiEndPoint = ApiEndPoint()
val apiEndPoint: ApiEndPoint = ApiEndPoint()
val apiError = ApiError(
val apiError: ApiError = ApiError(
message = "A repository with this name already exists on this account",
documentation_url = Some("https://developer.github.com/v3/repos/#create")
)
val apiGroup = ApiGroup(
val apiGroup: ApiGroup = ApiGroup(
account.copy(
isAdmin = true,
isGroupAccount = true,
@@ -428,7 +431,7 @@ object ApiSpecModels {
)
)
val apiPlugin = ApiPlugin(
val apiPlugin: ApiPlugin = ApiPlugin(
plugin = PluginInfo(
pluginId = "gist",
pluginName = "Gist Plugin",
@@ -441,12 +444,12 @@ object ApiSpecModels {
)
)
val apiPusher = ApiPusher(account)
val apiPusher: ApiPusher = ApiPusher(account)
// have both urls as https, as the expected samples are using https
val gitHubContext = JsonFormat.Context("https://api.github.com", Some("https://api.github.com"))
val gitHubContext: JsonFormat.Context = JsonFormat.Context("https://api.github.com", Some("https://api.github.com"))
val apiRefHeadsMaster = ApiRef(
val apiRefHeadsMaster: ApiRef = ApiRef(
ref = "refs/heads/main",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/heads/main"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
@@ -457,7 +460,7 @@ object ApiSpecModels {
)
)
val apiRefTag = ApiRef(
val apiRefTag: ApiRef = ApiRef(
ref = "refs/tags/1.0",
url = ApiPath("/repos/gitbucket/gitbucket/git/refs/tags/1.0"),
node_id = "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
@@ -468,9 +471,9 @@ object ApiSpecModels {
)
)
val assetFileName = "010203040a0b0c0d"
val assetFileName: String = "010203040a0b0c0d"
val apiReleaseAsset = ApiReleaseAsset(
val apiReleaseAsset: ApiReleaseAsset = ApiReleaseAsset(
name = "release.zip",
size = 100
)(
@@ -479,7 +482,7 @@ object ApiSpecModels {
repositoryName = repo1Name
)
val apiRelease = ApiRelease(
val apiRelease: ApiRelease = ApiRelease(
name = "release1",
tag_name = "tag1",
body = Some("content"),
@@ -489,7 +492,7 @@ object ApiSpecModels {
// JSON String for APIs
val jsonUser = """{
val jsonUser: String = """{
|"login":"octocat",
|"email":"octocat@example.com",
|"type":"User",
@@ -501,7 +504,7 @@ object ApiSpecModels {
|"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar"
|}""".stripMargin
val jsonRepository = s"""{
val jsonRepository: String = s"""{
|"name":"Hello-World",
|"full_name":"octocat/Hello-World",
|"description":"This your first repo!",
@@ -519,10 +522,10 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World"
|}""".stripMargin
val jsonLabel =
val jsonLabel: String =
"""{"name":"bug","color":"f29513","url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/labels/bug"}"""
val jsonMilestone = """{
val jsonMilestone: String = """{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/milestones/1",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/milestone/1",
|"id":1,
@@ -535,7 +538,7 @@ object ApiSpecModels {
|"due_on":"2011-04-14T16:00:49Z"
|}""".stripMargin
val jsonIssue = s"""{
val jsonIssue: String = s"""{
|"number":1347,
|"title":"Found a bug",
|"user":$jsonUser,
@@ -552,7 +555,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin
val jsonNotAssignedIssue = s"""{
val jsonNotAssignedIssue: String = s"""{
|"number":1347,
|"title":"Found a bug",
|"user":$jsonUser,
@@ -568,7 +571,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin
val jsonIssuePR = s"""{
val jsonIssuePR: String = s"""{
|"number":1347,
|"title":"new-feature",
|"user":$jsonUser,
@@ -588,7 +591,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"}
|}""".stripMargin
val jsonPullRequest = s"""{
val jsonPullRequest: String = s"""{
|"number":1347,
|"state":"closed",
|"updated_at":"2011-04-14T16:00:49Z",
@@ -615,7 +618,7 @@ object ApiSpecModels {
|"statuses_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|}""".stripMargin
val jsonPullRequestReviewComment = s"""{
val jsonPullRequestReviewComment: String = s"""{
|"id":29724692,
|"path":"README.md",
|"commit_id":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -632,7 +635,7 @@ object ApiSpecModels {
|"pull_request":{"href":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347"}}
|}""".stripMargin
val jsonComment = s"""{
val jsonComment: String = s"""{
|"id":1,
|"user":$jsonUser,
|"body":"Me too",
@@ -641,7 +644,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347#comment-1"
|}""".stripMargin
val jsonCommentPR = s"""{
val jsonCommentPR: String = s"""{
|"id":1,
|"user":$jsonUser,
|"body":"Me too",
@@ -650,7 +653,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347#comment-1"
|}""".stripMargin
val jsonCommitListItem = s"""{
val jsonCommitListItem: String = s"""{
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"commit":{
|"message":"full message",
@@ -664,7 +667,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e"
|}""".stripMargin
val jsonCommit = (id: String) => s"""{
val jsonCommit: String => String = (id: String) => s"""{
|"id":"$id",
|"message":"full message",
|"timestamp":"2011-04-14T16:00:49Z",
@@ -677,7 +680,7 @@ object ApiSpecModels {
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/$id"
|}""".stripMargin
val jsonCommits = s"""{
val jsonCommits: String = s"""{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e",
@@ -707,7 +710,7 @@ object ApiSpecModels {
|"patch":"@@ -1 +1,2 @@\\n-body1\\n\\\\ No newline at end of file\\n+body1\\n+body2\\n\\\\ No newline at end of file"}]
|}""".stripMargin
val jsonCommitStatus = s"""{
val jsonCommitStatus: String = s"""{
|"created_at":"2011-04-14T16:00:49Z",
|"updated_at":"2011-04-14T16:00:49Z",
|"state":"success",
@@ -719,7 +722,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/statuses"
|}""".stripMargin
val jsonCombinedCommitStatus = s"""{
val jsonCombinedCommitStatus: String = s"""{
|"state":"success",
|"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e",
|"total_count":1,
@@ -728,7 +731,7 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/status"
|}""".stripMargin
val jsonBranchProtectionOutput =
val jsonBranchProtectionOutput: String =
"""{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection",
|"enabled":true,
@@ -736,15 +739,25 @@ object ApiSpecModels {
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks",
|"enforcement_level":"everyone",
|"contexts":["continuous-integration/travis-ci"],
|"contexts_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks/contexts"}
|"contexts_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/branches/main/protection/required_status_checks/contexts"
|},
|"restrictions":{
|"users":["admin"]
|},
|"enforce_admins":{
|"enabled":true
|}
|}""".stripMargin
val jsonBranchProtectionInput =
val jsonBranchProtectionInput: String =
"""{
|"enabled":true,
|"required_status_checks":{
|"enforcement_level":"everyone",
|"contexts":["continuous-integration/travis-ci"]
|},
|"restrictions":{
|"users":["admin"]
|}
|}""".stripMargin
@@ -757,9 +770,9 @@ object ApiSpecModels {
|"html":"http://gitbucket.exmple.com/octocat/Hello-World/tree/main"}
|}""".stripMargin
val jsonBranchForList = """{"name":"main","commit":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonBranchForList: String = """{"name":"main","commit":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonContents =
val jsonContents: String =
"""{
|"type":"file",
|"name":"README.md",
@@ -770,14 +783,14 @@ object ApiSpecModels {
|"download_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/doc/README.md"
|}""".stripMargin
val jsonEndPoint = """{"rate_limit_url":"http://gitbucket.exmple.com/api/v3/rate_limit"}"""
val jsonEndPoint: String = """{"rate_limit_url":"http://gitbucket.exmple.com/api/v3/rate_limit"}"""
val jsonError = """{
val jsonError: String = """{
|"message":"A repository with this name already exists on this account",
|"documentation_url":"https://developer.github.com/v3/repos/#create"
|}""".stripMargin
val jsonGroup = """{
val jsonGroup: String = """{
|"login":"octocat",
|"description":"Admin group",
|"created_at":"2011-04-14T16:00:49Z",
@@ -787,7 +800,7 @@ object ApiSpecModels {
|"avatar_url":"http://gitbucket.exmple.com/octocat/_avatar"
|}""".stripMargin
val jsonPlugin = """{
val jsonPlugin: String = """{
|"id":"gist",
|"name":"Gist Plugin",
|"version":"4.16.0",
@@ -795,12 +808,12 @@ object ApiSpecModels {
|"jarFileName":"gitbucket-gist-plugin-gitbucket_4.30.0-SNAPSHOT-4.17.0.jar"
|}""".stripMargin
val jsonPusher = """{"name":"octocat","email":"octocat@example.com"}"""
val jsonPusher: String = """{"name":"octocat","email":"octocat@example.com"}"""
// I checked all refs in gitbucket repo, and there appears to be only type "commit" and type "tag"
val jsonRef = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonRef: String = """{"ref":"refs/heads/featureA","object":{"sha":"6dcb09b5b57875f334f61aebed695e2e4193db5e"}}"""
val jsonRefHeadsMain =
val jsonRefHeadsMain: String =
"""{
|"ref": "refs/heads/main",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL2hlYWRzL21hc3Rlcg==",
@@ -812,7 +825,7 @@ object ApiSpecModels {
|}
|}""".stripMargin
val jsonRefTag =
val jsonRefTag: String =
"""{
|"ref": "refs/tags/1.0",
|"node_id": "MDM6UmVmOTM1MDc0NjpyZWZzL3RhZ3MvMS4w",
@@ -824,7 +837,7 @@ object ApiSpecModels {
|}
|}""".stripMargin
val jsonReleaseAsset =
val jsonReleaseAsset: String =
s"""{
|"name":"release.zip",
|"size":100,
@@ -833,7 +846,7 @@ object ApiSpecModels {
|"browser_download_url":"http://gitbucket.exmple.com/octocat/Hello-World/releases/tag1/assets/${assetFileName}"
|}""".stripMargin
val jsonRelease =
val jsonRelease: String =
s"""{
|"name":"release1",
|"tag_name":"tag1",

View File

@@ -1,19 +1,19 @@
package gitbucket.core.api
import org.json4s.Formats
import org.json4s.{Formats, JValue, jvalue2extractable}
import org.json4s.jackson.JsonMethods
import org.json4s.jvalue2extractable
import org.scalatest.Assertion
import org.scalatest.funsuite.AnyFunSuite
class JsonFormatSpec extends AnyFunSuite {
import ApiSpecModels._
import ApiSpecModels.*
implicit val format: Formats = JsonFormat.jsonFormats
private def expected(json: String) = json.replaceAll("\n", "")
def normalizeJson(json: String) = {
def normalizeJson(json: String): JValue = {
org.json4s.jackson.parseJson(json)
}
def assertEqualJson(actual: String, expected: String) = {
def assertEqualJson(actual: String, expected: String): Assertion = {
assert(normalizeJson(actual) == normalizeJson(expected))
}
@@ -57,7 +57,9 @@ class JsonFormatSpec extends AnyFunSuite {
assert(JsonFormat(apiBranchProtectionOutput) == expected(jsonBranchProtectionOutput))
}
test("deserialize apiBranchProtection") {
assert(JsonMethods.parse(jsonBranchProtectionInput).extract[ApiBranchProtection] == apiBranchProtectionInput)
assert(
JsonMethods.parse(jsonBranchProtectionInput).extract[ApiBranchProtectionResponse] == apiBranchProtectionInput
)
}
test("apiBranch") {
assert(JsonFormat(apiBranch) == expected(jsonBranch))

View File

@@ -29,26 +29,28 @@ class ProtectedBranchServiceSpec
it("should enable and update and disable") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(
getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo(
"user1",
"repo1",
"branch",
true,
Nil,
false
None,
false,
None
)
)
enableBranchProtection("user1", "repo1", "branch", true, Seq("hoge", "huge"))
enableBranchProtection("user1", "repo1", "branch", true, true, Seq("hoge", "huge"), false, Nil)
assert(
getProtectedBranchInfo("user1", "repo1", "branch") == ProtectedBranchInfo(
"user1",
"repo1",
"branch",
true,
Seq("hoge", "huge"),
true
enabled = true,
contexts = Some(Seq("hoge", "huge")),
enforceAdmins = true,
restrictionsUsers = None
)
)
disableBranchProtection("user1", "repo1", "branch")
@@ -57,21 +59,21 @@ class ProtectedBranchServiceSpec
)
}
}
it("should empty contexts is no-include-administrators") {
it("should empty contexts is include-administrators") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false)
enableBranchProtection("user1", "repo1", "branch", true, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators == false)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(!getProtectedBranchInfo("user1", "repo1", "branch").enforceAdmins)
enableBranchProtection("user1", "repo1", "branch", true, false, Nil, false, Nil)
assert(getProtectedBranchInfo("user1", "repo1", "branch").enforceAdmins)
}
}
it("getProtectedBranchList") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
enableBranchProtection("user1", "repo1", "branch", false, Nil)
enableBranchProtection("user1", "repo1", "branch2", false, Seq("fuga"))
enableBranchProtection("user1", "repo1", "branch3", true, Seq("hoge"))
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
enableBranchProtection("user1", "repo1", "branch2", false, false, Seq("fuga"), false, Nil)
enableBranchProtection("user1", "repo1", "branch3", true, false, Seq("hoge"), false, Nil)
assert(getProtectedBranchList("user1", "repo1").toSet == Set("branch", "branch2", "branch3"))
}
}
@@ -87,12 +89,12 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE_NONFASTFORWARD
)
generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"Cannot force-push to a protected branch"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Cannot force-push to a protected branch")
)
}
}
@@ -109,12 +111,12 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE_NONFASTFORWARD
)
generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"Cannot force-push to a protected branch"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Cannot force-push to a protected branch")
)
}
}
@@ -131,33 +133,33 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE
)
val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"Required status check \"must\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Required status check \"must\" is expected")
)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must", "must2"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == Some(
"Required status check \"must2\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user2", false)
.contains("Required status check \"must2\" is expected")
)
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false) == None)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
}
}
}
@@ -173,37 +175,60 @@ class ProtectedBranchServiceSpec
ReceiveCommand.Type.UPDATE
)
val user1 = generateNewUserWithDBRepository("user1", "repo1")
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must"), false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", true, true, Seq("must"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"Required status check \"must\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Required status check \"must\" is expected")
)
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2"))
enableBranchProtection("user1", "repo1", "branch", false, true, Seq("must", "must2"), false, Nil)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
enableBranchProtection("user1", "repo1", "branch", true, true, Seq("must", "must2"), false, Nil)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"2 of 2 required status checks are expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("2 of 2 required status checks are expected")
)
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
assert(
receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == Some(
"Required status check \"must2\" is expected"
)
receiveHook
.preReceive("user1", "repo1", rp, rc, "user1", false)
.contains("Required status check \"must2\" is expected")
)
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false) == None)
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user1", false).isEmpty)
}
}
}
it("should restrict push to allowed users only") {
withTestDB { implicit session =>
withTestRepository { git =>
val rp = new ReceivePack(git.getRepository)
rp.setAllowNonFastForwards(true)
val rc = new ReceiveCommand(
ObjectId.fromString(sha),
ObjectId.fromString(sha2),
"refs/heads/branch",
ReceiveCommand.Type.UPDATE
)
generateNewUserWithDBRepository("user1", "repo1")
generateNewAccount("user2")
enableBranchProtection("user1", "repo1", "branch", false, false, Nil, true, Seq("user2"))
assert(receiveHook.preReceive("user1", "repo1", rp, rc, "user2", false).isEmpty)
assert(
receiveHook
.preReceive("user1", "repo1", rp, rc, "user3", false)
.contains("You do not have permission to push to this branch")
)
}
}
}
@@ -212,29 +237,53 @@ class ProtectedBranchServiceSpec
it("administrator is owner") {
withTestDB { implicit session =>
generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, Nil, false)
assert(x.isAdministrator("user1") == true)
assert(x.isAdministrator("user2") == false)
val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
assert(x.isAdministrator("user1"))
assert(!x.isAdministrator("user2"))
}
}
it("administrator is manager") {
withTestDB { implicit session =>
val x = ProtectedBranchInfo("grp1", "repo1", "branch", true, Nil, false)
val x = ProtectedBranchInfo(
"grp1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
x.createGroup("grp1", None, None)
generateNewAccount("user1")
generateNewAccount("user2")
generateNewAccount("user3")
x.updateGroupMembers("grp1", List("user1" -> true, "user2" -> false))
assert(x.isAdministrator("user1") == true)
assert(x.isAdministrator("user2") == false)
assert(x.isAdministrator("user3") == false)
assert(x.isAdministrator("user1"))
assert(!x.isAdministrator("user2"))
assert(!x.isAdministrator("user3"))
}
}
it("unSuccessedContexts") {
withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, List("must"), false)
val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(List("must")),
enforceAdmins = false,
restrictionsUsers = None
)
assert(x.unSuccessedContexts(sha) == Set("must"))
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
assert(x.unSuccessedContexts(sha) == Set("must"))
@@ -251,7 +300,15 @@ class ProtectedBranchServiceSpec
it("unSuccessedContexts when empty") {
withTestDB { implicit session =>
val user1 = generateNewUserWithDBRepository("user1", "repo1")
val x = ProtectedBranchInfo("user1", "repo1", "branch", true, Nil, false)
val x = ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Nil),
enforceAdmins = false,
restrictionsUsers = None
)
val sha = "0c77148632618b59b6f70004e3084002be2b8804"
assert(x.unSuccessedContexts(sha) == Set())
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
@@ -261,23 +318,63 @@ class ProtectedBranchServiceSpec
it("if disabled, needStatusCheck is false") {
withTestDB { implicit session =>
assert(
ProtectedBranchInfo("user1", "repo1", "branch", false, Seq("must"), true).needStatusCheck("user1") == false
!ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = false,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user1")
)
}
}
it("needStatusCheck includeAdministrators") {
withTestDB { implicit session =>
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), false).needStatusCheck("user2") == true
ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = false,
restrictionsUsers = None
).needStatusCheck("user2")
)
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), false).needStatusCheck("user1") == false
!ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = false,
restrictionsUsers = None
).needStatusCheck("user1")
)
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), true).needStatusCheck("user2") == true
ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user2")
)
assert(
ProtectedBranchInfo("user1", "repo1", "branch", true, Seq("must"), true).needStatusCheck("user1") == true
ProtectedBranchInfo(
"user1",
"repo1",
"branch",
enabled = true,
contexts = Some(Seq("must")),
enforceAdmins = true,
restrictionsUsers = None
).needStatusCheck("user1")
)
}
}

View File

@@ -21,7 +21,7 @@ class RepositoryServiceSpec extends AnyFunSuite with ServiceSpecBase with Reposi
now = new java.util.Date
)
service.enableBranchProtection("root", "repo", "branch", true, Seq("must1", "must2"))
service.enableBranchProtection("root", "repo", "branch", true, true, Seq("must1", "must2"), false, Nil)
val orgPbi = service.getProtectedBranchInfo("root", "repo", "branch")
val org = service.getCommitStatus("root", "repo", id).get