diff --git a/src/main/scala/gitbucket/core/api/ApiBranch.scala b/src/main/scala/gitbucket/core/api/ApiBranch.scala new file mode 100644 index 0000000..cf6805b --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiBranch.scala @@ -0,0 +1,16 @@ +package gitbucket.core.api + +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)(repositoryName:RepositoryName) extends FieldSerializable { + def _links = Map( + "self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"), + "html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}")) +} diff --git a/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala b/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala new file mode 100644 index 0000000..85d2f4d --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiBranchProtection.scala @@ -0,0 +1,37 @@ +package gitbucket.core.api + +import gitbucket.core.service.ProtectedBrancheService +import org.json4s._ + +/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ +case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]){ + def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone) +} + +object ApiBranchProtection{ + /** form for enabling-and-disabling-branch-protection */ + case class EnablingAndDisabling(protection: ApiBranchProtection) + + def apply(info: Option[ProtectedBrancheService.ProtectedBranchInfo]): ApiBranchProtection = info match { + case None => ApiBranchProtection(false, Some(statusNone)) + case Some(info) => ApiBranchProtection(true, Some(Status(if(info.includeAdministrators){ Everyone }else{ NonAdmins }, info.requireStatusChecksToPass))) + } + val statusNone = Status(Off, Seq.empty) + case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String]) + 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") + + implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => ( + { + case JString("off") => Off + case JString("non_admins") => NonAdmins + case JString("everyone") => Everyone + }, + { + case x: EnforcementLevel => JString(x.name) + } + )) +} + diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index bd9c3bf..0733ba4 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -30,7 +30,8 @@ FieldSerializer[ApiCombinedCommitStatus]() + FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + - FieldSerializer[ApiComment]() + FieldSerializer[ApiComment]() + + ApiBranchProtection.enforcementLevelSerializer def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format => ( diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 46f4b83..e51b475 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -2,7 +2,7 @@ import gitbucket.core.settings.html import gitbucket.core.model.WebHook -import gitbucket.core.service.{RepositoryService, AccountService, WebHookService} +import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBrancheService, CommitStatusService} import gitbucket.core.service.WebHookService._ import gitbucket.core.util._ import gitbucket.core.util.JGitUtil._ @@ -18,11 +18,11 @@ class RepositorySettingsController extends RepositorySettingsControllerBase - with RepositoryService with AccountService with WebHookService + with RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService with OwnerAuthenticator with UsersAuthenticator trait RepositorySettingsControllerBase extends ControllerBase { - self: RepositoryService with AccountService with WebHookService + self: RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService with OwnerAuthenticator with UsersAuthenticator => // for repository options @@ -106,10 +106,12 @@ redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") }) + /** branch settings */ get("/:owner/:repository/settings/branches")(ownerOnly { repository => html.branches(repository, flash.get("info")) }); + /** Update default branch */ post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) => if(repository.branchList.find(_ == form.defaultBranch).isEmpty){ redirect(s"/${repository.owner}/${repository.name}/settings/options") @@ -124,6 +126,36 @@ } }) + /** Branch protection for branch */ + get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository => + import gitbucket.core.api._ + val branch = params("branch") + if(repository.branchList.find(_ == branch).isEmpty){ + redirect(s"/${repository.owner}/${repository.name}/settings/branches") + }else{ + val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) + val lastWeeks = getRecentCommitStatues(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).map(_.context).toSet + val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity) + html.brancheprotection(repository, branch, protection, knownContexts, flash.get("info")) + } + }); + + /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ + patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => + import gitbucket.core.api._ + (for{ + branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined + protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection) + } yield { + if(protection.enabled){ + enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts) + }else{ + disableBranchProtection(repository.owner, repository.name, branch) + } + JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository))) + }) getOrElse NotFound + }); + /** * Display the Collaborators page. */ diff --git a/src/main/scala/gitbucket/core/service/CommitStatusService.scala b/src/main/scala/gitbucket/core/service/CommitStatusService.scala index 2ebea2b..0e9a427 100644 --- a/src/main/scala/gitbucket/core/service/CommitStatusService.scala +++ b/src/main/scala/gitbucket/core/service/CommitStatusService.scala @@ -7,7 +7,7 @@ import gitbucket.core.util.Implicits._ import gitbucket.core.util.StringUtil._ import gitbucket.core.service.RepositoryService.RepositoryInfo - +import org.joda.time.LocalDateTime trait CommitStatusService { /** insert or update */ @@ -42,6 +42,10 @@ def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] = byCommitStatues(userName, repositoryName, sha).list + implicit val date2SqlDate = MappedColumnType.base[java.util.Date, java.sql.Timestamp]( d => new java.sql.Timestamp(d.getTime), d => new java.util.Date(d.getTime) ) + def getRecentCommitStatues(userName: String, repositoryName: String, time: java.util.Date)(implicit s: Session) :List[CommitStatus] = + CommitStatuses.filter(t => t.byRepository(userName, repositoryName)).filter(t => t.updatedDate > time.bind).list + def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] = byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts) .filter{ case (t,a) => t.creator === a.userName }.list diff --git a/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala b/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala index 572e081..4029801 100644 --- a/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala +++ b/src/main/scala/gitbucket/core/service/ProtectedBrancheService.scala @@ -10,16 +10,26 @@ import org.eclipse.jgit.lib.ObjectId +object MockDB{ + val data:scala.collection.mutable.Map[(String,String,String),(Boolean, Seq[String])] = scala.collection.mutable.Map(("root", "test58", "hoge2") -> (false, Seq.empty)) +} + trait ProtectedBrancheService { import ProtectedBrancheService._ def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = { // TODO: mock - if(owner == "root" && repository == "test58" && branch == "hoge2"){ - Some(new ProtectedBranchInfo(owner, repository, Seq.empty, false)) - }else{ - None + MockDB.data.get((owner, repository, branch)).map{ case (includeAdministrators, requireStatusChecksToPass) => + new ProtectedBranchInfo(owner, repository, requireStatusChecksToPass, includeAdministrators) } } + def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, requireStatusChecksToPass: Seq[String])(implicit session: Session): Unit = { + // TODO: mock + MockDB.data.put((owner, repository, branch), includeAdministrators -> requireStatusChecksToPass) + } + def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit = { + // TODO: mock + MockDB.data.remove((owner, repository, branch)) + } def getBranchProtectedReason(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = { val branch = command.getRefName.stripPrefix("refs/heads/") @@ -31,7 +41,7 @@ } } object ProtectedBrancheService { - class ProtectedBranchInfo( + case class ProtectedBranchInfo( owner: String, repository: String, /** diff --git a/src/main/twirl/gitbucket/core/settings/brancheprotection.scala.html b/src/main/twirl/gitbucket/core/settings/brancheprotection.scala.html new file mode 100644 index 0000000..a930ac1 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/brancheprotection.scala.html @@ -0,0 +1,110 @@ +@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo, + branch: String, + protection: gitbucket.core.api.ApiBranchProtection, + knownContexts: Seq[String], + info: Option[Any])(implicit context: gitbucket.core.controller.Context) +@import context._ +@import gitbucket.core.view.helpers._ +@import gitbucket.core.model.WebHook._ +@check(bool:Boolean)={@if(bool){ checked}} +@html.main(s"Branch protection for ${branch}", Some(repository)){ + @html.menu("settings", repository){ + @menu("branches", repository){ + @helper.html.information(info) +
+ + } + } +} + diff --git a/src/main/twirl/gitbucket/core/settings/branches.scala.html b/src/main/twirl/gitbucket/core/settings/branches.scala.html index ed57bbf..1bb2b32 100644 --- a/src/main/twirl/gitbucket/core/settings/branches.scala.html +++ b/src/main/twirl/gitbucket/core/settings/branches.scala.html @@ -30,6 +30,21 @@ + +Protect branches to disable force pushing, prevent branches from being deleted, and optionally require status checks before merging. New to protected branches?
+ +