mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-08 22:45:51 +01:00
Merge branch 'master' into solidbase-integration
Conflicts: src/main/scala/gitbucket/core/model/Profile.scala src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
This commit is contained in:
13
README.md
13
README.md
@@ -9,28 +9,21 @@ The current version of GitBucket provides a basic features below:
|
||||
|
||||
- Public / Private Git repository (http and ssh access)
|
||||
- Repository viewer and online file editing
|
||||
- Repository search (Code and Issues)
|
||||
- Wiki
|
||||
- Issues
|
||||
- Fork / Pull request
|
||||
- Issues / Pull request
|
||||
- Email notification
|
||||
- Activity timeline
|
||||
- Simple user and group management with LDAP integration
|
||||
- Gravatar support
|
||||
- Plug-in system
|
||||
|
||||
If you want to try the development version of GitBucket, see [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
|
||||
|
||||
Installation
|
||||
--------
|
||||
GitBucket requires **Java8**. You have to install beforehand when it's not installed.
|
||||
|
||||
1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases).
|
||||
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
|
||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
|
||||
|
||||
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx)
|
||||
|
||||
The default administrator account is **root** and password is **root**.
|
||||
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser and logged-in with **root** / **root**.
|
||||
|
||||
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
|
||||
|
||||
|
||||
25
src/main/resources/update/3_11.sql
Normal file
25
src/main/resources/update/3_11.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.ProtectedBranchService
|
||||
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: ProtectedBranchService.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[ApiPullRequest.Commit]() +
|
||||
FieldSerializer[ApiIssue]() +
|
||||
FieldSerializer[ApiComment]()
|
||||
FieldSerializer[ApiComment]() +
|
||||
ApiBranchProtection.enforcementLevelSerializer
|
||||
|
||||
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 SystemSettingsService {
|
||||
|
||||
implicit val jsonFormats = DefaultFormats
|
||||
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
|
||||
|
||||
// TODO Scala 2.11
|
||||
// // Don't set content type via Accept header.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
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.service.CommitStatusService
|
||||
import gitbucket.core.service.MergeService
|
||||
@@ -27,13 +27,13 @@ import scala.collection.JavaConverters._
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with CommitStatusService with MergeService
|
||||
with CommitStatusService with MergeService with ProtectedBranchService
|
||||
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
|
||||
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
with CommitStatusService with MergeService =>
|
||||
with CommitStatusService with MergeService with ProtectedBranchService =>
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
|
||||
|
||||
@@ -119,7 +119,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
commits,
|
||||
diffs,
|
||||
hasWritePermission(owner, name, context.loginAccount),
|
||||
repository)
|
||||
repository,
|
||||
flash.toMap.map(f => f._1 -> f._2.toString))
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound
|
||||
@@ -166,22 +167,34 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
|
||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
||||
params("id").toIntOpt.flatMap{ issueId =>
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
|
||||
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
|
||||
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
|
||||
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(
|
||||
hasConfrict,
|
||||
hasProblem,
|
||||
mergeStatus,
|
||||
issue,
|
||||
pullreq,
|
||||
statuses,
|
||||
repository,
|
||||
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
|
||||
}
|
||||
@@ -203,6 +216,75 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} 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) =>
|
||||
params("id").toIntOpt.flatMap { issueId =>
|
||||
val owner = repository.owner
|
||||
@@ -528,4 +610,15 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
repository,
|
||||
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.model.WebHook
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
|
||||
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBranchService, CommitStatusService}
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.JGitUtil._
|
||||
@@ -18,23 +18,29 @@ import org.eclipse.jgit.lib.ObjectId
|
||||
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with WebHookService
|
||||
with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
|
||||
with OwnerAuthenticator with UsersAuthenticator
|
||||
|
||||
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with WebHookService
|
||||
self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
|
||||
with OwnerAuthenticator with UsersAuthenticator =>
|
||||
|
||||
// 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(
|
||||
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))),
|
||||
"description" -> trim(label("Description" , optional(text()))),
|
||||
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
|
||||
"isPrivate" -> trim(label("Repository Type", boolean()))
|
||||
)(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
|
||||
case class CollaboratorForm(userName: String)
|
||||
|
||||
@@ -75,12 +81,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Save the repository options.
|
||||
*/
|
||||
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
|
||||
saveRepositoryOptions(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.description,
|
||||
defaultBranch,
|
||||
repository.repository.parentUserName.map { _ =>
|
||||
repository.repository.isPrivate
|
||||
} getOrElse form.isPrivate
|
||||
@@ -98,14 +102,61 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
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."
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,6 @@ import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.model.{Account, CommitState, WebHook}
|
||||
import gitbucket.core.service.CommitStatusService
|
||||
import gitbucket.core.service.WebHookService._
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
@@ -34,7 +33,7 @@ import org.scalatra._
|
||||
class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
@@ -42,7 +41,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
|
||||
trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
|
||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
|
||||
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService =>
|
||||
|
||||
ArchiveCommand.registerFormat("zip", new ZipFormat)
|
||||
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
|
||||
@@ -222,12 +221,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
|
||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||
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,
|
||||
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
|
||||
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")),
|
||||
protectedBranch)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
|
||||
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 =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
@@ -235,7 +237,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
getPathObjectId(git, path, revCommit).map { objectId =>
|
||||
val paths = path.split("/")
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -486,6 +489,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
* Displays branches.
|
||||
*/
|
||||
get("/:owner/:repository/branches")(referrersOnly { repository =>
|
||||
val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet
|
||||
val branches = JGitUtil.getBranches(
|
||||
owner = repository.owner,
|
||||
name = repository.name,
|
||||
@@ -493,7 +497,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
origin = repository.repository.originUserName.isEmpty
|
||||
)
|
||||
.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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 registeredDate = column[java.util.Date]("REGISTERED_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
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,20 @@ case class CommitStatus(
|
||||
registeredDate: 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)
|
||||
|
||||
|
||||
@@ -49,5 +49,6 @@ trait CoreProfile extends ProfileProvider with Profile
|
||||
with SshKeyComponent
|
||||
with WebHookComponent
|
||||
with WebHookEventComponent
|
||||
with ProtectedBranchComponent
|
||||
|
||||
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)
|
||||
@@ -67,6 +67,16 @@ trait Plugin {
|
||||
*/
|
||||
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
|
||||
|
||||
/**
|
||||
* Override to add receive hooks.
|
||||
*/
|
||||
val receiveHooks: Seq[ReceiveHook] = Nil
|
||||
|
||||
/**
|
||||
* Override to add receive hooks.
|
||||
*/
|
||||
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
|
||||
|
||||
/**
|
||||
* This method is invoked in initialization of plugin system.
|
||||
* Register plugin functionality to PluginRegistry.
|
||||
@@ -87,6 +97,9 @@ trait Plugin {
|
||||
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
|
||||
registry.addRepositoryRouting(routing)
|
||||
}
|
||||
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
|
||||
registry.addReceiveHook(receiveHook)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import javax.servlet.ServletContext
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
|
||||
import gitbucket.core.controller.{Context, ControllerBase}
|
||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
@@ -30,6 +31,8 @@ class PluginRegistry {
|
||||
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
|
||||
)
|
||||
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
|
||||
private val receiveHooks = new ListBuffer[ReceiveHook]
|
||||
receiveHooks += new ProtectedBranchReceiveHook()
|
||||
|
||||
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
||||
plugins += pluginInfo
|
||||
@@ -99,6 +102,12 @@ class PluginRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
def addReceiveHook(commitHook: ReceiveHook): Unit = {
|
||||
receiveHooks += commitHook
|
||||
}
|
||||
|
||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
||||
|
||||
private case class GlobalAction(
|
||||
method: String,
|
||||
path: String,
|
||||
|
||||
15
src/main/scala/gitbucket/core/plugin/ReceiveHook.scala
Normal file
15
src/main/scala/gitbucket/core/plugin/ReceiveHook.scala
Normal file
@@ -0,0 +1,15 @@
|
||||
package gitbucket.core.plugin
|
||||
|
||||
import gitbucket.core.model.Profile._
|
||||
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
|
||||
import profile.simple._
|
||||
|
||||
trait ReceiveHook {
|
||||
|
||||
def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
|
||||
(implicit session: Session): Option[String] = None
|
||||
|
||||
def postReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
|
||||
(implicit session: Session): Unit = ()
|
||||
|
||||
}
|
||||
@@ -7,16 +7,19 @@ import gitbucket.core.model.{CommitState, CommitStatus, Account}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
|
||||
import org.joda.time.LocalDateTime
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
|
||||
trait CommitStatusService {
|
||||
/** insert or update */
|
||||
def createCommitStatus(userName: String, repositoryName: String, sha:String, context:String, state:CommitState, targetUrl:Option[String], description:Option[String], now:java.util.Date, creator:Account)(implicit s: Session): Int =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind )
|
||||
def createCommitStatus(userName: String, repositoryName: String, sha: String, context: String, state: CommitState,
|
||||
targetUrl: Option[String], description: Option[String], now: java.util.Date, creator: Account)(implicit s: Session): Int =
|
||||
CommitStatuses
|
||||
.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind )
|
||||
.map(_.commitStatusId).firstOption match {
|
||||
case Some(id: Int) => {
|
||||
CommitStatuses.filter(_.byPrimaryKey(id)).map{
|
||||
t => (t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
|
||||
CommitStatuses.filter(_.byPrimaryKey(id)).map { t =>
|
||||
(t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
|
||||
}.update((state, targetUrl, now, creator.userName, description))
|
||||
id
|
||||
}
|
||||
@@ -42,9 +45,11 @@ trait CommitStatusService {
|
||||
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
|
||||
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)] =
|
||||
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
|
||||
.filter{ case (t,a) => t.creator === a.userName }.list
|
||||
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts).filter { case (t, a) => t.creator === a.userName }.list
|
||||
|
||||
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha)).sortBy(_.updatedDate desc)
|
||||
|
||||
@@ -10,10 +10,9 @@ import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
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
|
||||
|
||||
|
||||
trait MergeService {
|
||||
import MergeService._
|
||||
/**
|
||||
@@ -52,26 +51,30 @@ 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 = {
|
||||
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${branch}"
|
||||
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
|
||||
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
|
||||
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
|
||||
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
|
||||
val remoteRefName = s"refs/heads/${remoteBranch}"
|
||||
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
|
||||
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
|
||||
try {
|
||||
// fetch objects from origin repository branch
|
||||
git.fetch
|
||||
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
|
||||
.setRemote(getRepositoryDir(remoteUserName, remoteRepositoryName).toURI.toString)
|
||||
.setRefSpecs(refSpec)
|
||||
.call
|
||||
// merge conflict check
|
||||
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)
|
||||
try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
if(merger.merge(mergeBaseTip, mergeTip)){
|
||||
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
case e: NoMergeBaseException => None
|
||||
}
|
||||
} finally {
|
||||
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 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){
|
||||
val repository = git.getRepository
|
||||
val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||
@@ -120,12 +169,7 @@ object MergeService{
|
||||
def updateBranch(treeId:ObjectId, message:String, branchName:String){
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
||||
// update refs
|
||||
val refUpdate = repository.updateRef(branchName)
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(true)
|
||||
refUpdate.setRefLogIdent(committer)
|
||||
refUpdate.update()
|
||||
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
|
||||
}
|
||||
if(!conflicted){
|
||||
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
|
||||
@@ -145,28 +189,12 @@ object MergeService{
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||
// update refs
|
||||
val refUpdate = repository.updateRef(s"refs/heads/${branch}")
|
||||
refUpdate.setNewObjectId(mergeCommitId)
|
||||
refUpdate.setForceUpdate(false)
|
||||
refUpdate.setRefLogIdent(committer)
|
||||
refUpdate.setRefLogMessage("merged", true)
|
||||
refUpdate.update()
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
|
||||
}
|
||||
// return treeId
|
||||
private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = {
|
||||
val mergeCommit = new CommitBuilder()
|
||||
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 createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
|
||||
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
|
||||
|
||||
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model._
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.plugin.ReceiveHook
|
||||
import profile.simple._
|
||||
|
||||
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
|
||||
|
||||
|
||||
trait ProtectedBranchService {
|
||||
import ProtectedBranchService._
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
object ProtectedBranchService {
|
||||
|
||||
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
|
||||
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, 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(receivePack.isAllowNonFastForwards, command, pusher)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.util.JGitUtil
|
||||
import profile.simple._
|
||||
@@ -145,4 +145,29 @@ object PullRequestService {
|
||||
|
||||
case class PullRequestCount(userName: String, count: Int)
|
||||
|
||||
case class MergeStatus(
|
||||
hasConflict: Boolean,
|
||||
commitStatues:List[CommitStatus],
|
||||
branchProtection: ProtectedBranchService.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ trait RepositoryService { self: AccountService =>
|
||||
val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val commitStatuses = CommitStatuses .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 =>
|
||||
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
|
||||
@@ -95,6 +97,8 @@ trait RepositoryService { self: AccountService =>
|
||||
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
CommitComments .insertAll(commitComments.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
|
||||
PullRequests.filter { t =>
|
||||
@@ -310,10 +314,16 @@ trait RepositoryService { self: AccountService =>
|
||||
* Save repository options.
|
||||
*/
|
||||
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))
|
||||
.map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
|
||||
.update (description, defaultBranch, isPrivate, currentDate)
|
||||
.map { r => (r.description.?, r.isPrivate, r.updatedDate) }
|
||||
.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.
|
||||
|
||||
@@ -111,13 +111,21 @@ import scala.collection.JavaConverters._
|
||||
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
|
||||
extends PostReceiveHook with PreReceiveHook
|
||||
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
|
||||
with WebHookPullRequestService {
|
||||
with WebHookPullRequestService with ProtectedBranchService {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
private var existIds: Seq[String] = Nil
|
||||
|
||||
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
|
||||
try {
|
||||
commands.asScala.foreach { command =>
|
||||
// call pre-commit hook
|
||||
PluginRegistry().getReceiveHooks
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
|
||||
.headOption.foreach { error =>
|
||||
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
|
||||
}
|
||||
}
|
||||
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
|
||||
existIds = JGitUtil.getAllCommitIds(git)
|
||||
}
|
||||
@@ -207,6 +215,9 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
newId = command.getNewId(), oldId = command.getOldId())
|
||||
}
|
||||
}
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
|
||||
@@ -289,10 +289,10 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
}
|
||||
|
||||
def commitStateIcon(state: CommitState) = Html(state match {
|
||||
case CommitState.PENDING => "●"
|
||||
case CommitState.SUCCESS => "✔"
|
||||
case CommitState.ERROR => "×"
|
||||
case CommitState.FAILURE => "×"
|
||||
case CommitState.PENDING => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-primitive-dot"></i>"""
|
||||
case CommitState.SUCCESS => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-check"></i>"""
|
||||
case CommitState.ERROR => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
|
||||
case CommitState.FAILURE => """<i style="color:inherit;width:inherit;height:inherit" class="octicon octicon-x"></i>"""
|
||||
})
|
||||
|
||||
def commitStateText(state: CommitState, commitId:String) = state match {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
case comment: gitbucket.core.model.IssueComment => Some(comment)
|
||||
case other => None
|
||||
}.exists(_.action == "merge")){ merged =>
|
||||
@if(hasWritePermission && !issue.closed){
|
||||
@if(!issue.closed){
|
||||
<div class="check-conflict" style="display: none;">
|
||||
<div class="box issue-comment-box" style="background-color: #fbeed5">
|
||||
<div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
|
||||
@@ -55,10 +55,12 @@ $(function(){
|
||||
$('#merge-pull-request').show();
|
||||
});
|
||||
|
||||
@if(hasWritePermission){
|
||||
$('.check-conflict').show();
|
||||
var checkConflict = $('.check-conflict').show();
|
||||
if(checkConflict.length){
|
||||
$.get('@url(repository)/pull/@issue.issueId/mergeguide', function(data){ $('.check-conflict').html(data); });
|
||||
}
|
||||
|
||||
@if(hasWritePermission){
|
||||
$('.delete-branch').click(function(e){
|
||||
var branchName = $(e.target).data('name');
|
||||
return confirm('Are you sure you want to remove the ' + branchName + ' branch?');
|
||||
|
||||
@@ -1,76 +1,93 @@
|
||||
@(hasConflict: Boolean,
|
||||
hasProblem: Boolean,
|
||||
@(status: gitbucket.core.service.PullRequestService.MergeStatus,
|
||||
issue: gitbucket.core.model.Issue,
|
||||
pullreq: gitbucket.core.model.PullRequest,
|
||||
statuses: List[model.CommitStatus],
|
||||
originRepository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
forkedRepository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import gitbucket.core.service.SystemSettingsService
|
||||
@import context._
|
||||
@import gitbucket.core.view.helpers._
|
||||
@import model.CommitState
|
||||
<div class="box issue-comment-box" style="background-color: @if(hasProblem){ #fbeed5 }else{ #d8f5cd };">
|
||||
<div class="box-content issue-content" style="border: 1px solid @if(hasProblem){ #c09853 }else{ #95c97e }; padding: 10px;">
|
||||
<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(status.hasProblem){ #c09853 }else{ #95c97e };padding:0">
|
||||
<div id="merge-pull-request">
|
||||
@if(!statuses.isEmpty){
|
||||
@if(!status.statuses.isEmpty){
|
||||
<div class="build-statuses">
|
||||
@if(statuses.size==1){
|
||||
@defining(statuses.head){ status =>
|
||||
<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">
|
||||
@defining(status.commitStateSummary){ case (summaryState, summary) =>
|
||||
<div class="build-status-item-header">
|
||||
<a class="pull-right" id="toggle-all-checks"></a>
|
||||
<span class="build-status-icon text-@{state.name}">@commitStateIcon(state)</span>
|
||||
<strong class="text-@{state.name}">@commitStateText(state, pullreq.commitIdTo)</strong>
|
||||
<span class="text-@{state.name}">— @{stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")} checks</span>
|
||||
<span class="build-status-icon text-@{summaryState.name}">@commitStateIcon(summaryState)</span>
|
||||
<strong class="text-@{summaryState.name}">@commitStateText(summaryState, pullreq.commitIdTo)</strong>
|
||||
<span class="text-@{summaryState.name}">— @summary checks</span>
|
||||
</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">
|
||||
@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="text-@{status.state.name}">@status.context</span>
|
||||
<strong>@status.context</strong>
|
||||
@status.description.map{ desc => <span class="muted">— @desc</span> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="pull-right">
|
||||
<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"}/>
|
||||
</div>
|
||||
<div>
|
||||
@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 style="padding:15px">
|
||||
@if(status.hasConflict){
|
||||
<div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
|
||||
<span class="strong">This branch has conflicts that must be resolved</span>
|
||||
<div class="small">
|
||||
@if(hasConflict){
|
||||
<a href="#" id="show-command-line">Use the command line</a> to resolve conflicts before continuing.
|
||||
@if(status.hasMergePermission){
|
||||
<a href="#" class="show-command-line">Use the command line</a> to resolve conflicts before continuing.
|
||||
} 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 id="command-line" style="display: none;">
|
||||
<hr>
|
||||
@if(hasConflict){
|
||||
} else { @if(status.branchIsOutOfDate){
|
||||
@if(status.hasUpdatePermission){
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
}
|
||||
</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>
|
||||
<p>
|
||||
@@ -116,6 +133,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="confirm-merge-form" style="display: none;">
|
||||
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
|
||||
<div class="strong">
|
||||
@@ -134,8 +153,8 @@
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#show-command-line').click(function(){
|
||||
$('#command-line').show();
|
||||
$('.show-command-line').click(function(){
|
||||
$('#command-line').toggle();
|
||||
return false;
|
||||
});
|
||||
function setToggleAllChecksLabel(){
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
dayByDayCommits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
|
||||
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
|
||||
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 gitbucket.core.view.helpers._
|
||||
@import gitbucket.core.model._
|
||||
@@ -73,6 +74,12 @@
|
||||
</ul>
|
||||
<div class="tab-content fill-width pull-left">
|
||||
<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)
|
||||
</div>
|
||||
<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,
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||
@import context._
|
||||
@@ -13,7 +13,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@branchInfo.map { case (branch, prs) =>
|
||||
@branchInfo.map { case (branch, prs, isProtected) =>
|
||||
<tr><td style="padding: 12px;">
|
||||
<div class="branch-action">
|
||||
@branch.mergeInfo.map{ info =>
|
||||
@@ -47,14 +47,19 @@
|
||||
<span style="margin-left: 8px;">
|
||||
@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>
|
||||
} 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 {
|
||||
<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>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
<span class="branch-meta">
|
||||
<span>Updated @helper.html.datetimeago(branch.commitTime, false)
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||
pathList: List[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 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.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">
|
||||
<span class="error" id="error-newFileName"></span>
|
||||
<div class="head">
|
||||
@@ -82,6 +86,9 @@ $(function(){
|
||||
@if(fileName.isDefined){
|
||||
editor.getSession().setMode("ace/mode/@editorType(fileName.get)");
|
||||
}
|
||||
@if(protectedBranch){
|
||||
editor.setReadOnly(true);
|
||||
}
|
||||
|
||||
editor.on('change', function(){
|
||||
updateCommitButtonStatus();
|
||||
|
||||
69
src/main/twirl/gitbucket/core/settings/branches.scala.html
Normal file
69
src/main/twirl/gitbucket/core/settings/branches.scala.html
Normal file
@@ -0,0 +1,69 @@
|
||||
@(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 strong">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 strong">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,123 @@
|
||||
@(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>
|
||||
<input type="checkbox" name="enabled" onclick="update()" @check(protection.enabled)>
|
||||
<span class="strong">Protect this branch</span>
|
||||
</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>
|
||||
<input type="checkbox" name="has_required_statuses" onclick="update()" @check(protection.status.enforcement_level.name!="off")>
|
||||
<span class="strong">Require status checks to pass before merging</span>
|
||||
</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>
|
||||
<input type="checkbox" name="enforce_for_admins" onclick="update()" @check(protection.status.enforcement_level.name=="everyone")>
|
||||
<span class="strong">Include administrators</span>
|
||||
</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 { context =>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="contexts" value="@context" onclick="update()" @check(protection.status.contexts.find(_ == context))>
|
||||
<span>@context</span>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input class="btn btn-success btn-lg" 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"}>
|
||||
<a href="@url(repository)/settings/collaborators">Collaborators</a>
|
||||
</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"}>
|
||||
<a href="@url(repository)/settings/hooks">Service Hooks</a>
|
||||
</li>
|
||||
|
||||
@@ -18,22 +18,6 @@
|
||||
<label for="description" class="strong">Description:</label>
|
||||
<input type="text" name="description" id="description" class="form-control" value="@repository.repository.description"/>
|
||||
</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">
|
||||
<label class="radio">
|
||||
<input type="radio" name="isPrivate" value="false"
|
||||
|
||||
@@ -1408,13 +1408,43 @@ div.author-info div.committer {
|
||||
margin: -10px -10px 10px -10px;
|
||||
}
|
||||
.build-statuses .build-status-item{
|
||||
padding: 10px 15px 10px 12px;
|
||||
padding: 10px 15px 10px 64px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.build-statuses-list .build-status-item{
|
||||
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 */
|
||||
/****************************************************************************/
|
||||
|
||||
@@ -354,6 +354,16 @@ class JsonFormatSpec extends Specification {
|
||||
}"""
|
||||
|
||||
|
||||
val apiBranchProtection = ApiBranchProtection(true, Some(ApiBranchProtection.Status(ApiBranchProtection.Everyone, Seq("continuous-integration/travis-ci"))))
|
||||
val apiBranchProtectionJson = """{
|
||||
"enabled": true,
|
||||
"required_status_checks": {
|
||||
"enforcement_level": "everyone",
|
||||
"contexts": [
|
||||
"continuous-integration/travis-ci"
|
||||
]
|
||||
}
|
||||
}"""
|
||||
|
||||
def beFormatted(json2Arg:String) = new Matcher[String] {
|
||||
def apply[S <: String](e: Expectable[S]) = {
|
||||
@@ -411,5 +421,8 @@ class JsonFormatSpec extends Specification {
|
||||
"apiPullRequestReviewComment" in {
|
||||
JsonFormat(apiPullRequestReviewComment) must beFormatted(apiPullRequestReviewCommentJson)
|
||||
}
|
||||
"apiBranchProtection" in {
|
||||
JsonFormat(apiBranchProtection) must beFormatted(apiBranchProtectionJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.util.GitSpecUtil._
|
||||
import org.specs2.mutable.Specification
|
||||
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import gitbucket.core.model.CommitState
|
||||
import gitbucket.core.service.ProtectedBranchService.{ProtectedBranchReceiveHook, ProtectedBranchInfo}
|
||||
import scalaz._, Scalaz._
|
||||
|
||||
class ProtectedBranchServiceSpec extends Specification with ServiceSpecBase with ProtectedBranchService with CommitStatusService {
|
||||
|
||||
val receiveHook = new ProtectedBranchReceiveHook()
|
||||
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 =>
|
||||
withTestRepository { git =>
|
||||
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) }
|
||||
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
|
||||
generateNewUserWithDBRepository("user1", "repo1")
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None
|
||||
enableBranchProtection("user1", "repo1", "branch", false, Nil)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("Cannot force-push to a protected branch")
|
||||
}
|
||||
}
|
||||
}
|
||||
"getBranchProtectedReason on force push from othre" in {
|
||||
withTestDB { implicit session =>
|
||||
withTestRepository { git =>
|
||||
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(true) }
|
||||
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE_NONFASTFORWARD)
|
||||
generateNewUserWithDBRepository("user1", "repo1")
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== None
|
||||
enableBranchProtection("user1", "repo1", "branch", false, Nil)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("Cannot force-push to a protected branch")
|
||||
}
|
||||
}
|
||||
}
|
||||
"getBranchProtectedReason check status on push from othre" in {
|
||||
withTestDB { implicit session =>
|
||||
withTestRepository { git =>
|
||||
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) }
|
||||
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
|
||||
val user1 = generateNewUserWithDBRepository("user1", "repo1")
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== None
|
||||
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("Required status check \"must\" is expected")
|
||||
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("2 of 2 required status checks are expected")
|
||||
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("2 of 2 required status checks are expected")
|
||||
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== Some("Required status check \"must2\" is expected")
|
||||
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user2") must_== None
|
||||
}
|
||||
}
|
||||
}
|
||||
"getBranchProtectedReason check status on push from admin" in {
|
||||
withTestDB { implicit session =>
|
||||
withTestRepository { git =>
|
||||
val rp = new ReceivePack(git.getRepository) <| { _.setAllowNonFastForwards(false) }
|
||||
val rc = new ReceiveCommand(ObjectId.fromString(sha), ObjectId.fromString(sha2), "refs/heads/branch", ReceiveCommand.Type.UPDATE)
|
||||
val user1 = generateNewUserWithDBRepository("user1", "repo1")
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None
|
||||
enableBranchProtection("user1", "repo1", "branch", false, Seq("must"))
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None
|
||||
enableBranchProtection("user1", "repo1", "branch", true, Seq("must"))
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("Required status check \"must\" is expected")
|
||||
enableBranchProtection("user1", "repo1", "branch", false, Seq("must", "must2"))
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None
|
||||
enableBranchProtection("user1", "repo1", "branch", true, Seq("must", "must2"))
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("2 of 2 required status checks are expected")
|
||||
createCommitStatus("user1", "repo1", sha2, "context", CommitState.SUCCESS, None, None, now, user1)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("2 of 2 required status checks are expected")
|
||||
createCommitStatus("user1", "repo1", sha2, "must", CommitState.SUCCESS, None, None, now, user1)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== Some("Required status check \"must2\" is expected")
|
||||
createCommitStatus("user1", "repo1", sha2, "must2", CommitState.SUCCESS, None, None, now, user1)
|
||||
receiveHook.preReceive("user1", "repo1", rp, rc, "user1") must_== None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"ProtectedBranchInfo" should {
|
||||
"administrator is owner" in {
|
||||
withTestDB { implicit session =>
|
||||
generateNewUserWithDBRepository("user1", "repo1")
|
||||
val x = ProtectedBranchInfo("user1", "repo1", true, Nil, false)
|
||||
x.isAdministrator("user1") must_== true
|
||||
x.isAdministrator("user2") must_== false
|
||||
}
|
||||
}
|
||||
"administrator is manager" in {
|
||||
withTestDB { implicit session =>
|
||||
val 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")
|
||||
val 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")
|
||||
val 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model._
|
||||
import gitbucket.core.model.Profile._
|
||||
|
||||
import org.specs2.mutable.Specification
|
||||
|
||||
|
||||
class RepositoryServiceSpec extends Specification with ServiceSpecBase with RepositoryService with AccountService{
|
||||
"RepositoryService" should {
|
||||
"renameRepository can rename CommitState" in { withTestDB { implicit session =>
|
||||
"renameRepository can rename CommitState, ProtectedBranches" in { withTestDB { implicit session =>
|
||||
val tester = generateNewAccount("tester")
|
||||
createRepository("repo","root",None,false)
|
||||
val commitStatusService = new CommitStatusService{}
|
||||
val id = commitStatusService.createCommitStatus(
|
||||
val service = new CommitStatusService with ProtectedBranchService {}
|
||||
val id = service.createCommitStatus(
|
||||
userName = "root",
|
||||
repositoryName = "repo",
|
||||
sha = "0e97b8f59f7cdd709418bb59de53f741fd1c1bd7",
|
||||
@@ -22,14 +20,20 @@ class RepositoryServiceSpec extends Specification with ServiceSpecBase with Repo
|
||||
description = Some("description"),
|
||||
creator = tester,
|
||||
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")
|
||||
val neo = commitStatusService.getCommitStatus("tester","repo2", org.commitId, org.context).get
|
||||
|
||||
val neo = service.getCommitStatus("tester","repo2", org.commitId, org.context).get
|
||||
neo must_==
|
||||
org.copy(
|
||||
commitStatusId=neo.commitStatusId,
|
||||
repositoryName="repo2",
|
||||
userName="tester")
|
||||
service.getProtectedBranchInfo("tester", "repo2", "branch") must_==
|
||||
orgPbi.copy(owner="tester", repository="repo2")
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user