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:
Naoki Takezoe
2016-01-19 15:31:26 +09:00
36 changed files with 1244 additions and 267 deletions

View File

@@ -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.

View 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;

View File

@@ -0,0 +1,16 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
case class ApiBranch(
name: String,
// commit: ApiBranchCommit,
protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable {
def _links = Map(
"self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"),
"html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}"))
}

View File

@@ -0,0 +1,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)
}
))
}

View File

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

View File

@@ -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.

View File

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

View File

@@ -2,7 +2,7 @@ package gitbucket.core.controller
import gitbucket.core.settings.html
import gitbucket.core.model.WebHook
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, 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.
*/

View File

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

View File

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

View File

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

View File

@@ -49,5 +49,6 @@ trait CoreProfile extends ProfileProvider with Profile
with SshKeyComponent
with WebHookComponent
with WebHookEventComponent
with ProtectedBranchComponent
object Profile extends CoreProfile

View 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)

View File

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

View File

@@ -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,

View 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 = ()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 => "&#x2714;"
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 {

View File

@@ -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?');

View File

@@ -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 cant 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(){

View File

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

View File

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

View File

@@ -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();

View 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 dont 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>
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -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 */
/****************************************************************************/

View File

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

View File

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

View File

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