diff --git a/README.md b/README.md
index fc3338b..c24a772 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,10 @@
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
=========
-GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
+GitBucket is a Git platform powered by Scala offering:
+- easy installation
+- high extensibility by plugins
+- API compatibility with Github
Features
--------
@@ -56,10 +59,16 @@
- Make sure check whether there is a same question or request in the past.
- When raise a new issue, write subject in **English** at least.
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
-- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
+- First priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it.
Release Notes
--------
+### 3.13 - 1 Apr 2016
+- Refresh user interface for wide screen
+- Add `pull_request` key in list issues API for pull requests
+- Add `X-Hub-Signature` security to webhooks
+- Provide SHA-256 checksum for `gitbucket.war`
+
### 3.12 - 27 Feb 2016
- New GitHub UI
- Improve mobile view
diff --git a/build.sbt b/build.sbt
index dbd5567..3734c7f 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,6 +1,6 @@
val Organization = "gitbucket"
val Name = "gitbucket"
-val GitBucketVersion = "3.12.0"
+val GitBucketVersion = "3.14.0-SNAPSHOT"
val ScalatraVersion = "2.4.0"
val JettyVersion = "9.3.6.v20151106"
@@ -10,7 +10,7 @@
organization := Organization
name := Name
version := GitBucketVersion
-scalaVersion := "2.11.7"
+scalaVersion := "2.11.8"
// dependency settings
resolvers ++= Seq(
@@ -19,6 +19,7 @@
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
)
libraryDependencies ++= Seq(
+ "org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0",
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
@@ -27,7 +28,7 @@
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "solidbase" % "1.0.0-SNAPSHOT",
- "io.github.gitbucket" % "markedj" % "1.0.7-SNAPSHOT",
+ "io.github.gitbucket" % "markedj" % "1.0.7",
"org.apache.commons" % "commons-compress" % "1.10",
"org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
@@ -53,7 +54,7 @@
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
// Compiler settings
-scalacOptions := Seq("-deprecation", "-language:postfixOps")
+scalacOptions := Seq("-deprecation", "-language:postfixOps", "-Ybackend:GenBCode", "-Ydelambdafy:method", "-target:jvm-1.8")
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
diff --git a/release/pom.xml b/release/pom.xml
index 40693f2..0d84d21 100644
--- a/release/pom.xml
+++ b/release/pom.xml
@@ -10,7 +10,7 @@
org.apache.maven.wagonwagon-ssh
- 1.0-beta-6
+ 2.10
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index b266261..30c507a 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -27,9 +27,10 @@
}
context.mount(new IndexController, "/")
+ context.mount(new ApiController, "/api/v3")
context.mount(new FileUploadController, "/upload")
+ context.mount(new SystemSettingsController, "/admin")
context.mount(new DashboardController, "/*")
- context.mount(new SystemSettingsController, "/*")
context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")
diff --git a/src/main/scala/gitbucket/core/api/ApiIssue.scala b/src/main/scala/gitbucket/core/api/ApiIssue.scala
index a73a335..47374ed 100644
--- a/src/main/scala/gitbucket/core/api/ApiIssue.scala
+++ b/src/main/scala/gitbucket/core/api/ApiIssue.scala
@@ -20,6 +20,16 @@
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
+ val pull_request = if (isPullRequest) {
+ Some(Map(
+ "url" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${number}"),
+ "html_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}")
+ // "diff_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.diff"),
+ // "patch_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.patch")
+ ))
+ } else {
+ None
+ }
}
object ApiIssue{
diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala
index 54bdc58..5ddc010 100644
--- a/src/main/scala/gitbucket/core/controller/AccountController.scala
+++ b/src/main/scala/gitbucket/core/controller/AccountController.scala
@@ -1,7 +1,6 @@
package gitbucket.core.controller
import gitbucket.core.account.html
-import gitbucket.core.api._
import gitbucket.core.helper
import gitbucket.core.model.GroupMember
import gitbucket.core.service._
@@ -14,22 +13,19 @@
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
-import org.eclipse.jgit.api.Git
-import org.eclipse.jgit.dircache.DirCache
-import org.eclipse.jgit.lib.{FileMode, Constants}
import org.scalatra.i18n.Messages
class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
- with AccessTokenService with WebHookService
+ with AccessTokenService with WebHookService with RepositoryCreationService
trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
- with AccessTokenService with WebHookService =>
+ with AccessTokenService with WebHookService with RepositoryCreationService =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
@@ -156,25 +152,6 @@
}
}
- /**
- * https://developer.github.com/v3/users/#get-a-single-user
- */
- get("/api/v3/users/:userName") {
- getAccountByUserName(params("userName")).map { account =>
- JsonFormat(ApiUser(account))
- } getOrElse NotFound
- }
-
- /**
- * https://developer.github.com/v3/users/#get-the-authenticated-user
- */
- get("/api/v3/user") {
- context.loginAccount.map { account =>
- JsonFormat(ApiUser(account))
- } getOrElse Unauthorized
- }
-
-
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
@@ -367,7 +344,7 @@
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name).isEmpty){
- createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
+ createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
}
// redirect to the repository
@@ -375,54 +352,6 @@
}
})
- /**
- * Create user repository
- * https://developer.github.com/v3/repos/#create
- */
- post("/api/v3/user/repos")(usersOnly {
- val owner = context.loginAccount.get.userName
- (for {
- data <- extractFromJsonBody[CreateARepository] if data.isValid
- } yield {
- LockUtil.lock(s"${owner}/${data.name}") {
- if(getRepository(owner, data.name).isEmpty){
- createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
- val repository = getRepository(owner, data.name).get
- JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
- } else {
- ApiError(
- "A repository with this name already exists on this account",
- Some("https://developer.github.com/v3/repos/#create")
- )
- }
- }
- }) getOrElse NotFound
- })
-
- /**
- * Create group repository
- * https://developer.github.com/v3/repos/#create
- */
- post("/api/v3/orgs/:org/repos")(managersOnly {
- val groupName = params("org")
- (for {
- data <- extractFromJsonBody[CreateARepository] if data.isValid
- } yield {
- LockUtil.lock(s"${groupName}/${data.name}") {
- if(getRepository(groupName, data.name).isEmpty){
- createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
- val repository = getRepository(groupName, data.name).get
- JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
- } else {
- ApiError(
- "A repository with this name already exists for this group",
- Some("https://developer.github.com/v3/repos/#create")
- )
- }
- }
- }) getOrElse NotFound
- })
-
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
@@ -456,7 +385,7 @@
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
- createRepository(
+ insertRepository(
repositoryName = repository.name,
userName = accountName,
description = repository.repository.description,
@@ -496,68 +425,6 @@
}
})
- private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
- val ownerAccount = getAccountByUserName(owner).get
- val loginAccount = context.loginAccount.get
- val loginUserName = loginAccount.userName
-
- // Insert to the database at first
- createRepository(name, owner, description, isPrivate)
-
- // Add collaborators for group repository
- if(ownerAccount.isGroupAccount){
- getGroupMembers(owner).foreach { member =>
- addCollaborator(owner, name, member.userName)
- }
- }
-
- // Insert default labels
- insertDefaultLabels(owner, name)
-
- // Create the actual repository
- val gitdir = getRepositoryDir(owner, name)
- JGitUtil.initRepository(gitdir)
-
- if(createReadme){
- using(Git.open(gitdir)){ git =>
- val builder = DirCache.newInCore.builder()
- val inserter = git.getRepository.newObjectInserter()
- val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
- val content = if(description.nonEmpty){
- name + "\n" +
- "===============\n" +
- "\n" +
- description.get
- } else {
- name + "\n" +
- "===============\n"
- }
-
- builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
- inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
- builder.finish()
-
- JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
- Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
- }
- }
-
- // Create Wiki repository
- createWikiRepository(loginAccount, owner, name)
-
- // Record activity
- recordCreateRepositoryActivity(owner, name, loginUserName)
- }
-
- private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
- createLabel(userName, repositoryName, "bug", "fc2929")
- createLabel(userName, repositoryName, "duplicate", "cccccc")
- createLabel(userName, repositoryName, "enhancement", "84b6eb")
- createLabel(userName, repositoryName, "invalid", "e6e6e6")
- createLabel(userName, repositoryName, "question", "cc317c")
- createLabel(userName, repositoryName, "wontfix", "ffffff")
- }
-
private def existsAccount: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala
new file mode 100644
index 0000000..11d764b
--- /dev/null
+++ b/src/main/scala/gitbucket/core/controller/ApiController.scala
@@ -0,0 +1,389 @@
+package gitbucket.core.controller
+
+import gitbucket.core.api._
+import gitbucket.core.model._
+import gitbucket.core.service.IssuesService.IssueSearchCondition
+import gitbucket.core.service.PullRequestService._
+import gitbucket.core.service._
+import gitbucket.core.util.ControlUtil._
+import gitbucket.core.util.Directory._
+import gitbucket.core.util.JGitUtil.CommitInfo
+import gitbucket.core.util._
+import gitbucket.core.util.Implicits._
+import org.eclipse.jgit.api.Git
+import org.scalatra.{NoContent, UnprocessableEntity, Created}
+import scala.collection.JavaConverters._
+
+class ApiController extends ApiControllerBase
+ with RepositoryService
+ with AccountService
+ with ProtectedBranchService
+ with IssuesService
+ with LabelsService
+ with PullRequestService
+ with CommitStatusService
+ with RepositoryCreationService
+ with HandleCommentService
+ with WebHookService
+ with WebHookPullRequestService
+ with WebHookIssueCommentService
+ with WikiService
+ with ActivityService
+ with OwnerAuthenticator
+ with UsersAuthenticator
+ with GroupManagerAuthenticator
+ with ReferrerAuthenticator
+ with ReadableUsersAuthenticator
+ with CollaboratorsAuthenticator
+
+trait ApiControllerBase extends ControllerBase {
+ self: RepositoryService
+ with AccountService
+ with ProtectedBranchService
+ with IssuesService
+ with LabelsService
+ with PullRequestService
+ with CommitStatusService
+ with RepositoryCreationService
+ with HandleCommentService
+ with OwnerAuthenticator
+ with UsersAuthenticator
+ with GroupManagerAuthenticator
+ with ReferrerAuthenticator
+ with ReadableUsersAuthenticator
+ with CollaboratorsAuthenticator =>
+
+ /**
+ * https://developer.github.com/v3/users/#get-a-single-user
+ */
+ get("/api/v3/users/:userName") {
+ getAccountByUserName(params("userName")).map { account =>
+ JsonFormat(ApiUser(account))
+ } getOrElse NotFound
+ }
+
+ /**
+ * https://developer.github.com/v3/users/#get-the-authenticated-user
+ */
+ get("/api/v3/user") {
+ context.loginAccount.map { account =>
+ JsonFormat(ApiUser(account))
+ } getOrElse Unauthorized
+ }
+
+ /**
+ * Create user repository
+ * https://developer.github.com/v3/repos/#create
+ */
+ post("/api/v3/user/repos")(usersOnly {
+ val owner = context.loginAccount.get.userName
+ (for {
+ data <- extractFromJsonBody[CreateARepository] if data.isValid
+ } yield {
+ LockUtil.lock(s"${owner}/${data.name}") {
+ if(getRepository(owner, data.name).isEmpty){
+ createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
+ val repository = getRepository(owner, data.name).get
+ JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
+ } else {
+ ApiError(
+ "A repository with this name already exists on this account",
+ Some("https://developer.github.com/v3/repos/#create")
+ )
+ }
+ }
+ }) getOrElse NotFound
+ })
+
+ /**
+ * Create group repository
+ * https://developer.github.com/v3/repos/#create
+ */
+ post("/api/v3/orgs/:org/repos")(managersOnly {
+ val groupName = params("org")
+ (for {
+ data <- extractFromJsonBody[CreateARepository] if data.isValid
+ } yield {
+ LockUtil.lock(s"${groupName}/${data.name}") {
+ if(getRepository(groupName, data.name).isEmpty){
+ createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
+ val repository = getRepository(groupName, data.name).get
+ JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
+ } else {
+ ApiError(
+ "A repository with this name already exists for this group",
+ Some("https://developer.github.com/v3/repos/#create")
+ )
+ }
+ }
+ }) getOrElse NotFound
+ })
+
+ /** 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
+ })
+
+ /**
+ * @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
+ * but not enabled.
+ */
+ get("/api/v3/rate_limit"){
+ contentType = formats("json")
+ // this message is same as github enterprise...
+ org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
+ }
+
+ /**
+ * https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
+ */
+ get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
+ (for{
+ issueId <- params("id").toIntOpt
+ comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
+ } yield {
+ JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
+ }).getOrElse(NotFound)
+ })
+
+ /**
+ * https://developer.github.com/v3/issues/comments/#create-a-comment
+ */
+ post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
+ (for{
+ issueId <- params("id").toIntOpt
+ issue <- getIssue(repository.owner, repository.name, issueId.toString)
+ body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
+ action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
+ (issue, id) <- handleComment(issue, Some(body), repository, action)
+ issueComment <- getComment(repository.owner, repository.name, id.toString())
+ } yield {
+ JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
+ }) getOrElse NotFound
+ })
+
+ /**
+ * List all labels for this repository
+ * https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
+ */
+ get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
+ JsonFormat(getLabels(repository.owner, repository.name).map { label =>
+ ApiLabel(label, RepositoryName(repository))
+ })
+ })
+
+ /**
+ * Get a single label
+ * https://developer.github.com/v3/issues/labels/#get-a-single-label
+ */
+ get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
+ getLabel(repository.owner, repository.name, params("labelName")).map { label =>
+ JsonFormat(ApiLabel(label, RepositoryName(repository)))
+ } getOrElse NotFound()
+ })
+
+ /**
+ * Create a label
+ * https://developer.github.com/v3/issues/labels/#create-a-label
+ */
+ post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
+ (for{
+ data <- extractFromJsonBody[CreateALabel] if data.isValid
+ } yield {
+ LockUtil.lock(RepositoryName(repository).fullName) {
+ if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
+ val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
+ getLabel(repository.owner, repository.name, labelId).map { label =>
+ Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
+ } getOrElse NotFound()
+ } else {
+ // TODO ApiError should support errors field to enhance compatibility of GitHub API
+ UnprocessableEntity(ApiError(
+ "Validation Failed",
+ Some("https://developer.github.com/v3/issues/labels/#create-a-label")
+ ))
+ }
+ }
+ }) getOrElse NotFound()
+ })
+
+ /**
+ * Update a label
+ * https://developer.github.com/v3/issues/labels/#update-a-label
+ */
+ patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
+ (for{
+ data <- extractFromJsonBody[CreateALabel] if data.isValid
+ } yield {
+ LockUtil.lock(RepositoryName(repository).fullName) {
+ getLabel(repository.owner, repository.name, params("labelName")).map { label =>
+ if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
+ updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
+ JsonFormat(ApiLabel(
+ getLabel(repository.owner, repository.name, label.labelId).get,
+ RepositoryName(repository)))
+ } else {
+ // TODO ApiError should support errors field to enhance compatibility of GitHub API
+ UnprocessableEntity(ApiError(
+ "Validation Failed",
+ Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
+ }
+ } getOrElse NotFound()
+ }
+ }) getOrElse NotFound()
+ })
+
+ /**
+ * Delete a label
+ * https://developer.github.com/v3/issues/labels/#delete-a-label
+ */
+ delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
+ LockUtil.lock(RepositoryName(repository).fullName) {
+ getLabel(repository.owner, repository.name, params("labelName")).map { label =>
+ deleteLabel(repository.owner, repository.name, label.labelId)
+ NoContent()
+ } getOrElse NotFound()
+ }
+ })
+
+ /**
+ * https://developer.github.com/v3/pulls/#list-pull-requests
+ */
+ get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
+ val page = IssueSearchCondition.page(request)
+ // TODO: more api spec condition
+ val condition = IssueSearchCondition(request)
+ val baseOwner = getAccountByUserName(repository.owner).get
+ val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
+ JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
+ ApiPullRequest(
+ issue,
+ pullRequest,
+ ApiRepository(headRepo, ApiUser(headOwner)),
+ ApiRepository(repository, ApiUser(baseOwner)),
+ ApiUser(issueUser)) })
+ })
+
+ /**
+ * https://developer.github.com/v3/pulls/#get-a-single-pull-request
+ */
+ get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
+ (for{
+ issueId <- params("id").toIntOpt
+ (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
+ users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
+ baseOwner <- users.get(repository.owner)
+ headOwner <- users.get(pullRequest.requestUserName)
+ issueUser <- users.get(issue.openedUserName)
+ headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
+ } yield {
+ JsonFormat(ApiPullRequest(
+ issue,
+ pullRequest,
+ ApiRepository(headRepo, ApiUser(headOwner)),
+ ApiRepository(repository, ApiUser(baseOwner)),
+ ApiUser(issueUser)))
+ }).getOrElse(NotFound)
+ })
+
+ /**
+ * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
+ */
+ get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
+ val owner = repository.owner
+ val name = repository.name
+ params("id").toIntOpt.flatMap{ issueId =>
+ getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
+ using(Git.open(getRepositoryDir(owner, name))){ git =>
+ val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
+ val newId = git.getRepository.resolve(pullreq.commitIdTo)
+ val repoFullName = RepositoryName(repository)
+ val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
+ JsonFormat(commits)
+ }
+ }
+ } getOrElse NotFound
+ })
+
+ /**
+ * https://developer.github.com/v3/repos/#get
+ */
+ get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
+ JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
+ })
+
+ /**
+ * https://developer.github.com/v3/repos/statuses/#create-a-status
+ */
+ post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
+ (for{
+ ref <- params.get("sha")
+ sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
+ data <- extractFromJsonBody[CreateAStatus] if data.isValid
+ creator <- context.loginAccount
+ state <- CommitState.valueOf(data.state)
+ statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
+ state, data.target_url, data.description, new java.util.Date(), creator)
+ status <- getCommitStatus(repository.owner, repository.name, statusId)
+ } yield {
+ JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
+ }) getOrElse NotFound
+ })
+
+ /**
+ * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
+ *
+ * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
+ */
+ val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
+ (for{
+ ref <- params.get("ref")
+ sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
+ } yield {
+ JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
+ ApiCommitStatus(status, ApiUser(creator))
+ })
+ }) getOrElse NotFound
+ })
+
+ /**
+ * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
+ *
+ * legacy route
+ */
+ get("/api/v3/repos/:owner/:repo/statuses/:ref"){
+ listStatusesRoute.action()
+ }
+
+ /**
+ * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
+ *
+ * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
+ */
+ get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
+ (for{
+ ref <- params.get("ref")
+ owner <- getAccountByUserName(repository.owner)
+ sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
+ } yield {
+ val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
+ JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
+ }) getOrElse NotFound
+ })
+
+ private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
+ hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
+
+}
+
diff --git a/src/main/scala/gitbucket/core/controller/DashboardController.scala b/src/main/scala/gitbucket/core/controller/DashboardController.scala
index 1516c0f..f2a0f11 100644
--- a/src/main/scala/gitbucket/core/controller/DashboardController.scala
+++ b/src/main/scala/gitbucket/core/controller/DashboardController.scala
@@ -108,7 +108,9 @@
case _ => condition.copy(author = Some(userName))
},
filter,
- getGroupNames(userName))
+ getGroupNames(userName),
+ getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
+ getUserRepositories(userName, withoutPhysicalInfo = true))
}
private def searchPullRequests(filter: String) = {
@@ -131,7 +133,9 @@
case _ => condition.copy(author = Some(userName))
},
filter,
- getGroupNames(userName))
+ getGroupNames(userName),
+ getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
+ getUserRepositories(userName, withoutPhysicalInfo = true))
}
diff --git a/src/main/scala/gitbucket/core/controller/FileUploadController.scala b/src/main/scala/gitbucket/core/controller/FileUploadController.scala
index d6213a2..e74f9d4 100644
--- a/src/main/scala/gitbucket/core/controller/FileUploadController.scala
+++ b/src/main/scala/gitbucket/core/controller/FileUploadController.scala
@@ -1,18 +1,24 @@
package gitbucket.core.controller
-import gitbucket.core.util.{Keys, FileUtil}
+import gitbucket.core.model.Account
+import gitbucket.core.service.{AccountService, RepositoryService}
+import gitbucket.core.servlet.Database
+import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.dircache.DirCache
+import org.eclipse.jgit.lib.{FileMode, Constants}
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
-import org.apache.commons.io.FileUtils
+import org.apache.commons.io.{IOUtils, FileUtils}
/**
* Provides Ajax based file upload functionality.
*
* This servlet saves uploaded file.
*/
-class FileUploadController extends ScalatraServlet with FileUploadSupport {
+class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
@@ -31,6 +37,54 @@
}, FileUtil.isUploadableType)
}
+ post("/wiki/:owner/:repository"){
+ // Don't accept not logged-in users
+ session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account =>
+ val owner = params("owner")
+ val repository = params("repository")
+
+ // Check whether logged-in user is collaborator
+ collaboratorsOnly(owner, repository, loginAccount){
+ execute({ (file, fileId) =>
+ val fileName = file.getName
+ LockUtil.lock(s"${owner}/${repository}/wiki") {
+ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
+ val builder = DirCache.newInCore.builder()
+ val inserter = git.getRepository.newObjectInserter()
+ val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
+
+ if(headId != null){
+ JGitUtil.processTree(git, headId){ (path, tree) =>
+ if(path != fileName){
+ builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
+ }
+ }
+ }
+
+ val bytes = IOUtils.toByteArray(file.getInputStream)
+ builder.add(JGitUtil.createDirCacheEntry(fileName, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes)))
+ builder.finish()
+
+ val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
+ Constants.HEAD, loginAccount.userName, loginAccount.mailAddress, s"Uploaded ${fileName}")
+
+ fileName
+ }
+ }
+ }, FileUtil.isImage)
+ }
+ } getOrElse BadRequest
+ }
+
+ private def collaboratorsOnly(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = {
+ implicit val session = Database.getSession(request)
+ loginAccount match {
+ case x if(x.isAdmin) => action
+ case x if(getCollaborators(owner, repository).contains(x.userName)) => action
+ case _ => BadRequest
+ }
+ }
+
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
diff --git a/src/main/scala/gitbucket/core/controller/IndexController.scala b/src/main/scala/gitbucket/core/controller/IndexController.scala
index 5c83cbb..ba3cfd8 100644
--- a/src/main/scala/gitbucket/core/controller/IndexController.scala
+++ b/src/main/scala/gitbucket/core/controller/IndexController.scala
@@ -1,9 +1,8 @@
package gitbucket.core.controller
-import gitbucket.core.api._
import gitbucket.core.helper.xml
import gitbucket.core.model.Account
-import gitbucket.core.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService}
+import gitbucket.core.service._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
@@ -121,16 +120,6 @@
getAccountByUserName(params("userName")).isDefined
})
- /**
- * @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
- * but not enabled.
- */
- get("/api/v3/rate_limit"){
- contentType = formats("json")
- // this message is same as github enterprise...
- org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
- }
-
// TODO Move to RepositoryViwerController?
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
@@ -148,13 +137,21 @@
target.toLowerCase match {
case "issue" => gitbucket.core.search.html.issues(
- searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
+ searchIssues(repository.owner, repository.name, query),
+ countWikiPages(repository.owner, repository.name, query),
+ query, page, repository)
+
+ case "wiki" => gitbucket.core.search.html.wiki(
+ countFiles(repository.owner, repository.name, query),
+ countIssues(repository.owner, repository.name, query),
+ searchWikiPages(repository.owner, repository.name, query),
query, page, repository)
case _ => gitbucket.core.search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
+ countWikiPages(repository.owner, repository.name, query),
query, page, repository)
}
}
diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala
index 6ef4964..912c4f9 100644
--- a/src/main/scala/gitbucket/core/controller/IssuesController.scala
+++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala
@@ -1,8 +1,6 @@
package gitbucket.core.controller
-import gitbucket.core.api._
import gitbucket.core.issues.html
-import gitbucket.core.model.Issue
import gitbucket.core.service.IssuesService._
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
@@ -16,11 +14,11 @@
class IssuesController extends IssuesControllerBase
- with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
+ with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
trait IssuesControllerBase extends ControllerBase {
- self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
+ self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
case class IssueCreateForm(title: String, content: Option[String],
@@ -78,18 +76,6 @@
}
})
- /**
- * https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
- */
- get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
- (for{
- issueId <- params("id").toIntOpt
- comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
- } yield {
- JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
- }).getOrElse(NotFound)
- })
-
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
html.create(
@@ -128,7 +114,7 @@
getIssue(owner, name, issueId.toString).foreach { issue =>
// extract references and create refer comment
- createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
+ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
@@ -150,7 +136,7 @@
// update issue
updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment
- createReferComment(owner, name, issue.copy(title = title), title)
+ createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
@@ -165,7 +151,7 @@
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
- createReferComment(owner, name, issue, content.getOrElse(""))
+ createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
@@ -174,30 +160,22 @@
})
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
- redirect(s"/${repository.owner}/${repository.name}/${
- if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
+ getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
+ val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
+ handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
+ redirect(s"/${repository.owner}/${repository.name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
+ }
} getOrElse NotFound
})
- /**
- * https://developer.github.com/v3/issues/comments/#create-a-comment
- */
- post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
- (for{
- issueId <- params("id").toIntOpt
- body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
- (issue, id) <- handleComment(issueId, Some(body), repository)()
- issueComment <- getComment(repository.owner, repository.name, id.toString())
- } yield {
- JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
- }) getOrElse NotFound
- })
-
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
- handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
- redirect(s"/${repository.owner}/${repository.name}/${
- if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
+ getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
+ val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
+ handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
+ redirect(s"/${repository.owner}/${repository.name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
+ }
} getOrElse NotFound
})
@@ -315,8 +293,16 @@
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action =>
action match {
- case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
- case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
+ case Some("open") => executeBatch(repository) { issueId =>
+ getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
+ handleComment(issue, None, repository, Some("reopen"))
+ }
+ }
+ case Some("close") => executeBatch(repository) { issueId =>
+ getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
+ handleComment(issue, None, repository, Some("close"))
+ }
+ }
case _ => // TODO BadRequest
}
}
@@ -373,99 +359,6 @@
}
}
- // TODO Same method exists in PullRequestController. Should it moved to IssueService?
- private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
- StringUtil.extractIssueId(message).foreach { issueId =>
- val content = fromIssue.issueId + ":" + fromIssue.title
- if(getIssue(owner, repository, issueId).isDefined){
- // Not add if refer comment already exist.
- if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
- createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
- }
- }
- }
- }
-
- /**
- * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
- */
- private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
- (getAction: Issue => Option[String] =
- p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
-
- defining(repository.owner, repository.name){ case (owner, name) =>
- val userName = context.loginAccount.get.userName
-
- getIssue(owner, name, issueId.toString) flatMap { issue =>
- val (action, recordActivity) =
- getAction(issue)
- .collect {
- case "close" if(!issue.closed) => true ->
- (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
- case "reopen" if(issue.closed) => false ->
- (Some("reopen") -> Some(recordReopenIssueActivity _))
- }
- .map { case (closed, t) =>
- updateClosed(owner, name, issueId, closed)
- t
- }
- .getOrElse(None -> None)
-
- val commentId = (content, action) match {
- case (None, None) => None
- case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
- case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
- }
-
- // record comment activity if comment is entered
- content foreach {
- (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
- (owner, name, userName, issueId, _)
- }
- recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
-
- // extract references and create refer comment
- content.map { content =>
- createReferComment(owner, name, issue, content)
- }
-
- // call web hooks
- action match {
- case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
- case Some(act) => val webHookAction = act match {
- case "open" => "opened"
- case "reopen" => "reopened"
- case "close" => "closed"
- case _ => act
- }
- if(issue.isPullRequest){
- callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
- } else {
- callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
- }
- }
-
- // notifications
- Notifier() match {
- case f =>
- content foreach {
- f.toNotify(repository, issue, _){
- Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
- if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
- }
- }
- action foreach {
- f.toNotify(repository, issue, _){
- Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
- }
- }
- }
-
- commentId.map( issue -> _ )
- }
- }
- }
-
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request)
diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala
index 4074c61..9b17841 100644
--- a/src/main/scala/gitbucket/core/controller/LabelsController.scala
+++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala
@@ -1,13 +1,12 @@
package gitbucket.core.controller
-import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
-import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator}
+import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
-import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
+import org.scalatra.Ok
class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService
@@ -24,6 +23,7 @@
"labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
+
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
html.list(
getLabels(repository.owner, repository.name),
@@ -32,26 +32,6 @@
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
- /**
- * List all labels for this repository
- * https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
- */
- get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
- JsonFormat(getLabels(repository.owner, repository.name).map { label =>
- ApiLabel(label, RepositoryName(repository))
- })
- })
-
- /**
- * Get a single label
- * https://developer.github.com/v3/issues/labels/#get-a-single-label
- */
- get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
- getLabel(repository.owner, repository.name, params("labelName")).map { label =>
- JsonFormat(ApiLabel(label, RepositoryName(repository)))
- } getOrElse NotFound()
- })
-
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
html.edit(None, repository)
})
@@ -66,31 +46,6 @@
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
- /**
- * Create a label
- * https://developer.github.com/v3/issues/labels/#create-a-label
- */
- post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
- (for{
- data <- extractFromJsonBody[CreateALabel] if data.isValid
- } yield {
- LockUtil.lock(RepositoryName(repository).fullName) {
- if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
- val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
- getLabel(repository.owner, repository.name, labelId).map { label =>
- Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
- } getOrElse NotFound()
- } else {
- // TODO ApiError should support errors field to enhance compatibility of GitHub API
- UnprocessableEntity(ApiError(
- "Validation Failed",
- Some("https://developer.github.com/v3/issues/labels/#create-a-label")
- ))
- }
- }
- }) getOrElse NotFound()
- })
-
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository)
@@ -107,51 +62,12 @@
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
- /**
- * Update a label
- * https://developer.github.com/v3/issues/labels/#update-a-label
- */
- patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
- (for{
- data <- extractFromJsonBody[CreateALabel] if data.isValid
- } yield {
- LockUtil.lock(RepositoryName(repository).fullName) {
- getLabel(repository.owner, repository.name, params("labelName")).map { label =>
- if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
- updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
- JsonFormat(ApiLabel(
- getLabel(repository.owner, repository.name, label.labelId).get,
- RepositoryName(repository)))
- } else {
- // TODO ApiError should support errors field to enhance compatibility of GitHub API
- UnprocessableEntity(ApiError(
- "Validation Failed",
- Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
- }
- } getOrElse NotFound()
- }
- }) getOrElse NotFound()
- })
-
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok()
})
/**
- * Delete a label
- * https://developer.github.com/v3/issues/labels/#delete-a-label
- */
- delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
- LockUtil.lock(RepositoryName(repository).fullName) {
- getLabel(repository.owner, repository.name, params("labelName")).map { label =>
- deleteLabel(repository.owner, repository.name, label.labelId)
- NoContent()
- } getOrElse NotFound()
- }
- })
-
- /**
* Constraint for the identifier such as user name, repository name or page name.
*/
private def labelName: Constraint = new Constraint(){
@@ -169,7 +85,11 @@
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
val owner = params("owner")
val repository = params("repository")
- getLabel(owner, repository, value).map(_ => "Name has already been taken.")
+ params.get("labelId").map { labelId =>
+ getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.")
+ }.getOrElse {
+ getLabel(owner, repository, value).map(_ => "Name has already been taken.")
+ }
}
}
diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
index 11141ff..48f0aa2 100644
--- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
+++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala
@@ -1,7 +1,6 @@
package gitbucket.core.controller
-import gitbucket.core.api._
-import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
+import gitbucket.core.model.WebHook
import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
@@ -82,24 +81,6 @@
}
})
- /**
- * https://developer.github.com/v3/pulls/#list-pull-requests
- */
- get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
- val page = IssueSearchCondition.page(request)
- // TODO: more api spec condition
- val condition = IssueSearchCondition(request)
- val baseOwner = getAccountByUserName(repository.owner).get
- val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
- JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
- ApiPullRequest(
- issue,
- pullRequest,
- ApiRepository(headRepo, ApiUser(headOwner)),
- ApiRepository(repository, ApiUser(baseOwner)),
- ApiUser(issueUser)) })
- })
-
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
@@ -126,47 +107,6 @@
} getOrElse NotFound
})
- /**
- * https://developer.github.com/v3/pulls/#get-a-single-pull-request
- */
- get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
- (for{
- issueId <- params("id").toIntOpt
- (issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
- users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
- baseOwner <- users.get(repository.owner)
- headOwner <- users.get(pullRequest.requestUserName)
- issueUser <- users.get(issue.openedUserName)
- headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
- } yield {
- JsonFormat(ApiPullRequest(
- issue,
- pullRequest,
- ApiRepository(headRepo, ApiUser(headOwner)),
- ApiRepository(repository, ApiUser(baseOwner)),
- ApiUser(issueUser)))
- }).getOrElse(NotFound)
- })
-
- /**
- * https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
- */
- get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
- val owner = repository.owner
- val name = repository.name
- params("id").toIntOpt.flatMap{ issueId =>
- getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
- using(Git.open(getRepositoryDir(owner, name))){ git =>
- val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
- val newId = git.getRepository.resolve(pullreq.commitIdTo)
- val repoFullName = RepositoryName(repository)
- val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
- JsonFormat(commits)
- }
- }
- } getOrElse NotFound
- })
-
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
@@ -238,7 +178,7 @@
}
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 {
+ s"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) =>
@@ -523,7 +463,7 @@
getIssue(owner, name, issueId.toString) foreach { issue =>
// extract references and create refer comment
- createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
+ createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
@@ -535,19 +475,6 @@
}
})
- // TODO Same method exists in IssueController. Should it moved to IssueService?
- private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
- StringUtil.extractIssueId(message).foreach { issueId =>
- val content = fromIssue.issueId + ":" + fromIssue.title
- if(getIssue(owner, repository, issueId).isDefined){
- // Not add if refer comment already exist.
- if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
- createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
- }
- }
- }
- }
-
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
@@ -611,14 +538,4 @@
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")
- }
- }
- }
- }
}
diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala
index 7867334..d15c424 100644
--- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala
+++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala
@@ -142,22 +142,6 @@
}
})
- /** 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.
*/
diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
index 1007cf9..b1fa0f9 100644
--- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
+++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
@@ -2,7 +2,6 @@
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
-import gitbucket.core.api._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html
import gitbucket.core.helper
@@ -13,7 +12,7 @@
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.model.{Account, WebHook}
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
@@ -123,13 +122,6 @@
})
/**
- * https://developer.github.com/v3/repos/#get
- */
- get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
- JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
- })
-
- /**
* Displays the file list of the specified path and branch.
*/
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
@@ -160,65 +152,6 @@
}
})
- /**
- * https://developer.github.com/v3/repos/statuses/#create-a-status
- */
- post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
- (for{
- ref <- params.get("sha")
- sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
- data <- extractFromJsonBody[CreateAStatus] if data.isValid
- creator <- context.loginAccount
- state <- CommitState.valueOf(data.state)
- statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
- state, data.target_url, data.description, new java.util.Date(), creator)
- status <- getCommitStatus(repository.owner, repository.name, statusId)
- } yield {
- JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
- }) getOrElse NotFound
- })
-
- /**
- * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
- *
- * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
- */
- val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
- (for{
- ref <- params.get("ref")
- sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
- } yield {
- JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
- ApiCommitStatus(status, ApiUser(creator))
- })
- }) getOrElse NotFound
- })
-
- /**
- * https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
- *
- * legacy route
- */
- get("/api/v3/repos/:owner/:repo/statuses/:ref"){
- listStatusesRoute.action()
- }
-
- /**
- * https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
- *
- * ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
- */
- get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
- (for{
- ref <- params.get("ref")
- owner <- getAccountByUserName(repository.owner)
- sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
- } yield {
- val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
- JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
- }) getOrElse NotFound
- })
-
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)
@@ -564,6 +497,10 @@
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
+ context.loginAccount match {
+ case None => List()
+ case account: Option[Account] => getGroupsByUserName(account.get.userName)
+ }, // groups of current user
repository)
})
@@ -574,13 +511,7 @@
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val ref = multiParams("splat").head
JGitUtil.getTreeId(git, ref).map{ treeId =>
- html.find(ref,
- treeId,
- repository,
- context.loginAccount match {
- case None => List()
- case account: Option[Account] => getGroupsByUserName(account.get.userName)
- })
+ html.find(ref, treeId, repository)
} getOrElse NotFound
}
})
@@ -642,10 +573,6 @@
html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
- context.loginAccount match {
- case None => List()
- case account: Option[Account] => getGroupsByUserName(account.get.userName)
- }, // groups of current user
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
diff --git a/src/main/scala/gitbucket/core/controller/WikiController.scala b/src/main/scala/gitbucket/core/controller/WikiController.scala
index b81e75e..49fb2e2 100644
--- a/src/main/scala/gitbucket/core/controller/WikiController.scala
+++ b/src/main/scala/gitbucket/core/controller/WikiController.scala
@@ -12,7 +12,8 @@
import org.scalatra.i18n.Messages
class WikiController extends WikiControllerBase
- with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
+ with WikiService with RepositoryService with AccountService with ActivityService
+ with CollaboratorsAuthenticator with ReferrerAuthenticator
trait WikiControllerBase extends ControllerBase {
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
diff --git a/src/main/scala/gitbucket/core/plugin/Plugin.scala b/src/main/scala/gitbucket/core/plugin/Plugin.scala
index cf456ca..caaf726 100644
--- a/src/main/scala/gitbucket/core/plugin/Plugin.scala
+++ b/src/main/scala/gitbucket/core/plugin/Plugin.scala
@@ -1,16 +1,18 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
-import gitbucket.core.controller.ControllerBase
+import gitbucket.core.controller.{Context, ControllerBase}
+import gitbucket.core.model.Account
+import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
import io.github.gitbucket.solidbase.model.Version
/**
* Trait for define plugin interface.
- * To provide plugin, put Plugin class which mixed in this trait into the package root.
+ * To provide a plugin, put a Plugin class which extends this class into the package root.
*/
-trait Plugin {
+abstract class Plugin {
val pluginId: String
val pluginName: String
@@ -78,6 +80,76 @@
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
/**
+ * Override to add global menus.
+ */
+ val globalMenus: Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add global menus.
+ */
+ def globalMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add repository menus.
+ */
+ val repositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add repository menus.
+ */
+ def repositoryMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add repository setting tabs.
+ */
+ val repositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add repository setting tabs.
+ */
+ def repositorySettingTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add profile tabs.
+ */
+ val profileTabs: Seq[(Account, Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add profile tabs.
+ */
+ def profileTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Account, Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add system setting menus.
+ */
+ val systemSettingMenus: Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add system setting menus.
+ */
+ def systemSettingMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add account setting menus.
+ */
+ val accountSettingMenus: Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add account setting menus.
+ */
+ def accountSettingMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add dashboard tabs.
+ */
+ val dashboardTabs: Seq[(Context) => Option[Link]] = Nil
+
+ /**
+ * Override to add dashboard tabs.
+ */
+ def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
+
+ /**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
*/
@@ -100,6 +172,27 @@
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
registry.addReceiveHook(receiveHook)
}
+ (globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
+ registry.addGlobalMenu(globalMenu)
+ }
+ (repositoryMenus ++ repositoryMenus(registry, context, settings)).foreach { repositoryMenu =>
+ registry.addRepositoryMenu(repositoryMenu)
+ }
+ (repositorySettingTabs ++ repositorySettingTabs(registry, context, settings)).foreach { repositorySettingTab =>
+ registry.addRepositorySettingTab(repositorySettingTab)
+ }
+ (profileTabs ++ profileTabs(registry, context, settings)).foreach { profileTab =>
+ registry.addProfileTab(profileTab)
+ }
+ (systemSettingMenus ++ systemSettingMenus(registry, context, settings)).foreach { systemSettingMenu =>
+ registry.addSystemSettingMenu(systemSettingMenu)
+ }
+ (accountSettingMenus ++ accountSettingMenus(registry, context, settings)).foreach { accountSettingMenu =>
+ registry.addAccountSettingMenu(accountSettingMenu)
+ }
+ (dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
+ registry.addDashboardTab(dashboardTab)
+ }
}
/**
diff --git a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala
index 788ff97..6c5d478 100644
--- a/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala
+++ b/src/main/scala/gitbucket/core/plugin/PluginRegistory.scala
@@ -3,9 +3,9 @@
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import javax.servlet.ServletContext
-import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.controller.{Context, ControllerBase}
+import gitbucket.core.model.Account
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
@@ -34,6 +34,14 @@
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
+ private val globalMenus = new ListBuffer[(Context) => Option[Link]]
+ private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
+ private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
+ private val profileTabs = new ListBuffer[(Account, Context) => Option[Link]]
+ private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
+ private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
+ private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
+
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
}
@@ -108,17 +116,47 @@
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
- private case class GlobalAction(
- method: String,
- path: String,
- function: (HttpServletRequest, HttpServletResponse, Context) => Any
- )
+ def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = {
+ globalMenus += globalMenu
+ }
- private case class RepositoryAction(
- method: String,
- path: String,
- function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
- )
+ def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
+
+ def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = {
+ repositoryMenus += repositoryMenu
+ }
+
+ def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
+
+ def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = {
+ repositorySettingTabs += repositorySettingTab
+ }
+
+ def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
+
+ def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = {
+ profileTabs += profileTab
+ }
+
+ def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
+
+ def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = {
+ systemSettingMenus += systemSettingMenu
+ }
+
+ def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
+
+ def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = {
+ accountSettingMenus += accountSettingMenu
+ }
+
+ def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
+
+ def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = {
+ dashboardTabs += dashboardTab
+ }
+
+ def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
}
@@ -187,6 +225,8 @@
}
+case class Link(id: String, label: String, path: String, icon: Option[String] = None)
+
case class PluginInfo(
pluginId: String,
pluginName: String,
diff --git a/src/main/scala/gitbucket/core/service/HandleCommentService.scala b/src/main/scala/gitbucket/core/service/HandleCommentService.scala
new file mode 100644
index 0000000..6a1b068
--- /dev/null
+++ b/src/main/scala/gitbucket/core/service/HandleCommentService.scala
@@ -0,0 +1,91 @@
+package gitbucket.core.service
+
+import gitbucket.core.controller.Context
+import gitbucket.core.model.Issue
+import gitbucket.core.model.Profile._
+import gitbucket.core.util.ControlUtil._
+import gitbucket.core.util.Implicits._
+import gitbucket.core.util.Notifier
+import profile.simple._
+
+trait HandleCommentService {
+ self: RepositoryService with IssuesService with ActivityService
+ with WebHookService with WebHookIssueCommentService with WebHookPullRequestService =>
+
+ /**
+ * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
+ */
+ def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
+ (implicit context: Context, s: Session) = {
+
+ defining(repository.owner, repository.name){ case (owner, name) =>
+ val userName = context.loginAccount.get.userName
+
+ val (action, recordActivity) = actionOpt
+ .collect {
+ case "close" if(!issue.closed) => true ->
+ (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
+ case "reopen" if(issue.closed) => false ->
+ (Some("reopen") -> Some(recordReopenIssueActivity _))
+ }
+ .map { case (closed, t) =>
+ updateClosed(owner, name, issue.issueId, closed)
+ t
+ }
+ .getOrElse(None -> None)
+
+ val commentId = (content, action) match {
+ case (None, None) => None
+ case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
+ case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
+ }
+
+ // record comment activity if comment is entered
+ content foreach {
+ (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
+ (owner, name, userName, issue.issueId, _)
+ }
+ recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
+
+ // extract references and create refer comment
+ content.map { content =>
+ createReferComment(owner, name, issue, content, context.loginAccount.get)
+ }
+
+ // call web hooks
+ action match {
+ case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
+ case Some(act) => val webHookAction = act match {
+ case "open" => "opened"
+ case "reopen" => "reopened"
+ case "close" => "closed"
+ case _ => act
+ }
+ if(issue.isPullRequest){
+ callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
+ } else {
+ callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
+ }
+ }
+
+ // notifications
+ Notifier() match {
+ case f =>
+ content foreach {
+ f.toNotify(repository, issue, _){
+ Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
+ if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
+ }
+ }
+ action foreach {
+ f.toNotify(repository, issue, _){
+ Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
+ }
+ }
+ }
+
+ commentId.map( issue -> _ )
+ }
+ }
+
+}
diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala
index 82c8c58..86430a9 100644
--- a/src/main/scala/gitbucket/core/service/IssuesService.scala
+++ b/src/main/scala/gitbucket/core/service/IssuesService.scala
@@ -1,6 +1,8 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
+import gitbucket.core.util.JGitUtil.CommitInfo
+import gitbucket.core.util.StringUtil
import profile.simple._
import gitbucket.core.util.StringUtil._
@@ -12,6 +14,7 @@
trait IssuesService {
+ self: AccountService =>
import IssuesService._
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
@@ -394,6 +397,29 @@
}
}
}
+
+ def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String, loginAccount: Account)(implicit s: Session) = {
+ StringUtil.extractIssueId(message).foreach { issueId =>
+ val content = fromIssue.issueId + ":" + fromIssue.title
+ if(getIssue(owner, repository, issueId).isDefined){
+ // Not add if refer comment already exist.
+ if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
+ createComment(owner, repository, loginAccount.userName, issueId.toInt, content, "refer")
+ }
+ }
+ }
+ }
+
+ def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session) = {
+ 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")
+ }
+ }
+ }
+ }
+
}
object IssuesService {
diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala
new file mode 100644
index 0000000..90e0afd
--- /dev/null
+++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala
@@ -0,0 +1,79 @@
+package gitbucket.core.service
+
+import gitbucket.core.model.Profile._
+import gitbucket.core.util.ControlUtil._
+import gitbucket.core.util.Directory._
+import gitbucket.core.util.JGitUtil
+import gitbucket.core.model.Account
+import org.eclipse.jgit.api.Git
+import org.eclipse.jgit.dircache.DirCache
+import org.eclipse.jgit.lib.{FileMode, Constants}
+import profile.simple._
+
+trait RepositoryCreationService {
+ self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService =>
+
+ def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
+ (implicit s: Session) {
+ val ownerAccount = getAccountByUserName(owner).get
+ val loginUserName = loginAccount.userName
+
+ // Insert to the database at first
+ insertRepository(name, owner, description, isPrivate)
+
+ // Add collaborators for group repository
+ if(ownerAccount.isGroupAccount){
+ getGroupMembers(owner).foreach { member =>
+ addCollaborator(owner, name, member.userName)
+ }
+ }
+
+ // Insert default labels
+ insertDefaultLabels(owner, name)
+
+ // Create the actual repository
+ val gitdir = getRepositoryDir(owner, name)
+ JGitUtil.initRepository(gitdir)
+
+ if(createReadme){
+ using(Git.open(gitdir)){ git =>
+ val builder = DirCache.newInCore.builder()
+ val inserter = git.getRepository.newObjectInserter()
+ val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
+ val content = if(description.nonEmpty){
+ name + "\n" +
+ "===============\n" +
+ "\n" +
+ description.get
+ } else {
+ name + "\n" +
+ "===============\n"
+ }
+
+ builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
+ inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
+ builder.finish()
+
+ JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
+ Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
+ }
+ }
+
+ // Create Wiki repository
+ createWikiRepository(loginAccount, owner, name)
+
+ // Record activity
+ recordCreateRepositoryActivity(owner, name, loginUserName)
+ }
+
+ def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
+ createLabel(userName, repositoryName, "bug", "fc2929")
+ createLabel(userName, repositoryName, "duplicate", "cccccc")
+ createLabel(userName, repositoryName, "enhancement", "84b6eb")
+ createLabel(userName, repositoryName, "invalid", "e6e6e6")
+ createLabel(userName, repositoryName, "question", "cc317c")
+ createLabel(userName, repositoryName, "wontfix", "ffffff")
+ }
+
+
+}
diff --git a/src/main/scala/gitbucket/core/service/RepositorySearchService.scala b/src/main/scala/gitbucket/core/service/RepositorySearchService.scala
index 3d9bd8a..bba7172 100644
--- a/src/main/scala/gitbucket/core/service/RepositorySearchService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositorySearchService.scala
@@ -53,7 +53,30 @@
}
}
- private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
+ def countWikiPages(owner: String, repository: String, query: String): Int =
+ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
+ if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
+ }
+
+ def searchWikiPages(owner: String, repository: String, query: String): List[FileSearchResult] =
+ using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
+ if(JGitUtil.isEmpty(git)){
+ Nil
+ } else {
+ val files = searchRepositoryFiles(git, query)
+ val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
+ files.map { case (path, text) =>
+ val (highlightText, lineNumber) = getHighlightText(text, query)
+ FileSearchResult(
+ path.replaceFirst("\\.md$", ""),
+ commits(path).getCommitterIdent.getWhen,
+ highlightText,
+ lineNumber)
+ }
+ }
+ }
+
+ def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
val revWalk = new RevWalk(git.getRepository)
val objectId = git.getRepository.resolve("HEAD")
val revCommit = revWalk.parseCommit(objectId)
diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala
index 408c9b6..efc2e78 100644
--- a/src/main/scala/gitbucket/core/service/RepositoryService.scala
+++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala
@@ -19,7 +19,7 @@
* @param originRepositoryName specify for the forked repository. (default is None)
* @param originUserName specify for the forked repository. (default is None)
*/
- def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
+ def insertRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
(implicit s: Session): Unit = {
diff --git a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
index 0583f61..047e5ed 100644
--- a/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
+++ b/src/main/scala/gitbucket/core/servlet/GitRepositoryServlet.scala
@@ -10,7 +10,6 @@
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
-import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util._
import org.eclipse.jgit.api.Git
@@ -168,7 +167,7 @@
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) {
pushedIds.add(commit.id)
- createIssueComment(commit)
+ createIssueComment(owner, repository, commit)
// close issues
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
@@ -230,13 +229,4 @@
}
}
- private def createIssueComment(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")
- }
- }
- }
- }
}
diff --git a/src/main/twirl/gitbucket/core/account/application.scala.html b/src/main/twirl/gitbucket/core/account/application.scala.html
index d2d8f6e..fd23ca6 100644
--- a/src/main/twirl/gitbucket/core/account/application.scala.html
+++ b/src/main/twirl/gitbucket/core/account/application.scala.html
@@ -5,56 +5,51 @@
@import gitbucket.core.view.helpers._
@html.main("Applications"){
-
-
- @menu("application", settings.ssh)
-
-
-
-
Personal access tokens
-
- @if(personalTokens.isEmpty && gneratedToken.isEmpty){
- No tokens.
- } else {
- Tokens you have generated that can be used to access the GitBucket API.
-
- }
- @gneratedToken.map{ case (token, tokenString) =>
-
- Make sure to copy your new personal access token now. You won't be able to see it again!
-
+ @if(personalTokens.isEmpty && gneratedToken.isEmpty){
+ No tokens.
+ } else {
+ Tokens you have generated that can be used to access the GitBucket API.
+
+ }
+ @gneratedToken.map{ case (token, tokenString) =>
+
+ Make sure to copy your new personal access token now. You won't be able to see it again!
+