Add git proection api and form

This commit is contained in:
nazoking
2015-12-03 19:42:24 +09:00
parent e187d026cc
commit 6661314be5
9 changed files with 248 additions and 10 deletions

View File

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

View File

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

View File

@@ -30,7 +30,8 @@ object JsonFormat {
FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]() +
FieldSerializer[ApiIssue]() +
FieldSerializer[ApiComment]()
FieldSerializer[ApiComment]() +
ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
(

View File

@@ -2,7 +2,7 @@ package gitbucket.core.controller
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 @@ import org.eclipse.jgit.lib.ObjectId
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
})
/** 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.
*/

View File

@@ -7,7 +7,7 @@ import gitbucket.core.model.{CommitState, CommitStatus, Account}
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 @@ trait CommitStatusService {
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

View File

@@ -10,16 +10,26 @@ import org.eclipse.jgit.transport.ReceivePack
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 @@ trait ProtectedBrancheService {
}
}
object ProtectedBrancheService {
class ProtectedBranchInfo(
case class ProtectedBranchInfo(
owner: String,
repository: String,
/**

View File

@@ -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)
<div class="alert alert-info" style="display:none" id="saved-info">Branch protection options saved</div>
<div class="box"><form name="branchProtection" onsubmit="submitForm(event)">
<div class="box-header">Branch protection for <b>@branch</b></div>
<div class="box-content-bottom">
<div class="checkbox">
<label class="strong"><input type="checkbox" name="enabled" onclick="update()" @check(protection.enabled)>Protect this branch</label>
<p class="help-block">Disables force-pushes to this branch and prevents it from being deleted.</p>
</div>
<div class="checkbox js-enabled" style="display:none">
<label class="strong"><input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off")>Require status checks to pass before merging</label>
<p class="help-block">Choose which status checks must pass before branches can be merged into test.
When enabled, commits must first be pushed to another branch, then merged or pushed directly to test after status checks have passed.</p>
<div class="js-has_required_statuses" style="display:none">
<div class="checkbox">
<label class="strong"><input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>Include administrators</label>
<p class="help-block">Enforce required status checks for repository administrators.</p>
</div>
<div class="box">
<div class="box-header">Status checks found in the last week for this repository</div>
<div class="box-content-bottom">
@knownContexts.map{ br =>
<label class="checkbox">
<input type="checkbox" name="contexts" value="@br" onclick="update()" @check(protection.status.contexts.find(_==br))>
<span>@br</span>
</label>
}
</div>
</div>
</div>
</div>
<input class="btn btn-success" type="submit" value="Save changes" />
</div>
</form></div>
}
}
}
<script>
function getValue(){
var v = {}, contexts=[];
$("input[type=checkbox]:checked").each(function(){
if(this.name == 'context'){
contexts.push(this.name);
}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 : []
}
};
}else{
return {
enabled: false,
required_status_checks: {
enforcement_level: "off",
contexts: []
}
};
}
}
function updateView(protection){
$('.js-enabled').toggle(protection.enabled);
$('.js-has_required_statuses').toggle(protection.required_status_checks.enforcement_level != 'off');
}
function update(){
var protection = getValue();
updateView(protection);
}
$(update);
function submitForm(e){
e.stopPropagation();
e.preventDefault();
var protection = getValue();
$.ajax({
method:'PATCH',
url:'/api/v3/repos/@repository.owner/@repository.name/branches/@encodeRefName(branch)',
contentType: 'application/json',
dataType: 'json',
data:JSON.stringify({protection:protection}),
success:function(r){
$('#saved-info').show();
},
error:function(err){
console.log(err);
alert('update error');
}
});
}
</script>

View File

@@ -30,6 +30,21 @@
<input type="submit" class="btn" value="Update" />
</form>
</div>
<div class="box-header">Protected branches</div>
<div class="box-content-bottom">
<p>Protect branches to disable force pushing, prevent branches from being deleted, and optionally require status checks before merging. New to protected branches?</p>
<fieldset class="margin">
<select name="protectBranch" id="protectBranch" onchange="location=$(this).val()">
<option>Choose a branch...</option>
@repository.branchList.map { branch =>
<option value="@url(repository)/settings/branches/@encodeRefName(branch)">@branch</option>
}
</select>
<span class="error" id="error-protectBranch"></span>
</fieldset>
</div>
</div>
}
}

View File

@@ -354,6 +354,16 @@ class JsonFormatSpec extends Specification {
}"""
val apiBranchProtection = ApiBranchProtection(true, Some(ApiBranchProtection.Status(ApiBranchProtection.Everyone, Seq("continuous-integration/travis-ci"))))
val apiBranchProtectionJson = """{
"enabled": true,
"required_status_checks": {
"enforcement_level": "everyone",
"contexts": [
"continuous-integration/travis-ci"
]
}
}"""
def beFormatted(json2Arg:String) = new Matcher[String] {
def apply[S <: String](e: Expectable[S]) = {
@@ -411,5 +421,8 @@ class JsonFormatSpec extends Specification {
"apiPullRequestReviewComment" in {
JsonFormat(apiPullRequestReviewComment) must beFormatted(apiPullRequestReviewCommentJson)
}
"apiBranchProtection" in {
JsonFormat(apiBranchProtection) must beFormatted(apiBranchProtectionJson)
}
}
}