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[ApiPullRequest.Commit]() +
|
||||
FieldSerializer[ApiIssue]() +
|
||||
FieldSerializer[ApiComment]()
|
||||
FieldSerializer[ApiComment]() +
|
||||
ApiBranchProtection.enforcementLevelSerializer
|
||||
|
||||
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
|
||||
(
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
/**
|
||||
|
||||
@@ -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" />
|
||||
</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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user