mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 14:35:52 +01:00
Add git proection api and form
This commit is contained in:
16
src/main/scala/gitbucket/core/api/ApiBranch.scala
Normal file
16
src/main/scala/gitbucket/core/api/ApiBranch.scala
Normal 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}"))
|
||||||
|
}
|
||||||
37
src/main/scala/gitbucket/core/api/ApiBranchProtection.scala
Normal file
37
src/main/scala/gitbucket/core/api/ApiBranchProtection.scala
Normal 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)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,8 @@ object JsonFormat {
|
|||||||
FieldSerializer[ApiCombinedCommitStatus]() +
|
FieldSerializer[ApiCombinedCommitStatus]() +
|
||||||
FieldSerializer[ApiPullRequest.Commit]() +
|
FieldSerializer[ApiPullRequest.Commit]() +
|
||||||
FieldSerializer[ApiIssue]() +
|
FieldSerializer[ApiIssue]() +
|
||||||
FieldSerializer[ApiComment]()
|
FieldSerializer[ApiComment]() +
|
||||||
|
ApiBranchProtection.enforcementLevelSerializer
|
||||||
|
|
||||||
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
|
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package gitbucket.core.controller
|
|||||||
|
|
||||||
import gitbucket.core.settings.html
|
import gitbucket.core.settings.html
|
||||||
import gitbucket.core.model.WebHook
|
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.service.WebHookService._
|
||||||
import gitbucket.core.util._
|
import gitbucket.core.util._
|
||||||
import gitbucket.core.util.JGitUtil._
|
import gitbucket.core.util.JGitUtil._
|
||||||
@@ -18,11 +18,11 @@ import org.eclipse.jgit.lib.ObjectId
|
|||||||
|
|
||||||
|
|
||||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||||
with RepositoryService with AccountService with WebHookService
|
with RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService
|
||||||
with OwnerAuthenticator with UsersAuthenticator
|
with OwnerAuthenticator with UsersAuthenticator
|
||||||
|
|
||||||
trait RepositorySettingsControllerBase extends ControllerBase {
|
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with WebHookService
|
self: RepositoryService with AccountService with WebHookService with ProtectedBrancheService with CommitStatusService
|
||||||
with OwnerAuthenticator with UsersAuthenticator =>
|
with OwnerAuthenticator with UsersAuthenticator =>
|
||||||
|
|
||||||
// for repository options
|
// for repository options
|
||||||
@@ -106,10 +106,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** branch settings */
|
||||||
get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
|
get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
|
||||||
html.branches(repository, flash.get("info"))
|
html.branches(repository, flash.get("info"))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Update default branch */
|
||||||
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
|
||||||
if(repository.branchList.find(_ == form.defaultBranch).isEmpty){
|
if(repository.branchList.find(_ == form.defaultBranch).isEmpty){
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/options")
|
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.
|
* Display the Collaborators page.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import gitbucket.core.model.{CommitState, CommitStatus, Account}
|
|||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.StringUtil._
|
import gitbucket.core.util.StringUtil._
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
|
import org.joda.time.LocalDateTime
|
||||||
|
|
||||||
trait CommitStatusService {
|
trait CommitStatusService {
|
||||||
/** insert or update */
|
/** insert or update */
|
||||||
@@ -42,6 +42,10 @@ trait CommitStatusService {
|
|||||||
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
|
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
|
||||||
byCommitStatues(userName, repositoryName, sha).list
|
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)] =
|
def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] =
|
||||||
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
|
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
|
||||||
.filter{ case (t,a) => t.creator === a.userName }.list
|
.filter{ case (t,a) => t.creator === a.userName }.list
|
||||||
|
|||||||
@@ -10,16 +10,26 @@ import org.eclipse.jgit.transport.ReceivePack
|
|||||||
import org.eclipse.jgit.lib.ObjectId
|
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 {
|
trait ProtectedBrancheService {
|
||||||
import ProtectedBrancheService._
|
import ProtectedBrancheService._
|
||||||
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = {
|
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] = {
|
||||||
// TODO: mock
|
// TODO: mock
|
||||||
if(owner == "root" && repository == "test58" && branch == "hoge2"){
|
MockDB.data.get((owner, repository, branch)).map{ case (includeAdministrators, requireStatusChecksToPass) =>
|
||||||
Some(new ProtectedBranchInfo(owner, repository, Seq.empty, false))
|
new ProtectedBranchInfo(owner, repository, requireStatusChecksToPass, includeAdministrators)
|
||||||
}else{
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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] = {
|
def getBranchProtectedReason(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
|
||||||
val branch = command.getRefName.stripPrefix("refs/heads/")
|
val branch = command.getRefName.stripPrefix("refs/heads/")
|
||||||
@@ -31,7 +41,7 @@ trait ProtectedBrancheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
object ProtectedBrancheService {
|
object ProtectedBrancheService {
|
||||||
class ProtectedBranchInfo(
|
case class ProtectedBranchInfo(
|
||||||
owner: String,
|
owner: String,
|
||||||
repository: String,
|
repository: String,
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -30,6 +30,21 @@
|
|||||||
<input type="submit" class="btn" value="Update" />
|
<input type="submit" class="btn" value="Update" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 beFormatted(json2Arg:String) = new Matcher[String] {
|
||||||
def apply[S <: String](e: Expectable[S]) = {
|
def apply[S <: String](e: Expectable[S]) = {
|
||||||
@@ -411,5 +421,8 @@ class JsonFormatSpec extends Specification {
|
|||||||
"apiPullRequestReviewComment" in {
|
"apiPullRequestReviewComment" in {
|
||||||
JsonFormat(apiPullRequestReviewComment) must beFormatted(apiPullRequestReviewCommentJson)
|
JsonFormat(apiPullRequestReviewComment) must beFormatted(apiPullRequestReviewCommentJson)
|
||||||
}
|
}
|
||||||
|
"apiBranchProtection" in {
|
||||||
|
JsonFormat(apiBranchProtection) must beFormatted(apiBranchProtectionJson)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user