mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 22:45:51 +01:00
Merge branch 'feature/protected-branch' of https://github.com/team-lab/gitbucket into team-lab-feature/protected-branch
This commit is contained in:
25
src/main/resources/update/3_10.sql
Normal file
25
src/main/resources/update/3_10.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
DROP TABLE IF EXISTS PROTECTED_BRANCH;
|
||||||
|
|
||||||
|
CREATE TABLE PROTECTED_BRANCH(
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
BRANCH VARCHAR(100) NOT NULL,
|
||||||
|
STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH);
|
||||||
|
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT;
|
||||||
|
CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT(
|
||||||
|
USER_NAME VARCHAR(100) NOT NULL,
|
||||||
|
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
||||||
|
BRANCH VARCHAR(100) NOT NULL,
|
||||||
|
CONTEXT VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT);
|
||||||
|
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
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}"))
|
||||||
|
}
|
||||||
47
src/main/scala/gitbucket/core/api/ApiBranchProtection.scala
Normal file
47
src/main/scala/gitbucket/core/api/ApiBranchProtection.scala
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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: ProtectedBrancheService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
|
||||||
|
enabled = info.enabled,
|
||||||
|
required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts)))
|
||||||
|
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")
|
||||||
|
object EnforcementLevel {
|
||||||
|
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){
|
||||||
|
if(includeAdministrators){
|
||||||
|
Everyone
|
||||||
|
}else{
|
||||||
|
NonAdmins
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
Off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 =>
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ abstract class ControllerBase extends ScalatraFilter
|
|||||||
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
|
||||||
with SystemSettingsService {
|
with SystemSettingsService {
|
||||||
|
|
||||||
implicit val jsonFormats = DefaultFormats
|
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||||
|
|
||||||
// TODO Scala 2.11
|
// TODO Scala 2.11
|
||||||
// // Don't set content type via Accept header.
|
// // Don't set content type via Accept header.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
import gitbucket.core.api._
|
||||||
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
|
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
|
||||||
import gitbucket.core.pulls.html
|
import gitbucket.core.pulls.html
|
||||||
import gitbucket.core.service.CommitStatusService
|
import gitbucket.core.service.CommitStatusService
|
||||||
import gitbucket.core.service.MergeService
|
import gitbucket.core.service.MergeService
|
||||||
@@ -27,13 +27,13 @@ import scala.collection.JavaConverters._
|
|||||||
class PullRequestsController extends PullRequestsControllerBase
|
class PullRequestsController extends PullRequestsControllerBase
|
||||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||||
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
with CommitStatusService with MergeService
|
with CommitStatusService with MergeService with ProtectedBrancheService
|
||||||
|
|
||||||
|
|
||||||
trait PullRequestsControllerBase extends ControllerBase {
|
trait PullRequestsControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||||
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||||
with CommitStatusService with MergeService =>
|
with CommitStatusService with MergeService with ProtectedBrancheService =>
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||||
|
|
||||||
@@ -119,7 +119,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
commits,
|
commits,
|
||||||
diffs,
|
diffs,
|
||||||
hasWritePermission(owner, name, context.loginAccount),
|
hasWritePermission(owner, name, context.loginAccount),
|
||||||
repository)
|
repository,
|
||||||
|
flash.toMap.map(f => f._1 -> f._2.toString))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
@@ -166,22 +167,34 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
val name = repository.name
|
val name = repository.name
|
||||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
|
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
|
||||||
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
|
|
||||||
checkConflict(owner, name, pullreq.branch, issueId)
|
checkConflict(owner, name, pullreq.branch, issueId)
|
||||||
}
|
}
|
||||||
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
val hasMergePermission = hasWritePermission(owner, name, context.loginAccount)
|
||||||
|
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
|
||||||
|
val mergeStatus = PullRequestService.MergeStatus(
|
||||||
|
hasConflict = hasConflict,
|
||||||
|
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
|
||||||
|
branchProtection = branchProtection,
|
||||||
|
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
|
||||||
|
needStatusCheck = context.loginAccount.map{ u =>
|
||||||
|
branchProtection.needStatusCheck(u.userName)
|
||||||
|
}.getOrElse(true),
|
||||||
|
hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
|
||||||
|
context.loginAccount.map{ u =>
|
||||||
|
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
|
||||||
|
}.getOrElse(false),
|
||||||
|
hasMergePermission = hasMergePermission,
|
||||||
|
commitIdTo = pullreq.commitIdTo)
|
||||||
html.mergeguide(
|
html.mergeguide(
|
||||||
hasConfrict,
|
mergeStatus,
|
||||||
hasProblem,
|
|
||||||
issue,
|
issue,
|
||||||
pullreq,
|
pullreq,
|
||||||
statuses,
|
|
||||||
repository,
|
repository,
|
||||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
||||||
}
|
}
|
||||||
@@ -203,6 +216,75 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository =>
|
||||||
|
(for {
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
loginAccount <- context.loginAccount
|
||||||
|
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
|
||||||
|
owner = pullreq.requestUserName
|
||||||
|
name = pullreq.requestRepositoryName
|
||||||
|
if hasWritePermission(owner, name, context.loginAccount)
|
||||||
|
} yield {
|
||||||
|
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
|
||||||
|
if(branchProtection.needStatusCheck(loginAccount.userName)){
|
||||||
|
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
|
||||||
|
} else {
|
||||||
|
val repository = getRepository(owner, name, context.baseUrl).get
|
||||||
|
LockUtil.lock(s"${owner}/${name}"){
|
||||||
|
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
|
||||||
|
pullreq.branch
|
||||||
|
}else{
|
||||||
|
s"${pullreq.userName}:${pullreq.branch}"
|
||||||
|
}
|
||||||
|
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
|
||||||
|
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
|
||||||
|
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
||||||
|
case None => // conflict
|
||||||
|
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
||||||
|
case Some(oldId) =>
|
||||||
|
// update pull request
|
||||||
|
updatePullRequests(owner, name, pullreq.requestBranch)
|
||||||
|
|
||||||
|
using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
|
||||||
|
// after update branch
|
||||||
|
|
||||||
|
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
|
||||||
|
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
|
||||||
|
|
||||||
|
commits.foreach{ commit =>
|
||||||
|
if(!existIds.contains(commit.id)){
|
||||||
|
createIssueComment(owner, name, commit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// record activity
|
||||||
|
recordPushActivity(owner, name, loginAccount.userName, pullreq.branch, commits)
|
||||||
|
|
||||||
|
// close issue by commit message
|
||||||
|
if(pullreq.requestBranch == repository.repository.defaultBranch){
|
||||||
|
commits.map{ commit =>
|
||||||
|
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call web hook
|
||||||
|
callPullRequestWebHookByRequestBranch("synchronize", repository, pullreq.requestBranch, baseUrl, loginAccount)
|
||||||
|
callWebHookOf(owner, name, WebHook.Push) {
|
||||||
|
for {
|
||||||
|
ownerAccount <- getAccountByUserName(owner)
|
||||||
|
} yield {
|
||||||
|
WebHookService.WebHookPushPayload(git, loginAccount, pullreq.requestBranch, repository, commits, ownerAccount, oldId = oldId, newId = newCommitId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
|
||||||
params("id").toIntOpt.flatMap { issueId =>
|
params("id").toIntOpt.flatMap { issueId =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
@@ -528,4 +610,15 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
repository,
|
repository,
|
||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
|
||||||
|
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
|
||||||
|
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
||||||
|
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,23 +18,29 @@ 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
|
||||||
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
|
case class OptionsForm(repositoryName: String, description: Option[String], isPrivate: Boolean)
|
||||||
|
|
||||||
val optionsForm = mapping(
|
val optionsForm = mapping(
|
||||||
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))),
|
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||||
"description" -> trim(label("Description" , optional(text()))),
|
"description" -> trim(label("Description" , optional(text()))),
|
||||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
|
||||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||||
)(OptionsForm.apply)
|
)(OptionsForm.apply)
|
||||||
|
|
||||||
|
// for default branch
|
||||||
|
case class DefaultBranchForm(defaultBranch: String)
|
||||||
|
|
||||||
|
val defaultBranchForm = mapping(
|
||||||
|
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
|
||||||
|
)(DefaultBranchForm.apply)
|
||||||
|
|
||||||
// for collaborator addition
|
// for collaborator addition
|
||||||
case class CollaboratorForm(userName: String)
|
case class CollaboratorForm(userName: String)
|
||||||
|
|
||||||
@@ -75,12 +81,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Save the repository options.
|
* Save the repository options.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||||
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
|
|
||||||
saveRepositoryOptions(
|
saveRepositoryOptions(
|
||||||
repository.owner,
|
repository.owner,
|
||||||
repository.name,
|
repository.name,
|
||||||
form.description,
|
form.description,
|
||||||
defaultBranch,
|
|
||||||
repository.repository.parentUserName.map { _ =>
|
repository.repository.parentUserName.map { _ =>
|
||||||
repository.repository.isPrivate
|
repository.repository.isPrivate
|
||||||
} getOrElse form.isPrivate
|
} getOrElse form.isPrivate
|
||||||
@@ -98,14 +102,61 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
|
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Change repository HEAD
|
|
||||||
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
|
|
||||||
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
|
|
||||||
}
|
|
||||||
flash += "info" -> "Repository settings has been updated."
|
flash += "info" -> "Repository settings has been updated."
|
||||||
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 =>
|
||||||
|
val protecteions = getProtectedBranchList(repository.owner, repository.name)
|
||||||
|
html.branches(repository, protecteions, 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")
|
||||||
|
}else{
|
||||||
|
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
|
||||||
|
// Change repository HEAD
|
||||||
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||||
|
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch)
|
||||||
|
}
|
||||||
|
flash += "info" -> "Repository default branch has been updated."
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 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 = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet
|
||||||
|
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
|
||||||
|
html.branchprotection(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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import gitbucket.core.util.ControlUtil._
|
|||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.model.{Account, CommitState, WebHook}
|
import gitbucket.core.model.{Account, CommitState, WebHook}
|
||||||
import gitbucket.core.service.CommitStatusService
|
|
||||||
import gitbucket.core.service.WebHookService._
|
import gitbucket.core.service.WebHookService._
|
||||||
import gitbucket.core.view
|
import gitbucket.core.view
|
||||||
import gitbucket.core.view.helpers
|
import gitbucket.core.view.helpers
|
||||||
@@ -34,7 +33,7 @@ import org.scalatra._
|
|||||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
|
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBrancheService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The repository viewer.
|
* The repository viewer.
|
||||||
@@ -42,7 +41,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
|||||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
|
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBrancheService =>
|
||||||
|
|
||||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||||
@@ -222,12 +221,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||||
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
|
||||||
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
|
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")),
|
||||||
|
protectedBranch)
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
|
||||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
|
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||||
|
|
||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||||
@@ -235,7 +237,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||||
val paths = path.split("/")
|
val paths = path.split("/")
|
||||||
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
|
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
|
||||||
JGitUtil.getContentInfo(git, path, objectId))
|
JGitUtil.getContentInfo(git, path, objectId),
|
||||||
|
protectedBranch)
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -486,6 +489,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
* Displays branches.
|
* Displays branches.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||||
|
val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet
|
||||||
val branches = JGitUtil.getBranches(
|
val branches = JGitUtil.getBranches(
|
||||||
owner = repository.owner,
|
owner = repository.owner,
|
||||||
name = repository.name,
|
name = repository.name,
|
||||||
@@ -493,7 +497,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
origin = repository.repository.originUserName.isEmpty
|
origin = repository.repository.originUserName.isEmpty
|
||||||
)
|
)
|
||||||
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
||||||
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
|
.map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name)))
|
||||||
.reverse
|
.reverse
|
||||||
|
|
||||||
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
|
||||||
|
|||||||
@@ -54,4 +54,9 @@ protected[model] trait TemplateComponent { self: Profile =>
|
|||||||
byRepository(userName, repositoryName) && (this.commitId === commitId)
|
byRepository(userName, repositoryName) && (this.commitId === commitId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait BranchTemplate extends BasicTemplate{ self: Table[_] =>
|
||||||
|
val branch = column[String]("BRANCH")
|
||||||
|
def byBranch(owner: String, repository: String, branchName: String) = byRepository(owner, repository) && (branch === branchName.bind)
|
||||||
|
def byBranch(owner: Column[String], repository: Column[String], branchName: Column[String]) = byRepository(owner, repository) && (this.branch === branchName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile =>
|
|||||||
val creator = column[String]("CREATOR")
|
val creator = column[String]("CREATOR")
|
||||||
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
|
||||||
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
val updatedDate = column[java.util.Date]("UPDATED_DATE")
|
||||||
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply)
|
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply)
|
||||||
def byPrimaryKey(id: Int) = commitStatusId === id.bind
|
def byPrimaryKey(id: Int) = commitStatusId === id.bind
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,20 @@ case class CommitStatus(
|
|||||||
registeredDate: java.util.Date,
|
registeredDate: java.util.Date,
|
||||||
updatedDate: java.util.Date
|
updatedDate: java.util.Date
|
||||||
)
|
)
|
||||||
|
object CommitStatus {
|
||||||
|
def pending(owner: String, repository: String, context: String) = CommitStatus(
|
||||||
|
commitStatusId = 0,
|
||||||
|
userName = owner,
|
||||||
|
repositoryName = repository,
|
||||||
|
commitId = "",
|
||||||
|
context = context,
|
||||||
|
state = CommitState.PENDING,
|
||||||
|
targetUrl = None,
|
||||||
|
description = Some("Waiting for status to be reported"),
|
||||||
|
creator = "",
|
||||||
|
registeredDate = new java.util.Date(),
|
||||||
|
updatedDate = new java.util.Date())
|
||||||
|
}
|
||||||
|
|
||||||
sealed abstract class CommitState(val name: String)
|
sealed abstract class CommitState(val name: String)
|
||||||
|
|
||||||
|
|||||||
@@ -50,5 +50,6 @@ trait CoreProfile extends ProfileProvider with Profile
|
|||||||
with WebHookComponent
|
with WebHookComponent
|
||||||
with WebHookEventComponent
|
with WebHookEventComponent
|
||||||
with PluginComponent
|
with PluginComponent
|
||||||
|
with ProtectedBranchComponent
|
||||||
|
|
||||||
object Profile extends CoreProfile
|
object Profile extends CoreProfile
|
||||||
|
|||||||
37
src/main/scala/gitbucket/core/model/ProtectedBranch.scala
Normal file
37
src/main/scala/gitbucket/core/model/ProtectedBranch.scala
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package gitbucket.core.model
|
||||||
|
|
||||||
|
import scala.slick.lifted.MappedTo
|
||||||
|
import scala.slick.jdbc._
|
||||||
|
|
||||||
|
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
|
||||||
|
import profile.simple._
|
||||||
|
import self._
|
||||||
|
|
||||||
|
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) <> (ProtectedBranch.tupled, ProtectedBranch.unapply)
|
||||||
|
def byPrimaryKey(userName: String, repositoryName: String, branch: String) = byBranch(userName, repositoryName, branch)
|
||||||
|
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], branch: Column[String]) = byBranch(userName, repositoryName, branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val ProtectedBranchContexts = TableQuery[ProtectedBranchContexts]
|
||||||
|
class ProtectedBranchContexts(tag: Tag) extends Table[ProtectedBranchContext](tag, "PROTECTED_BRANCH_REQUIRE_CONTEXT") with BranchTemplate {
|
||||||
|
val context = column[String]("CONTEXT")
|
||||||
|
def * = (userName, repositoryName, branch, context) <> (ProtectedBranchContext.tupled, ProtectedBranchContext.unapply)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
case class ProtectedBranch(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
branch: String,
|
||||||
|
statusCheckAdmin: Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
case class ProtectedBranchContext(
|
||||||
|
userName: String,
|
||||||
|
repositoryName: String,
|
||||||
|
branch: String,
|
||||||
|
context: String)
|
||||||
@@ -7,7 +7,8 @@ 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
|
||||||
|
import gitbucket.core.model.Profile.dateColumnType
|
||||||
|
|
||||||
trait CommitStatusService {
|
trait CommitStatusService {
|
||||||
/** insert or update */
|
/** insert or update */
|
||||||
@@ -42,6 +43,9 @@ 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
|
||||||
|
|
||||||
|
def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)(implicit s: Session) :List[String] =
|
||||||
|
CommitStatuses.filter(t => t.byRepository(userName, repositoryName)).filter(t => t.updatedDate > time.bind).groupBy(_.context).map(_._1).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,10 +10,9 @@ import org.eclipse.jgit.merge.MergeStrategy
|
|||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.transport.RefSpec
|
import org.eclipse.jgit.transport.RefSpec
|
||||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||||
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
|
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
|
||||||
import org.eclipse.jgit.revwalk.RevWalk
|
import org.eclipse.jgit.revwalk.RevWalk
|
||||||
|
|
||||||
|
|
||||||
trait MergeService {
|
trait MergeService {
|
||||||
import MergeService._
|
import MergeService._
|
||||||
/**
|
/**
|
||||||
@@ -52,26 +51,30 @@ trait MergeService {
|
|||||||
/**
|
/**
|
||||||
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||||
*/
|
*/
|
||||||
def checkConflict(userName: String, repositoryName: String, branch: String,
|
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||||
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
|
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
|
||||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||||
val remoteRefName = s"refs/heads/${branch}"
|
val remoteRefName = s"refs/heads/${remoteBranch}"
|
||||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
|
||||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||||
try {
|
try {
|
||||||
// fetch objects from origin repository branch
|
// fetch objects from origin repository branch
|
||||||
git.fetch
|
git.fetch
|
||||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
.setRemote(getRepositoryDir(remoteUserName, remoteRepositoryName).toURI.toString)
|
||||||
.setRefSpecs(refSpec)
|
.setRefSpecs(refSpec)
|
||||||
.call
|
.call
|
||||||
// merge conflict check
|
// merge conflict check
|
||||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||||
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
|
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${localBranch}")
|
||||||
val mergeTip = git.getRepository.resolve(tmpRefName)
|
val mergeTip = git.getRepository.resolve(tmpRefName)
|
||||||
try {
|
try {
|
||||||
!merger.merge(mergeBaseTip, mergeTip)
|
if(merger.merge(mergeBaseTip, mergeTip)){
|
||||||
|
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
case e: NoMergeBaseException => true
|
case e: NoMergeBaseException => None
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
|
||||||
@@ -80,8 +83,54 @@ trait MergeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
|
||||||
|
*/
|
||||||
|
def checkConflict(userName: String, repositoryName: String, branch: String,
|
||||||
|
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
|
||||||
|
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
|
||||||
|
|
||||||
|
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||||
|
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
|
||||||
|
loginAccount: Account, message: String): Option[ObjectId] = {
|
||||||
|
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
|
||||||
|
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||||
|
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||||
|
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
|
||||||
|
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
|
||||||
|
}
|
||||||
|
oldBaseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
object MergeService{
|
object MergeService{
|
||||||
|
object Util{
|
||||||
|
// return treeId
|
||||||
|
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
|
||||||
|
val mergeCommit = new CommitBuilder()
|
||||||
|
mergeCommit.setTreeId(treeId)
|
||||||
|
mergeCommit.setParentIds(parents:_*)
|
||||||
|
mergeCommit.setAuthor(committer)
|
||||||
|
mergeCommit.setCommitter(committer)
|
||||||
|
mergeCommit.setMessage(message)
|
||||||
|
// insertObject and got mergeCommit Object Id
|
||||||
|
val inserter = repository.newObjectInserter
|
||||||
|
val mergeCommitId = inserter.insert(mergeCommit)
|
||||||
|
inserter.flush()
|
||||||
|
inserter.close()
|
||||||
|
mergeCommitId
|
||||||
|
}
|
||||||
|
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
|
||||||
|
// update refs
|
||||||
|
val refUpdate = repository.updateRef(ref)
|
||||||
|
refUpdate.setNewObjectId(newObjectId)
|
||||||
|
refUpdate.setForceUpdate(force)
|
||||||
|
refUpdate.setRefLogIdent(committer)
|
||||||
|
refLogMessage.map(refUpdate.setRefLogMessage(_, true))
|
||||||
|
refUpdate.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
|
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
|
||||||
val repository = git.getRepository
|
val repository = git.getRepository
|
||||||
val mergedBranchName = s"refs/pull/${issueId}/merge"
|
val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||||
@@ -120,12 +169,7 @@ object MergeService{
|
|||||||
def updateBranch(treeId:ObjectId, message:String, branchName:String){
|
def updateBranch(treeId:ObjectId, message:String, branchName:String){
|
||||||
// creates merge commit
|
// creates merge commit
|
||||||
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
||||||
// update refs
|
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
|
||||||
val refUpdate = repository.updateRef(branchName)
|
|
||||||
refUpdate.setNewObjectId(mergeCommitId)
|
|
||||||
refUpdate.setForceUpdate(true)
|
|
||||||
refUpdate.setRefLogIdent(committer)
|
|
||||||
refUpdate.update()
|
|
||||||
}
|
}
|
||||||
if(!conflicted){
|
if(!conflicted){
|
||||||
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
|
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
|
||||||
@@ -145,28 +189,12 @@ object MergeService{
|
|||||||
// creates merge commit
|
// creates merge commit
|
||||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||||
// update refs
|
// update refs
|
||||||
val refUpdate = repository.updateRef(s"refs/heads/${branch}")
|
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
|
||||||
refUpdate.setNewObjectId(mergeCommitId)
|
|
||||||
refUpdate.setForceUpdate(false)
|
|
||||||
refUpdate.setRefLogIdent(committer)
|
|
||||||
refUpdate.setRefLogMessage("merged", true)
|
|
||||||
refUpdate.update()
|
|
||||||
}
|
}
|
||||||
// return treeId
|
// return treeId
|
||||||
private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = {
|
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
|
||||||
val mergeCommit = new CommitBuilder()
|
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
|
||||||
mergeCommit.setTreeId(treeId)
|
|
||||||
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
|
|
||||||
mergeCommit.setAuthor(committer)
|
|
||||||
mergeCommit.setCommitter(committer)
|
|
||||||
mergeCommit.setMessage(message)
|
|
||||||
// insertObject and got mergeCommit Object Id
|
|
||||||
val inserter = repository.newObjectInserter
|
|
||||||
val mergeCommitId = inserter.insert(mergeCommit)
|
|
||||||
inserter.flush()
|
|
||||||
inserter.close()
|
|
||||||
mergeCommitId
|
|
||||||
}
|
|
||||||
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
|
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.model.{Collaborator, Repository, Account, CommitState, CommitStatus, ProtectedBranch, ProtectedBranchContext}
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.JGitUtil
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
import org.eclipse.jgit.transport.ReceiveCommand
|
||||||
|
import org.eclipse.jgit.transport.ReceivePack
|
||||||
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
|
||||||
|
|
||||||
|
trait ProtectedBrancheService {
|
||||||
|
import ProtectedBrancheService._
|
||||||
|
private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] =
|
||||||
|
ProtectedBranches
|
||||||
|
.leftJoin(ProtectedBranchContexts)
|
||||||
|
.on{ case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
|
||||||
|
.map{ case (pb, c) => pb -> c.context.? }
|
||||||
|
.filter(_._1.byPrimaryKey(owner, repository, branch))
|
||||||
|
.list
|
||||||
|
.groupBy(_._1)
|
||||||
|
.map(p => p._1 -> p._2.flatMap(_._2))
|
||||||
|
.map{ case (t1, contexts) =>
|
||||||
|
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
|
||||||
|
}.headOption
|
||||||
|
|
||||||
|
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
|
||||||
|
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
|
||||||
|
|
||||||
|
def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] =
|
||||||
|
ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list
|
||||||
|
|
||||||
|
def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit =
|
||||||
|
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
|
||||||
|
|
||||||
|
def getBranchProtectedReason(owner: String, repository: String, isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
|
||||||
|
val branch = command.getRefName.stripPrefix("refs/heads/")
|
||||||
|
if(branch != command.getRefName){
|
||||||
|
getProtectedBranchInfo(owner, repository, branch).getStopReason(isAllowNonFastForwards, command, pusher)
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object ProtectedBrancheService {
|
||||||
|
case class ProtectedBranchInfo(
|
||||||
|
owner: String,
|
||||||
|
repository: String,
|
||||||
|
enabled: Boolean,
|
||||||
|
/**
|
||||||
|
* Require status checks to pass before merging
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
contexts: Seq[String],
|
||||||
|
/**
|
||||||
|
* Include administrators
|
||||||
|
* Enforce required status checks for repository administrators.
|
||||||
|
*/
|
||||||
|
includeAdministrators: Boolean) extends AccountService with CommitStatusService {
|
||||||
|
|
||||||
|
def isAdministrator(pusher: String)(implicit session: Session): Boolean = pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can't be force pushed
|
||||||
|
* Can't be deleted
|
||||||
|
* Can't have changes merged into them until required status checks pass
|
||||||
|
*/
|
||||||
|
def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
|
||||||
|
if(enabled){
|
||||||
|
command.getType() match {
|
||||||
|
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
|
||||||
|
Some("Cannot force-push to a protected branch")
|
||||||
|
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
|
||||||
|
unSuccessedContexts(command.getNewId.name) match {
|
||||||
|
case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
|
||||||
|
case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
case ReceiveCommand.Type.DELETE =>
|
||||||
|
Some("Cannot delete a protected branch")
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){
|
||||||
|
Set.empty
|
||||||
|
} else {
|
||||||
|
contexts.toSet -- getCommitStatues(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object ProtectedBranchInfo{
|
||||||
|
def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
|
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook, CommitStatus, CommitState}
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
import gitbucket.core.util.JGitUtil
|
import gitbucket.core.util.JGitUtil
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
@@ -145,4 +145,29 @@ object PullRequestService {
|
|||||||
|
|
||||||
case class PullRequestCount(userName: String, count: Int)
|
case class PullRequestCount(userName: String, count: Int)
|
||||||
|
|
||||||
|
case class MergeStatus(
|
||||||
|
hasConflict: Boolean,
|
||||||
|
commitStatues:List[CommitStatus],
|
||||||
|
branchProtection: ProtectedBrancheService.ProtectedBranchInfo,
|
||||||
|
branchIsOutOfDate: Boolean,
|
||||||
|
hasUpdatePermission: Boolean,
|
||||||
|
needStatusCheck: Boolean,
|
||||||
|
hasMergePermission: Boolean,
|
||||||
|
commitIdTo: String){
|
||||||
|
|
||||||
|
val statuses: List[CommitStatus] =
|
||||||
|
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.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.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
|
||||||
|
val canUpdate = branchIsOutOfDate && !hasConflict
|
||||||
|
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
|
||||||
|
lazy val commitStateSummary:(CommitState, String) = {
|
||||||
|
val stateMap = statuses.groupBy(_.state)
|
||||||
|
val state = CommitState.combine(stateMap.keySet)
|
||||||
|
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
|
||||||
|
state -> summary
|
||||||
|
}
|
||||||
|
lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) }
|
||||||
|
lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,11 @@ trait RepositoryService { self: AccountService =>
|
|||||||
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val commitStatuses = CommitStatuses.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val commitStatuses = CommitStatuses .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||||
|
|
||||||
Repositories.filter { t =>
|
Repositories.filter { t =>
|
||||||
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
||||||
@@ -93,8 +95,10 @@ trait RepositoryService { self: AccountService =>
|
|||||||
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
CommitComments .insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
CommitStatuses .insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||||
|
|
||||||
// Update source repository of pull requests
|
// Update source repository of pull requests
|
||||||
PullRequests.filter { t =>
|
PullRequests.filter { t =>
|
||||||
@@ -310,10 +314,16 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* Save repository options.
|
* Save repository options.
|
||||||
*/
|
*/
|
||||||
def saveRepositoryOptions(userName: String, repositoryName: String,
|
def saveRepositoryOptions(userName: String, repositoryName: String,
|
||||||
description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
|
description: Option[String], isPrivate: Boolean)(implicit s: Session): Unit =
|
||||||
Repositories.filter(_.byRepository(userName, repositoryName))
|
Repositories.filter(_.byRepository(userName, repositoryName))
|
||||||
.map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
|
.map { r => (r.description.?, r.isPrivate, r.updatedDate) }
|
||||||
.update (description, defaultBranch, isPrivate, currentDate)
|
.update (description, isPrivate, currentDate)
|
||||||
|
|
||||||
|
def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
|
||||||
|
defaultBranch: String)(implicit s: Session): Unit =
|
||||||
|
Repositories.filter(_.byRepository(userName, repositoryName))
|
||||||
|
.map { r => r.defaultBranch }
|
||||||
|
.update (defaultBranch)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add collaborator to the repository.
|
* Add collaborator to the repository.
|
||||||
|
|||||||
@@ -111,13 +111,18 @@ import scala.collection.JavaConverters._
|
|||||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
|
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
|
||||||
extends PostReceiveHook with PreReceiveHook
|
extends PostReceiveHook with PreReceiveHook
|
||||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
|
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
|
||||||
with WebHookPullRequestService {
|
with WebHookPullRequestService with ProtectedBrancheService {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||||
private var existIds: Seq[String] = Nil
|
private var existIds: Seq[String] = Nil
|
||||||
|
|
||||||
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||||
try {
|
try {
|
||||||
|
commands.asScala.foreach { command =>
|
||||||
|
getBranchProtectedReason(owner, repository, receivePack.isAllowNonFastForwards, command, pusher).map{ reason =>
|
||||||
|
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||||
existIds = JGitUtil.getAllCommitIds(git)
|
existIds = JGitUtil.getAllCommitIds(git)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -289,10 +289,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
def commitStateIcon(state: CommitState) = Html(state match {
|
def commitStateIcon(state: CommitState) = Html(state match {
|
||||||
case CommitState.PENDING => "●"
|
case CommitState.PENDING => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-primitive-dot"></i>"""
|
||||||
case CommitState.SUCCESS => "✔"
|
case CommitState.SUCCESS => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-check"></i>"""
|
||||||
case CommitState.ERROR => "×"
|
case CommitState.ERROR => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
|
||||||
case CommitState.FAILURE => "×"
|
case CommitState.FAILURE => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
|
||||||
})
|
})
|
||||||
|
|
||||||
def commitStateText(state: CommitState, commitId:String) = state match {
|
def commitStateText(state: CommitState, commitId:String) = state match {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
case comment: gitbucket.core.model.IssueComment => Some(comment)
|
case comment: gitbucket.core.model.IssueComment => Some(comment)
|
||||||
case other => None
|
case other => None
|
||||||
}.exists(_.action == "merge")){ merged =>
|
}.exists(_.action == "merge")){ merged =>
|
||||||
@if(hasWritePermission && !issue.closed){
|
@if(!issue.closed){
|
||||||
<div class="check-conflict" style="display: none;">
|
<div class="check-conflict" style="display: none;">
|
||||||
<div class="box issue-comment-box" style="background-color: #fbeed5">
|
<div class="box issue-comment-box" style="background-color: #fbeed5">
|
||||||
<div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
|
<div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
|
||||||
@@ -55,10 +55,12 @@ $(function(){
|
|||||||
$('#merge-pull-request').show();
|
$('#merge-pull-request').show();
|
||||||
});
|
});
|
||||||
|
|
||||||
@if(hasWritePermission){
|
var checkConflict = $('.check-conflict').show();
|
||||||
$('.check-conflict').show();
|
if(checkConflict.length){
|
||||||
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
|
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
|
||||||
|
}
|
||||||
|
|
||||||
|
@if(hasWritePermission){
|
||||||
$('.delete-branch').click(function(e){
|
$('.delete-branch').click(function(e){
|
||||||
var branchName = $(e.target).data('name');
|
var branchName = $(e.target).data('name');
|
||||||
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
|
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
|
||||||
|
|||||||
@@ -1,76 +1,93 @@
|
|||||||
@(hasConflict: Boolean,
|
@(status: gitbucket.core.service.PullRequestService.MergeStatus,
|
||||||
hasProblem: Boolean,
|
|
||||||
issue: gitbucket.core.model.Issue,
|
issue: gitbucket.core.model.Issue,
|
||||||
pullreq: gitbucket.core.model.PullRequest,
|
pullreq: gitbucket.core.model.PullRequest,
|
||||||
statuses: List[model.CommitStatus],
|
|
||||||
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||||
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import gitbucket.core.service.SystemSettingsService
|
@import gitbucket.core.service.SystemSettingsService
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@import model.CommitState
|
@import model.CommitState
|
||||||
<div class="box issue-comment-box" style="background-color: @if(hasProblem){ #fbeed5 }else{ #d8f5cd };">
|
<div class="box issue-comment-box" style="background-color: @if(status.hasProblem){ #fbeed5 }else{ #d8f5cd };">
|
||||||
<div class="box-content issue-content" style="border: 1px solid @if(hasProblem){ #c09853 }else{ #95c97e }; padding: 10px;">
|
<div class="box-content issue-content" style="border: 1px solid @if(status.hasProblem){ #c09853 }else{ #95c97e };padding:0">
|
||||||
<div id="merge-pull-request">
|
<div id="merge-pull-request">
|
||||||
@if(!statuses.isEmpty){
|
@if(!status.statuses.isEmpty){
|
||||||
<div class="build-statuses">
|
<div class="build-statuses">
|
||||||
@if(statuses.size==1){
|
@defining(status.commitStateSummary){ case (summaryState, summary) =>
|
||||||
@defining(statuses.head){ status =>
|
<div class="build-status-item-header">
|
||||||
<div class="build-status-item">
|
|
||||||
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
|
|
||||||
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
|
||||||
<strong class="text-@{status.state.name}">@commitStateText(status.state, pullreq.commitIdTo)</strong>
|
|
||||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
@defining(statuses.groupBy(_.state)){ stateMap =>
|
|
||||||
@defining(CommitState.combine(stateMap.keySet)){ state =>
|
|
||||||
<div class="build-status-item">
|
|
||||||
<a class="pull-right" id="toggle-all-checks"></a>
|
<a class="pull-right" id="toggle-all-checks"></a>
|
||||||
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
|
<span class="build-status-icon text-@{summaryState.name}">@commitStateIcon(summaryState)</span>
|
||||||
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
|
<strong class="text-@{summaryState.name}">@commitStateText(summaryState, pullreq.commitIdTo)</strong>
|
||||||
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
|
<span class="text-@{summaryState.name}">— @summary checks</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="build-statuses-list" style="@if(state==CommitState.SUCCESS){ display:none; }else{ }">
|
}
|
||||||
@statuses.map{ status =>
|
<div class="build-statuses-list" style="@if(status.isAllSuccess){ display:none; }else{ }">
|
||||||
|
@status.statusesAndRequired.map{ case (status, required) =>
|
||||||
<div class="build-status-item">
|
<div class="build-status-item">
|
||||||
@status.targetUrl.map{ url => <a class="pull-right" href="@url">Details</a> }
|
<div class="pull-right">
|
||||||
|
@if(required){ <span class="label">Required</span> }
|
||||||
|
@status.targetUrl.map{ url => <a href="@url">Details</a> }
|
||||||
|
</div>
|
||||||
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
|
||||||
<span class="text-@{status.state.name}">@status.context</span>
|
<strong>@status.context</strong>
|
||||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="pull-right">
|
<div style="padding:15px">
|
||||||
<input type="button" class="btn @if(!hasProblem){ btn-success }else{ btn-default }" id="merge-pull-request-button" value="Merge pull request"@if(hasConflict){ disabled="true"}/>
|
@if(status.hasConflict){
|
||||||
</div>
|
<div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
|
||||||
<div>
|
<span class="strong">This branch has conflicts that must be resolved</span>
|
||||||
@if(hasConflict){
|
|
||||||
<span class="strong">We can’t automatically merge this pull request.</span>
|
|
||||||
} else {
|
|
||||||
@if(hasProblem){
|
|
||||||
<span class="strong">Merge with caution!</span>
|
|
||||||
} else {
|
|
||||||
<span class="strong">This pull request can be automatically merged.</span>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="small">
|
<div class="small">
|
||||||
@if(hasConflict){
|
@if(status.hasMergePermission){
|
||||||
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
|
<a href="#" class="show-command-line">Use the command line</a> to resolve conflicts before continuing.
|
||||||
} else {
|
} else {
|
||||||
You can also merge branches on the <a href="#" id="show-command-line">command line</a>.
|
Only those with write access to this repository can merge pull requests.
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div id="command-line" style="display: none;">
|
} else { @if(status.branchIsOutOfDate){
|
||||||
<hr>
|
@if(status.hasUpdatePermission){
|
||||||
@if(hasConflict){
|
<div class="pull-right">
|
||||||
|
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/update_branch">
|
||||||
|
<input type="hidden" name="expected_head_oid" value="@pullreq.commitIdFrom">
|
||||||
|
<button class="btn"@if(!status.canUpdate){ disabled="true"} id="update-branch-button">Update branch</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
|
||||||
|
<span class="strong">This branch is out-of-date with the base branch</span>
|
||||||
|
<div class="small">
|
||||||
|
Merge the latest changes from <code>@pullreq.branch</code> into this branch.
|
||||||
|
</div>
|
||||||
|
} else { @if(status.hasRequiredStatusProblem) {
|
||||||
|
<div class="merge-indicator merge-indicator-warning"><span class="octicon octicon-primitive-dot"></span></div>
|
||||||
|
<span class="strong">Required statuses must pass before merging.</span>
|
||||||
|
<div class="small">
|
||||||
|
All required status checks on this pull request must run successfully to enable automatic merging.
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="merge-indicator merge-indicator-success"><span class="octicon octicon-check"></span></div>
|
||||||
|
@if(status.hasMergePermission){
|
||||||
|
<span class="strong">Merging can be performed automatically.</span>
|
||||||
|
<div class="small">
|
||||||
|
Merging can be performed automatically.
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<span class="strong">This branch has no conflicts with the base branch.</span>
|
||||||
|
<div class="small">
|
||||||
|
Only those with write access to this repository can merge pull requests.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} } }
|
||||||
|
</div>
|
||||||
|
@if(status.hasMergePermission){
|
||||||
|
<div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa">
|
||||||
|
<input type="button" class="btn @if(!status.hasProblem){ btn-success }" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/>
|
||||||
|
You can also merge branches on the <a href="#" class="show-command-line">command line</a>.
|
||||||
|
<div id="command-line" style="display: none;margin-top: 15px;">
|
||||||
|
<hr />
|
||||||
|
@if(status.hasConflict){
|
||||||
<span class="strong">Checkout via command line</span>
|
<span class="strong">Checkout via command line</span>
|
||||||
<p>
|
<p>
|
||||||
If you cannot merge a pull request automatically here, you have the option of checking
|
If you cannot merge a pull request automatically here, you have the option of checking
|
||||||
@@ -90,7 +107,7 @@
|
|||||||
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
|
<button class="btn btn-small" type="button" id="repository-url-ssh" style="border-radius: 0px;">SSH</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly>
|
<input type="text" style="width: 500px;" value="@forkedRepository.httpUrl" id="repository-url" readonly />
|
||||||
}
|
}
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
@@ -116,6 +133,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div id="confirm-merge-form" style="display: none;">
|
<div id="confirm-merge-form" style="display: none;">
|
||||||
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
|
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
|
||||||
<div class="strong">
|
<div class="strong">
|
||||||
@@ -134,8 +153,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(function(){
|
$(function(){
|
||||||
$('#show-command-line').click(function(){
|
$('.show-command-line').click(function(){
|
||||||
$('#command-line').show();
|
$('#command-line').toggle();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
function setToggleAllChecksLabel(){
|
function setToggleAllChecksLabel(){
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
|
dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
|
||||||
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
|
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
|
||||||
hasWritePermission: Boolean,
|
hasWritePermission: Boolean,
|
||||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||||
|
flash: Map[String, String])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@import gitbucket.core.model._
|
@import gitbucket.core.model._
|
||||||
@@ -73,6 +74,12 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content fill-width pull-left">
|
<div class="tab-content fill-width pull-left">
|
||||||
<div class="tab-pane active" id="conversation">
|
<div class="tab-pane active" id="conversation">
|
||||||
|
@flash.get("error").map{ error =>
|
||||||
|
<div class="alert alert-error">@error</div>
|
||||||
|
}
|
||||||
|
@flash.get("info").map{ info =>
|
||||||
|
<div class="alert alert-info">@info</div>
|
||||||
|
}
|
||||||
@pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
@pulls.html.conversation(issue, pullreq, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane" id="commits">
|
<div class="tab-pane" id="commits">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@(branchInfo: Seq[(gitbucket.core.util.JGitUtil.BranchInfo, Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)])],
|
@(branchInfo: Seq[(gitbucket.core.util.JGitUtil.BranchInfo, Option[(gitbucket.core.model.PullRequest, gitbucket.core.model.Issue)], Boolean)],
|
||||||
hasWritePermission: Boolean,
|
hasWritePermission: Boolean,
|
||||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@branchInfo.map { case (branch, prs) =>
|
@branchInfo.map { case (branch, prs, isProtected) =>
|
||||||
<tr><td style="padding: 12px;">
|
<tr><td style="padding: 12px;">
|
||||||
<div class="branch-action">
|
<div class="branch-action">
|
||||||
@branch.mergeInfo.map{ info =>
|
@branch.mergeInfo.map{ info =>
|
||||||
@@ -47,14 +47,19 @@
|
|||||||
<span style="margin-left: 8px;">
|
<span style="margin-left: 8px;">
|
||||||
@if(prs.map(!_._2.closed).getOrElse(false)){
|
@if(prs.map(!_._2.closed).getOrElse(false)){
|
||||||
<a class="disabled" data-toggle="tooltip" title="You can’t delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
|
<a class="disabled" data-toggle="tooltip" title="You can’t delete this branch because it has an open pull request"><i class="octicon octicon-trashcan"></i></a>
|
||||||
|
} else {
|
||||||
|
@if(isProtected){
|
||||||
|
<a class="disabled" data-toggle="tooltip" title="You can’t delete a protected branch."><i class="octicon octicon-trashcan"></i></a>
|
||||||
} else {
|
} else {
|
||||||
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="delete-branch" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan @if(info.isMerged){warning} else {danger}"></i></a>
|
<a href="@url(repository)/delete/@encodeRefName(branch.name)" class="delete-branch" data-name="@branch.name" @if(info.isMerged){ data-toggle="tooltip" title="this branch is merged" }><i class="octicon octicon-trashcan @if(info.isMerged){warning} else {danger}"></i></a>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="branch-details">
|
<div class="branch-details">
|
||||||
|
@if(isProtected){ <span class="octicon octicon-shield" title="This branch is protected"></span> }
|
||||||
<a href="@url(repository)/tree/@encodeRefName(branch.name)" class="branch-name">@branch.name</a>
|
<a href="@url(repository)/tree/@encodeRefName(branch.name)" class="branch-name">@branch.name</a>
|
||||||
<span class="branch-meta">
|
<span class="branch-meta">
|
||||||
<span>Updated @helper.html.datetimeago(branch.commitTime, false)
|
<span>Updated @helper.html.datetimeago(branch.commitTime, false)
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||||
pathList: List[String],
|
pathList: List[String],
|
||||||
fileName: Option[String],
|
fileName: Option[String],
|
||||||
content: gitbucket.core.util.JGitUtil.ContentInfo)(implicit context: gitbucket.core.controller.Context)
|
content: gitbucket.core.util.JGitUtil.ContentInfo,
|
||||||
|
protectedBranch: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) {
|
@html.main(if(fileName.isEmpty) "New File" else s"Editing ${fileName.get} at ${branch} - ${repository.owner}/${repository.name}", Some(repository)) {
|
||||||
@html.menu("code", repository){
|
@html.menu("code", repository){
|
||||||
|
@if(protectedBranch){
|
||||||
|
<div class="alert alert-danger">branch @branch is protected.</div>
|
||||||
|
}
|
||||||
<form method="POST" action="@url(repository)/@if(fileName.isEmpty){create}else{update}" validate="true">
|
<form method="POST" action="@url(repository)/@if(fileName.isEmpty){create}else{update}" validate="true">
|
||||||
<span class="error" id="error-newFileName"></span>
|
<span class="error" id="error-newFileName"></span>
|
||||||
<div class="head">
|
<div class="head">
|
||||||
@@ -82,6 +86,9 @@ $(function(){
|
|||||||
@if(fileName.isDefined){
|
@if(fileName.isDefined){
|
||||||
editor.getSession().setMode("ace/mode/@editorType(fileName.get)");
|
editor.getSession().setMode("ace/mode/@editorType(fileName.get)");
|
||||||
}
|
}
|
||||||
|
@if(protectedBranch){
|
||||||
|
editor.setReadOnly(true);
|
||||||
|
}
|
||||||
|
|
||||||
editor.on('change', function(){
|
editor.on('change', function(){
|
||||||
updateCommitButtonStatus();
|
updateCommitButtonStatus();
|
||||||
|
|||||||
67
src/main/twirl/gitbucket/core/settings/branches.scala.html
Normal file
67
src/main/twirl/gitbucket/core/settings/branches.scala.html
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||||
|
protectedBranchList: Seq[String],
|
||||||
|
info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import context._
|
||||||
|
@import gitbucket.core.view.helpers._
|
||||||
|
@import gitbucket.core.model.WebHook._
|
||||||
|
@html.main("Branches", Some(repository)){
|
||||||
|
@html.menu("settings", repository){
|
||||||
|
@menu("branches", repository){
|
||||||
|
@if(repository.branchList.isEmpty){
|
||||||
|
<div class="well">
|
||||||
|
<center>
|
||||||
|
<p><i class="octicon octicon-git-branch" style="font-size:300%"></i></p>
|
||||||
|
<p>You don’t have any branches</p>
|
||||||
|
<p>Before you can edit branch settings, you need to add a branch.</p>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
}else{
|
||||||
|
@helper.html.information(info)
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Default branch</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>The default branch is considered the “base” branch in your repository, against which all pull requests and code commits are automatically made, unless you specify a different branch.</p>
|
||||||
|
<form id="form" method="post" action="@url(repository)/settings/update_default_branch" validate="true" class="form-inline">
|
||||||
|
<span class="error" id="error-defaultBranch"></span>
|
||||||
|
<select name="defaultBranch" id="defaultBranch" class="form-control">
|
||||||
|
@repository.branchList.map { branch =>
|
||||||
|
<option @if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<input type="submit" class="btn btn-default" value="Update" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Protected branches</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Protect branches to disable force pushing, prevent branches from being deleted, and optionally require status checks before merging. New to protected branches?
|
||||||
|
<form class="form-inline">
|
||||||
|
<select name="protectBranch" id="protectBranch" onchange="location=$(this).val()" class="form-control">
|
||||||
|
<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>
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<table class="table table-bordered table-hover branches">
|
||||||
|
@protectedBranchList.map{ branch =>
|
||||||
|
<tr><td>
|
||||||
|
<span class="branch-name">@branch</span>
|
||||||
|
<span class="branch-action">
|
||||||
|
<a href="@url(repository)/settings/branches/@encodeRefName(branch)" class="btn btn-small btn-default">Edit</a>
|
||||||
|
</span>
|
||||||
|
</td></tr>
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
@(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>
|
||||||
|
<form name="branchProtection" onsubmit="submitForm(event)"><div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Branch protection for <b>@branch</b></div>
|
||||||
|
<div class="panel-body">
|
||||||
|
|
||||||
|
<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="panel panel-default">
|
||||||
|
<div class="panel-heading">Status checks found in the last week for this repository</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
@knownContexts.map{ br =>
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="contexts" value="@br" onclick="update()" @check(protection.status.contexts.find(_==br))>
|
||||||
|
<span>@br</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input class="btn btn-success" type="submit" value="Save changes" />
|
||||||
|
</div>
|
||||||
|
</div></form>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
function getValue(){
|
||||||
|
var v = {}, contexts=[];
|
||||||
|
$("input[type=checkbox]:checked").each(function(){
|
||||||
|
if(this.name === 'contexts'){
|
||||||
|
contexts.push(this.value);
|
||||||
|
}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>
|
||||||
@@ -11,6 +11,11 @@
|
|||||||
<li@if(active=="collaborators"){ class="active"}>
|
<li@if(active=="collaborators"){ class="active"}>
|
||||||
<a href="@url(repository)/settings/collaborators">Collaborators</a>
|
<a href="@url(repository)/settings/collaborators">Collaborators</a>
|
||||||
</li>
|
</li>
|
||||||
|
@if(!repository.branchList.isEmpty){
|
||||||
|
<li@if(active=="branches"){ class="active"}>
|
||||||
|
<a href="@url(repository)/settings/branches">Branches</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
<li@if(active=="hooks"){ class="active"}>
|
<li@if(active=="hooks"){ class="active"}>
|
||||||
<a href="@url(repository)/settings/hooks">Service Hooks</a>
|
<a href="@url(repository)/settings/hooks">Service Hooks</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -18,22 +18,6 @@
|
|||||||
<label for="description" class="strong">Description:</label>
|
<label for="description" class="strong">Description:</label>
|
||||||
<input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/>
|
<input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="margin form-group">
|
|
||||||
<label for="defaultBranch" class="strong">Default Branch:</label>
|
|
||||||
<select name="defaultBranch" id="defaultBranch"@if(repository.branchList.isEmpty){ disabled} class="form-control">
|
|
||||||
@if(repository.branchList.isEmpty){
|
|
||||||
<option value="none" selected>No Branch</option>
|
|
||||||
} else {
|
|
||||||
@repository.branchList.map { branch =>
|
|
||||||
<option@if(branch==repository.repository.defaultBranch){ selected}>@branch</option>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
@if(repository.branchList.isEmpty){
|
|
||||||
<input type="hidden" name="defaultBranch" value="none"/>
|
|
||||||
}
|
|
||||||
<span class="error" id="error-defaultBranch"></span>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="margin">
|
<fieldset class="margin">
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="isPrivate" value="false"
|
<input type="radio" name="isPrivate" value="false"
|
||||||
|
|||||||
@@ -1408,13 +1408,43 @@ div.author-info div.committer {
|
|||||||
margin: -10px -10px 10px -10px;
|
margin: -10px -10px 10px -10px;
|
||||||
}
|
}
|
||||||
.build-statuses .build-status-item{
|
.build-statuses .build-status-item{
|
||||||
padding: 10px 15px 10px 12px;
|
padding: 10px 15px 10px 64px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
.build-statuses-list .build-status-item{
|
.build-statuses-list .build-status-item{
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.merge-indicator{
|
||||||
|
float:left;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.merge-indicator-success{
|
||||||
|
background-color: #6cc644;
|
||||||
|
}
|
||||||
|
.merge-indicator-warning{
|
||||||
|
background-color: #cea61b;
|
||||||
|
}
|
||||||
|
.merge-indicator-alert{
|
||||||
|
background-color: #888;
|
||||||
|
}
|
||||||
|
.merge-indicator .octicon{
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
.merge-indicator-warning .octicon{
|
||||||
|
color: white;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
/* Diff */
|
/* Diff */
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import org.specs2.mutable.Specification
|
||||||
|
import org.eclipse.jgit.transport.ReceiveCommand
|
||||||
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
import gitbucket.core.model.CommitState
|
||||||
|
import ProtectedBrancheService.ProtectedBranchInfo
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectedBrancheServiceSpec extends Specification with ServiceSpecBase with ProtectedBrancheService with CommitStatusService {
|
||||||
|
val now = new java.util.Date()
|
||||||
|
val sha = "0c77148632618b59b6f70004e3084002be2b8804"
|
||||||
|
val sha2 = "0c77148632618b59b6f70004e3084002be2b8805"
|
||||||
|
"getProtectedBranchInfo" should {
|
||||||
|
"empty is disabled" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo.disabled("user1", "repo1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"enable and update and disable" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Nil)
|
||||||
|
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo("user1", "repo1", true, Nil, false)
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", true, Seq("hoge","huge"))
|
||||||
|
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo("user1", "repo1", true, Seq("hoge","huge"), true)
|
||||||
|
disableBranchProtection("user1", "repo1", "branch")
|
||||||
|
getProtectedBranchInfo("user1", "repo1", "branch") must_== ProtectedBranchInfo.disabled("user1", "repo1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"empty contexts is no-include-administrators" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Nil)
|
||||||
|
getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators must_== false
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", true, Nil)
|
||||||
|
getProtectedBranchInfo("user1", "repo1", "branch").includeAdministrators must_== false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getProtectedBranchList" in {
|
||||||
|
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"))
|
||||||
|
getProtectedBranchList("user1", "repo1").toSet must_== Set("branch", "branch2", "branch3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getBranchProtectedReason on force push from admin" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
|
||||||
|
generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
getBranchProtectedReason("user1", "repo1", true, rc, "user1") must_== None
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Nil)
|
||||||
|
getBranchProtectedReason("user1", "repo1", true, rc, "user1") must_== Some("Cannot force-push to a protected branch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getBranchProtectedReason on force push from othre" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
|
||||||
|
generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
getBranchProtectedReason("user1", "repo1", true, rc, "user2") must_== None
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Nil)
|
||||||
|
getBranchProtectedReason("user1", "repo1", true, rc, "user2") must_== Some("Cannot force-push to a protected branch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getBranchProtectedReason check status on push from othre" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
|
||||||
|
val user1 = generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user2") must_== None
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user2") must_== Some("Required status check \"must\" is expected")
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user2") must_== Some("2 of 2 required status checks are expected")
|
||||||
|
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user2") must_== Some("2 of 2 required status checks are expected")
|
||||||
|
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user2") must_== Some("Required status check \"must2\" is expected")
|
||||||
|
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user2") must_== None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"getBranchProtectedReason check status on push from admin" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
|
||||||
|
val user1 = generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== None
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== None
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", true, Seq("must"))
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== Some("Required status check \"must\" is expected")
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== None
|
||||||
|
enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2"))
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== Some("2 of 2 required status checks are expected")
|
||||||
|
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== Some("2 of 2 required status checks are expected")
|
||||||
|
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== Some("Required status check \"must2\" is expected")
|
||||||
|
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
getBranchProtectedReason("user1", "repo1", false, rc, "user1") must_== None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ProtectedBranchInfo" should {
|
||||||
|
"administrator is owner" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
var x = ProtectedBranchInfo("user1", "repo1", true, Nil, false)
|
||||||
|
x.isAdministrator("user1") must_== true
|
||||||
|
x.isAdministrator("user2") must_== false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"administrator is manager" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
var x = ProtectedBranchInfo("grp1", "repo1", true, Nil, false)
|
||||||
|
x.createGroup("grp1", None)
|
||||||
|
generateNewAccount("user1")
|
||||||
|
generateNewAccount("user2")
|
||||||
|
generateNewAccount("user3")
|
||||||
|
|
||||||
|
x.updateGroupMembers("grp1", List("user1"->true, "user2"->false))
|
||||||
|
x.isAdministrator("user1") must_== true
|
||||||
|
x.isAdministrator("user2") must_== false
|
||||||
|
x.isAdministrator("user3") must_== false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"unSuccessedContexts" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
val user1 = generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
var x = ProtectedBranchInfo("user1", "repo1", true, List("must"), false)
|
||||||
|
x.unSuccessedContexts(sha) must_== Set("must")
|
||||||
|
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
x.unSuccessedContexts(sha) must_== Set("must")
|
||||||
|
createCommitStatus("user1", "repo1", sha, "must", CommitState.ERROR, None, None, now, user1)
|
||||||
|
x.unSuccessedContexts(sha) must_== Set("must")
|
||||||
|
createCommitStatus("user1", "repo1", sha, "must", CommitState.PENDING, None, None, now, user1)
|
||||||
|
x.unSuccessedContexts(sha) must_== Set("must")
|
||||||
|
createCommitStatus("user1", "repo1", sha, "must", CommitState.FAILURE, None, None, now, user1)
|
||||||
|
x.unSuccessedContexts(sha) must_== Set("must")
|
||||||
|
createCommitStatus("user1", "repo1", sha, "must", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
x.unSuccessedContexts(sha) must_== Set()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"unSuccessedContexts when empty" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
val user1 = generateNewUserWithDBRepository("user1", "repo1")
|
||||||
|
var x = ProtectedBranchInfo("user1", "repo1", true, Nil, false)
|
||||||
|
val sha = "0c77148632618b59b6f70004e3084002be2b8804"
|
||||||
|
x.unSuccessedContexts(sha) must_== Nil
|
||||||
|
createCommitStatus("user1", "repo1", sha, "context", CommitState.SUCCESS, None, None, now, user1)
|
||||||
|
x.unSuccessedContexts(sha) must_== Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"if disabled, needStatusCheck is false" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
ProtectedBranchInfo("user1", "repo1", false, Seq("must"), true).needStatusCheck("user1") must_== false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"needStatusCheck includeAdministrators" in {
|
||||||
|
withTestDB { implicit session =>
|
||||||
|
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), false).needStatusCheck("user2") must_== true
|
||||||
|
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), false).needStatusCheck("user1") must_== false
|
||||||
|
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), true ).needStatusCheck("user2") must_== true
|
||||||
|
ProtectedBranchInfo("user1", "repo1", true, Seq("must"), true ).needStatusCheck("user1") must_== true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,11 @@ import org.specs2.mutable.Specification
|
|||||||
|
|
||||||
class RepositoryServiceSpec extends Specification with ServiceSpecBase with RepositoryService with AccountService{
|
class RepositoryServiceSpec extends Specification with ServiceSpecBase with RepositoryService with AccountService{
|
||||||
"RepositoryService" should {
|
"RepositoryService" should {
|
||||||
"renameRepository can rename CommitState" in { withTestDB { implicit session =>
|
"renameRepository can rename CommitState, ProtectedBranches" in { withTestDB { implicit session =>
|
||||||
val tester = generateNewAccount("tester")
|
val tester = generateNewAccount("tester")
|
||||||
createRepository("repo","root",None,false)
|
createRepository("repo","root",None,false)
|
||||||
val commitStatusService = new CommitStatusService{}
|
val service = new CommitStatusService with ProtectedBrancheService{}
|
||||||
val id = commitStatusService.createCommitStatus(
|
val id = service.createCommitStatus(
|
||||||
userName = "root",
|
userName = "root",
|
||||||
repositoryName = "repo",
|
repositoryName = "repo",
|
||||||
sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
|
sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
|
||||||
@@ -22,14 +22,20 @@ class RepositoryServiceSpec extends Specification with ServiceSpecBase with Repo
|
|||||||
description = Some("description"),
|
description = Some("description"),
|
||||||
creator = tester,
|
creator = tester,
|
||||||
now = new java.util.Date)
|
now = new java.util.Date)
|
||||||
val org = commitStatusService.getCommitStatus("root","repo", id).get
|
service.enableBranchProtection("root", "repo", "branch", true, Seq("must1", "must2"))
|
||||||
|
var orgPbi = service.getProtectedBranchInfo("root", "repo", "branch")
|
||||||
|
val org = service.getCommitStatus("root","repo", id).get
|
||||||
|
|
||||||
renameRepository("root","repo","tester","repo2")
|
renameRepository("root","repo","tester","repo2")
|
||||||
val neo = commitStatusService.getCommitStatus("tester","repo2", org.commitId, org.context).get
|
|
||||||
|
val neo = service.getCommitStatus("tester","repo2", org.commitId, org.context).get
|
||||||
neo must_==
|
neo must_==
|
||||||
org.copy(
|
org.copy(
|
||||||
commitStatusId=neo.commitStatusId,
|
commitStatusId=neo.commitStatusId,
|
||||||
repositoryName="repo2",
|
repositoryName="repo2",
|
||||||
userName="tester")
|
userName="tester")
|
||||||
|
service.getProtectedBranchInfo("tester", "repo2", "branch") must_==
|
||||||
|
orgPbi.copy(owner="tester", repository="repo2")
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user