diff --git a/src/main/resources/update/gitbucket-core_4.44.xml b/src/main/resources/update/gitbucket-core_4.44.xml new file mode 100644 index 000000000..049740185 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.44.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + 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) + + + + + + + + + + + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index be7c9de91..128c1a83d 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -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) } diff --git a/src/main/scala/gitbucket/core/api/ApiBranch.scala b/src/main/scala/gitbucket/core/api/ApiBranch.scala index 629b2ac51..a6754e67c 100644 --- a/src/main/scala/gitbucket/core/api/ApiBranch.scala +++ b/src/main/scala/gitbucket/core/api/ApiBranch.scala @@ -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 = diff --git a/src/main/scala/gitbucket/core/api/ApiBranchProtectionRequest.scala b/src/main/scala/gitbucket/core/api/ApiBranchProtectionRequest.scala new file mode 100644 index 000000000..9d00f5a58 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiBranchProtectionRequest.scala @@ -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]) +} diff --git a/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala b/src/main/scala/gitbucket/core/api/ApiBranchProtectionResponse.scala similarity index 65% rename from src/main/scala/gitbucket/core/api/ApiBranchProtection.scala rename to src/main/scala/gitbucket/core/api/ApiBranchProtectionResponse.scala index 93d36aed4..74443bb5b 100644 --- a/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala +++ b/src/main/scala/gitbucket/core/api/ApiBranchProtectionResponse.scala @@ -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 => ( diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index ee3453420..12437eb0a 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -44,7 +44,7 @@ object JsonFormat { FieldSerializer[ApiCommits.File]() + FieldSerializer[ApiRelease]() + FieldSerializer[ApiReleaseAsset]() + - ApiBranchProtection.enforcementLevelSerializer + ApiBranchProtectionResponse.enforcementLevelSerializer def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](_ => diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala index 1fa3e49fa..9c2758074 100644 --- a/src/main/scala/gitbucket/core/controller/IndexController.scala +++ b/src/main/scala/gitbucket/core/controller/IndexController.scala @@ -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 "" }) diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index b598f1687..6a46c5338 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -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, diff --git a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryBranchControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryBranchControllerBase.scala index 0d7d4c236..f1f06d41a 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiRepositoryBranchControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiRepositoryBranchControllerBase.scala @@ -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() } }) diff --git a/src/main/scala/gitbucket/core/model/ProtectedBranch.scala b/src/main/scala/gitbucket/core/model/ProtectedBranch.scala index 09bc6002b..60ad90198 100644 --- a/src/main/scala/gitbucket/core/model/ProtectedBranch.scala +++ b/src/main/scala/gitbucket/core/model/ProtectedBranch.scala @@ -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) diff --git a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala index 326ca424e..cb9ee929b 100644 --- a/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala +++ b/src/main/scala/gitbucket/core/service/ProtectedBranchService.scala @@ -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 + ) + } } } diff --git a/src/main/scala/gitbucket/core/service/PullRequestService.scala b/src/main/scala/gitbucket/core/service/PullRequestService.scala index 49e471945..08348cedf 100644 --- a/src/main/scala/gitbucket/core/service/PullRequestService.scala +++ b/src/main/scala/gitbucket/core/service/PullRequestService.scala @@ -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 } } diff --git a/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala b/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala index dd38a0023..7fe17150e 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryCommitFileService.scala @@ -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)) diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 5ba21e2b1..e0df0a3bb 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -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) diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala index a971fa46d..06405add4 100644 --- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala @@ -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. diff --git a/src/main/scala/gitbucket/core/util/Authenticator.scala b/src/main/scala/gitbucket/core/util/Authenticator.scala index abd4eb5bc..ba838d27f 100644 --- a/src/main/scala/gitbucket/core/util/Authenticator.scala +++ b/src/main/scala/gitbucket/core/util/Authenticator.scala @@ -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() } diff --git a/src/main/twirl/gitbucket/core/settings/branchprotection.scala.html b/src/main/twirl/gitbucket/core/settings/branchprotection.scala.html index 5bd54131d..dc3940817 100644 --- a/src/main/twirl/gitbucket/core/settings/branchprotection.scala.html +++ b/src/main/twirl/gitbucket/core/settings/branchprotection.scala.html @@ -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 @@

Disables force-pushes to this branch and prevents it from being deleted.

+ + + + + + + + + + + + + + - -
- -

Enforce required status checks for repository administrators.

-
} @@ -68,59 +95,114 @@ } diff --git a/src/test/scala/gitbucket/core/api/ApiSpecModels.scala b/src/test/scala/gitbucket/core/api/ApiSpecModels.scala index 1adb50777..f2d466755 100644 --- a/src/test/scala/gitbucket/core/api/ApiSpecModels.scala +++ b/src/test/scala/gitbucket/core/api/ApiSpecModels.scala @@ -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", diff --git a/src/test/scala/gitbucket/core/api/JsonFormatSpec.scala b/src/test/scala/gitbucket/core/api/JsonFormatSpec.scala index a8f2c53e2..9c958b832 100644 --- a/src/test/scala/gitbucket/core/api/JsonFormatSpec.scala +++ b/src/test/scala/gitbucket/core/api/JsonFormatSpec.scala @@ -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)) diff --git a/src/test/scala/gitbucket/core/service/ProtectedBranchServiceSpec.scala b/src/test/scala/gitbucket/core/service/ProtectedBranchServiceSpec.scala index 80a1c5691..be56f2a87 100644 --- a/src/test/scala/gitbucket/core/service/ProtectedBranchServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/ProtectedBranchServiceSpec.scala @@ -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") ) } } diff --git a/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala b/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala index 1b3b9df77..d7cd25c95 100644 --- a/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala +++ b/src/test/scala/gitbucket/core/service/RepositoryServiceSpec.scala @@ -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