mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-01-25 08:49:16 +01:00
Merge branch 'master' into quill
# Conflicts: # build.sbt # src/main/scala/gitbucket/core/controller/AccountController.scala # src/main/scala/gitbucket/core/controller/UserManagementController.scala
This commit is contained in:
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,6 @@
|
||||
### Before submitting an issue to Gitbucket I have first:
|
||||
|
||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
|
||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||
- [] searched for similar already existing issue
|
||||
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
||||
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,6 +1,6 @@
|
||||
### Before submitting a pull-request to Gitbucket I have first:
|
||||
|
||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
|
||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||
- [] rebased my branch over master
|
||||
- [] verified that project is compiling
|
||||
- [] verified that tests are passing
|
||||
|
||||
@@ -60,6 +60,13 @@ Support
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 3.12 - 27 Feb 2016
|
||||
- New GitHub UI
|
||||
- Improve mobile view
|
||||
- Improve printing style
|
||||
- Individual URL for pull request tabs
|
||||
- SSH host configuration is separated from HTTP base URL
|
||||
|
||||
### 3.11 - 30 Jan 2016
|
||||
- Upgrade Scalatra to 2.4
|
||||
- Sidebar and Footer for Wiki
|
||||
|
||||
71
build.sbt
71
build.sbt
@@ -15,8 +15,7 @@ scalaVersion := "2.11.7"
|
||||
// dependency settings
|
||||
resolvers ++= Seq(
|
||||
Classpaths.typesafeReleases,
|
||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
||||
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/"
|
||||
)
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
|
||||
@@ -26,7 +25,7 @@ libraryDependencies ++= Seq(
|
||||
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
||||
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
||||
"commons-io" % "commons-io" % "2.4",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.6",
|
||||
"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",
|
||||
@@ -40,6 +39,7 @@ libraryDependencies ++= Seq(
|
||||
"com.typesafe" % "config" % "1.3.0",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
|
||||
"io.getquill" %% "quill-jdbc" % "0.4.1",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||
@@ -83,36 +83,36 @@ jrebelSettings
|
||||
// Create executable war file
|
||||
val executableConfig = config("executable").hide
|
||||
Keys.ivyConfigurations += executableConfig
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||
libraryDependencies ++= Seq(
|
||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||
)
|
||||
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
import org.apache.ivy.util.ChecksumHelper
|
||||
import java.util.jar.{ Manifest => JarManifest }
|
||||
import java.util.jar.Attributes.{ Name => AttrName }
|
||||
|
||||
val workDir = Keys.target.value / "executable"
|
||||
val warName = Keys.name.value + ".war"
|
||||
val workDir = Keys.target.value / "executable"
|
||||
val warName = Keys.name.value + ".war"
|
||||
|
||||
val log = streams.value.log
|
||||
val log = streams.value.log
|
||||
log info s"building executable webapp in ${workDir}"
|
||||
|
||||
// initialize temp directory
|
||||
val temp = workDir / "webapp"
|
||||
val temp = workDir / "webapp"
|
||||
IO delete temp
|
||||
|
||||
// include jetty classes
|
||||
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
||||
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
||||
jettyJars foreach { jar =>
|
||||
IO unzip (jar, temp, (name:String) =>
|
||||
(name startsWith "javax/") ||
|
||||
@@ -121,31 +121,34 @@ executableKey := {
|
||||
}
|
||||
|
||||
// include original war file
|
||||
val warFile = (Keys.`package`).value
|
||||
val warFile = (Keys.`package`).value
|
||||
IO unzip (warFile, temp)
|
||||
|
||||
// include launcher classes
|
||||
val classDir = (Keys.classDirectory in Compile).value
|
||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||
val classDir = (Keys.classDirectory in Compile).value
|
||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||
launchClasses foreach { name =>
|
||||
IO copyFile (classDir / name, temp / name)
|
||||
}
|
||||
|
||||
// zip it up
|
||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||
val manifest = new JarManifest
|
||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||
val outputFile = workDir / warName
|
||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||
val manifest = new JarManifest
|
||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||
val outputFile = workDir / warName
|
||||
IO jar (contentMappings, outputFile, manifest)
|
||||
|
||||
// generate checksums
|
||||
Seq("md5", "sha1") foreach { algorithm =>
|
||||
IO.write(
|
||||
workDir / (warName + "." + algorithm),
|
||||
ChecksumHelper computeAsString (outputFile, algorithm)
|
||||
)
|
||||
Seq(
|
||||
"md5" -> "MD5",
|
||||
"sha1" -> "SHA-1",
|
||||
"sha256" -> "SHA-256"
|
||||
)
|
||||
.foreach { case (extension, algorithm) =>
|
||||
val checksumFile = workDir / (warName + "." + extension)
|
||||
Checksums generate (outputFile, checksumFile, algorithm)
|
||||
}
|
||||
|
||||
// done
|
||||
@@ -154,7 +157,7 @@ executableKey := {
|
||||
}
|
||||
/*
|
||||
Keys.artifact in (Compile, executableKey) ~= {
|
||||
_ copy (`type` = "war", extension = "war"))
|
||||
_ copy (`type` = "war", extension = "war"))
|
||||
}
|
||||
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
||||
*/
|
||||
|
||||
@@ -32,3 +32,11 @@ $ sbt executable
|
||||
```
|
||||
|
||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
||||
|
||||
Run tests spec
|
||||
---------
|
||||
To run the full serie of tests, run the following command:
|
||||
|
||||
```
|
||||
sbt test
|
||||
```
|
||||
|
||||
@@ -6,15 +6,14 @@ Update version number
|
||||
|
||||
Note to update version number in files below:
|
||||
|
||||
### project/build.scala
|
||||
### build.sbt
|
||||
|
||||
```scala
|
||||
object MyBuild extends Build {
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val Version = "3.3.0" // <---- update version!!
|
||||
val ScalaVersion = "2.11.6"
|
||||
val ScalatraVersion = "2.3.1"
|
||||
val Organization = "gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "3.12.0" // <---- update version!!
|
||||
val ScalatraVersion = "2.4.0"
|
||||
val JettyVersion = "9.3.6.v20151106"
|
||||
```
|
||||
|
||||
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
||||
@@ -26,8 +25,8 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 3), // <---- add this line!!
|
||||
new Version(3, 2),
|
||||
new Version(3, 12), // <---- add this line!!
|
||||
new Version(3, 11),
|
||||
```
|
||||
|
||||
Generate release files
|
||||
|
||||
34
project/Checksums.scala
Normal file
34
project/Checksums.scala
Normal file
@@ -0,0 +1,34 @@
|
||||
import java.security.MessageDigest;
|
||||
import scala.annotation._
|
||||
import sbt._
|
||||
import sbt.Using._
|
||||
|
||||
object Checksums {
|
||||
private val bufferSize = 2048
|
||||
|
||||
def generate(source:File, target:File, algorithm:String):Unit =
|
||||
IO write (target, compute(source, algorithm))
|
||||
|
||||
def compute(file:File, algorithm:String):String =
|
||||
hex(raw(file, algorithm))
|
||||
|
||||
def raw(file:File, algorithm:String):Array[Byte] =
|
||||
(Using fileInputStream file) { is =>
|
||||
val md = MessageDigest getInstance algorithm
|
||||
val buf = new Array[Byte](bufferSize)
|
||||
md.reset()
|
||||
@tailrec
|
||||
def loop() {
|
||||
val len = is read buf
|
||||
if (len != -1) {
|
||||
md update (buf, 0, len)
|
||||
loop()
|
||||
}
|
||||
}
|
||||
loop()
|
||||
md.digest()
|
||||
}
|
||||
|
||||
def hex(bytes:Array[Byte]):String =
|
||||
bytes map { it => "%02x" format (it.toInt & 0xff) } mkString ""
|
||||
}
|
||||
1
src/main/resources/update/3_13.sql
Normal file
1
src/main/resources/update/3_13.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100);
|
||||
@@ -27,10 +27,10 @@ class ScalatraBootstrap extends LifeCycle {
|
||||
}
|
||||
|
||||
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 UserManagementController, "/*")
|
||||
context.mount(new SystemSettingsController, "/*")
|
||||
context.mount(new AccountController, "/*")
|
||||
context.mount(new RepositoryViewerController, "/*")
|
||||
context.mount(new WikiController, "/*")
|
||||
|
||||
@@ -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 gitbucket.core.util._
|
||||
|
||||
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
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 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
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.groupAccount){
|
||||
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
|
||||
|
||||
389
src/main/scala/gitbucket/core/controller/ApiController.scala
Normal file
389
src/main/scala/gitbucket/core/controller/ApiController.scala
Normal file
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -121,16 +121,6 @@ trait IndexControllerBase extends ControllerBase {
|
||||
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)}")
|
||||
|
||||
@@ -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 @@ import org.scalatra.Ok
|
||||
|
||||
|
||||
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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
|
||||
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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// 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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
// 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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
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 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -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
|
||||
@@ -32,26 +31,6 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
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 +45,6 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
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,50 +61,11 @@ trait LabelsControllerBase extends ControllerBase {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
} 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
|
||||
@@ -523,7 +463,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
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 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
// 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 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +49,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
)(CollaboratorForm.apply)
|
||||
|
||||
// for web hook url addition
|
||||
case class WebHookForm(url: String, events: Set[WebHook.Event])
|
||||
case class WebHookForm(url: String, events: Set[WebHook.Event], token: Option[String])
|
||||
|
||||
def webHookForm(update:Boolean) = mapping(
|
||||
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||
"events" -> webhookEvents
|
||||
"events" -> webhookEvents,
|
||||
"token" -> optional(trim(label("token", text(maxlength(100)))))
|
||||
)(WebHookForm.apply)
|
||||
|
||||
// for transfer ownership
|
||||
@@ -141,22 +142,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
@@ -198,7 +183,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook edit page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||
val webhook = WebHook(repository.owner, repository.name, "")
|
||||
val webhook = WebHook(repository.owner, repository.name, "", None)
|
||||
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
||||
})
|
||||
|
||||
@@ -206,7 +191,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Add the web hook URL.
|
||||
*/
|
||||
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
||||
addWebHook(repository.owner, repository.name, form.url, form.events)
|
||||
addWebHook(repository.owner, repository.name, form.url, form.events, form.token)
|
||||
flash += "info" -> s"Webhook ${form.url} created"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
@@ -235,7 +220,8 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
val url = params("url")
|
||||
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
|
||||
val token = Some(params("token"))
|
||||
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, token)
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||
@@ -294,7 +280,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Update web hook settings.
|
||||
*/
|
||||
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
|
||||
updateWebHook(repository.owner, repository.name, form.url, form.events)
|
||||
updateWebHook(repository.owner, repository.name, form.url, form.events, form.token)
|
||||
flash += "info" -> s"webhook ${form.url} updated"
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
||||
|
||||
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.StringUtil._
|
||||
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
|
||||
@@ -122,13 +121,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
fileList(_)
|
||||
})
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -160,65 +152,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.admin.html
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
||||
import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService}
|
||||
import gitbucket.core.util.AdminAuthenticator
|
||||
import gitbucket.core.ssh.SshServer
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import SystemSettingsService._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
class SystemSettingsController extends SystemSettingsControllerBase
|
||||
with AccountService with AdminAuthenticator
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
trait SystemSettingsControllerBase extends ControllerBase {
|
||||
self: AccountService with AdminAuthenticator =>
|
||||
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
private val form = mapping(
|
||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||
@@ -68,6 +74,61 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
|
||||
case class PluginForm(pluginIds: List[String])
|
||||
|
||||
|
||||
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
members: String)
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||
)(NewUserForm.apply)
|
||||
|
||||
val editUserForm = mapping(
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
||||
)(EditUserForm.apply)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members)))
|
||||
)(NewGroupForm.apply)
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
|
||||
get("/admin/system")(adminOnly {
|
||||
html.system(flash.get("info"))
|
||||
})
|
||||
@@ -92,4 +153,138 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
html.plugins(PluginRegistry().getPlugins())
|
||||
})
|
||||
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
val members = users.collect { case account if(account.groupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||
}.toMap
|
||||
|
||||
html.userlist(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
html.user(None)
|
||||
})
|
||||
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
html.user(getAccountByUserName(userName, true))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName, true).map { account =>
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove repositories
|
||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
// deleteRepository(userName, repositoryName)
|
||||
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
// }
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
}
|
||||
|
||||
updateAccount(account.copy(
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
fullName = form.fullName,
|
||||
mailAddress = form.mailAddress,
|
||||
administrator = form.isAdmin,
|
||||
url = form.url,
|
||||
removed = form.isRemoved))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/admin/users/_newgroup")(adminOnly {
|
||||
html.usergroup(None, Nil)
|
||||
})
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList)
|
||||
updateImage(form.groupName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
defining(params("groupName"), form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList){ case (groupName, members) =>
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, form.isRemoved)
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove from GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, Nil)
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
deleteRepository(groupName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||
}
|
||||
} else {
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, members)
|
||||
// Update COLLABORATOR for group repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
members.foreach { case (userName, isManager) =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
private def members: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
if(value.split(",").exists {
|
||||
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
||||
}) None else Some("Must select one manager at least.")
|
||||
}
|
||||
}
|
||||
|
||||
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
params.get(paramName).flatMap { userName =>
|
||||
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
||||
Some("You can't disable your account yourself")
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.service.{RepositoryService, AccountService}
|
||||
import gitbucket.core.admin.users.html
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.ControlUtil._
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import io.github.gitbucket.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.apache.commons.io.FileUtils
|
||||
|
||||
class UserManagementController extends UserManagementControllerBase
|
||||
with AccountService with RepositoryService with AdminAuthenticator
|
||||
|
||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||
|
||||
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean,
|
||||
url: Option[String], fileId: Option[String])
|
||||
|
||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
members: String)
|
||||
|
||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||
|
||||
val newUserForm = mapping(
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||
)(NewUserForm.apply)
|
||||
|
||||
val editUserForm = mapping(
|
||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
||||
)(EditUserForm.apply)
|
||||
|
||||
val newGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members)))
|
||||
)(NewGroupForm.apply)
|
||||
|
||||
val editGroupForm = mapping(
|
||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||
"members" -> trim(label("Members" ,text(required, members))),
|
||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||
"removed" -> trim(label("Disable" ,boolean()))
|
||||
)(EditGroupForm.apply)
|
||||
|
||||
get("/admin/users")(adminOnly {
|
||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||
val users = getAllUsers(includeRemoved)
|
||||
val members = users.collect { case account if(account.groupAccount) =>
|
||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||
}.toMap
|
||||
|
||||
html.list(users, members, includeRemoved)
|
||||
})
|
||||
|
||||
get("/admin/users/_newuser")(adminOnly {
|
||||
html.user(None)
|
||||
})
|
||||
|
||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||
updateImage(form.userName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||
val userName = params("userName")
|
||||
html.user(getAccountByUserName(userName, true))
|
||||
})
|
||||
|
||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName, true).map { account =>
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove repositories
|
||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||
// deleteRepository(userName, repositoryName)
|
||||
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||
// }
|
||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||
removeUserRelatedData(userName)
|
||||
}
|
||||
|
||||
updateAccount(account.copy(
|
||||
password = form.password.map(sha1).getOrElse(account.password),
|
||||
fullName = form.fullName,
|
||||
mailAddress = form.mailAddress,
|
||||
administrator = form.isAdmin,
|
||||
url = form.url,
|
||||
removed = form.isRemoved))
|
||||
|
||||
updateImage(userName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
})
|
||||
|
||||
get("/admin/users/_newgroup")(adminOnly {
|
||||
html.group(None, Nil)
|
||||
})
|
||||
|
||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||
createGroup(form.groupName, form.url)
|
||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList)
|
||||
updateImage(form.groupName, form.fileId, false)
|
||||
redirect("/admin/users")
|
||||
})
|
||||
|
||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||
defining(params("groupName")){ groupName =>
|
||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||
}
|
||||
})
|
||||
|
||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||
defining(params("groupName"), form.members.split(",").map {
|
||||
_.split(":") match {
|
||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||
}
|
||||
}.toList){ case (groupName, members) =>
|
||||
getAccountByUserName(groupName, true).map { account =>
|
||||
updateGroup(groupName, form.url, form.isRemoved)
|
||||
|
||||
if(form.isRemoved){
|
||||
// Remove from GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, Nil)
|
||||
// Remove repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
deleteRepository(groupName, repositoryName)
|
||||
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||
}
|
||||
} else {
|
||||
// Update GROUP_MEMBER
|
||||
updateGroupMembers(form.groupName, members)
|
||||
// Update COLLABORATOR for group repositories
|
||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||
removeCollaborators(form.groupName, repositoryName)
|
||||
members.foreach { case (userName, isManager) =>
|
||||
addCollaborator(form.groupName, repositoryName, userName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||
redirect("/admin/users")
|
||||
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
private def members: Constraint = new Constraint(){
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
if(value.split(",").exists {
|
||||
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
||||
}) None else Some("Must select one manager at least.")
|
||||
}
|
||||
}
|
||||
|
||||
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||
params.get(paramName).flatMap { userName =>
|
||||
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
||||
Some("You can't disable your account yourself")
|
||||
else
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||
|
||||
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||
val url = column[String]("URL")
|
||||
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
||||
val token = column[Option[String]]("TOKEN", O.Nullable)
|
||||
def * = (userName, repositoryName, url, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||
}
|
||||
@@ -16,7 +17,8 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||
case class WebHook(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
url: String
|
||||
url: String,
|
||||
token: Option[String]
|
||||
)
|
||||
|
||||
object WebHook {
|
||||
|
||||
@@ -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 -> _ )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 @@ import Q.interpolation
|
||||
|
||||
|
||||
trait IssuesService {
|
||||
self: AccountService =>
|
||||
import IssuesService._
|
||||
|
||||
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
||||
@@ -394,6 +397,29 @@ trait IssuesService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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.groupAccount){
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ trait RepositoryService { self: AccountService =>
|
||||
* @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 = {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
import fr.brouillard.oss.security.xhub.XHub
|
||||
import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter}
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
||||
import gitbucket.core.model.Profile._
|
||||
import org.apache.http.client.utils.URLEncodedUtils
|
||||
import profile.simple._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.RepositoryName
|
||||
@@ -33,8 +38,11 @@ trait WebHookService {
|
||||
|
||||
/** get All WebHook informations of repository event */
|
||||
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
|
||||
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind)
|
||||
.list.map(t => WebHook(t.userName, t.repositoryName, t.url))
|
||||
WebHooks.filter(_.byRepository(owner, repository))
|
||||
.innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
|
||||
.filter{ case (wh, whe) => whe.event === event.bind}
|
||||
.map{ case (wh, whe) => wh }
|
||||
.list.distinct
|
||||
|
||||
/** get All WebHook information from repository to url */
|
||||
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
|
||||
@@ -44,14 +52,15 @@ trait WebHookService {
|
||||
.map{ case (w,t) => w -> t.event }
|
||||
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
||||
|
||||
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
||||
WebHooks insert WebHook(owner, repository, url)
|
||||
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = {
|
||||
WebHooks insert WebHook(owner, repository, url, token)
|
||||
events.toSet.map{ event: WebHook.Event =>
|
||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||
}
|
||||
}
|
||||
|
||||
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
||||
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], token: Option[String])(implicit s: Session): Unit = {
|
||||
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => w.token).update(token)
|
||||
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
|
||||
events.toSet.map{ event: WebHook.Event =>
|
||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||
@@ -69,17 +78,17 @@ trait WebHookService {
|
||||
}
|
||||
}
|
||||
|
||||
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload)
|
||||
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
|
||||
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
|
||||
import org.apache.http.impl.client.HttpClientBuilder
|
||||
import ExecutionContext.Implicits.global
|
||||
import org.apache.http.protocol.HttpContext
|
||||
import org.apache.http.client.methods.HttpPost
|
||||
|
||||
if(webHookURLs.nonEmpty){
|
||||
if(webHooks.nonEmpty){
|
||||
val json = JsonFormat(payload)
|
||||
|
||||
webHookURLs.map { webHookUrl =>
|
||||
webHooks.map { webHook =>
|
||||
val reqPromise = Promise[HttpRequest]
|
||||
val f = Future {
|
||||
val itcp = new org.apache.http.HttpRequestInterceptor{
|
||||
@@ -89,19 +98,26 @@ trait WebHookService {
|
||||
}
|
||||
try{
|
||||
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
|
||||
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
|
||||
val httpPost = new HttpPost(webHookUrl.url)
|
||||
logger.debug(s"start web hook invocation for ${webHook.url}")
|
||||
val httpPost = new HttpPost(webHook.url)
|
||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpPost.addHeader("X-Github-Event", event.name)
|
||||
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
||||
|
||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||
params.add(new BasicNameValuePair("payload", json))
|
||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
||||
def postContent = new UrlEncodedFormEntity(params, "UTF-8")
|
||||
httpPost.setEntity(postContent)
|
||||
|
||||
if (!webHook.token.isEmpty) {
|
||||
// TODO find a better way and see how to extract content from postContent
|
||||
val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8")
|
||||
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, contentAsBytes))
|
||||
}
|
||||
|
||||
val res = httpClient.execute(httpPost)
|
||||
httpPost.releaseConnection()
|
||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
||||
logger.debug(s"end web hook invocation for ${webHook}")
|
||||
res
|
||||
}catch{
|
||||
case e:Throwable => {
|
||||
@@ -113,12 +129,12 @@ trait WebHookService {
|
||||
}
|
||||
}
|
||||
f.onSuccess {
|
||||
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
||||
case s => logger.debug(s"Success: web hook request to ${webHook.url}")
|
||||
}
|
||||
f.onFailure {
|
||||
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
||||
case t => logger.error(s"Failed: web hook request to ${webHook.url}", t)
|
||||
}
|
||||
(webHookUrl, json, reqPromise.future, f)
|
||||
(webHook, json, reqPromise.future, f)
|
||||
}
|
||||
} else {
|
||||
Nil
|
||||
|
||||
@@ -21,6 +21,7 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(3, 13),
|
||||
new Version(3, 12),
|
||||
new Version(3, 11),
|
||||
new Version(3, 10),
|
||||
|
||||
@@ -10,7 +10,6 @@ import gitbucket.core.service.WebHookService._
|
||||
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 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
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 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,6 @@ $(function(){
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust clickable area width
|
||||
$('#@textareaId').next('div.clickable').css({
|
||||
'width': ($('#@textareaId').width() + 18) + 'px',
|
||||
'font-size': '13px'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<span class="discussion-item-icon"><i class="octicon octicon-circle-slash"></i></span>
|
||||
@avatar(comment.commentedUserName, 16)
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
close @issueOrPullRequest()
|
||||
closed this @issueOrPullRequest()
|
||||
@helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +210,7 @@ $(function(){
|
||||
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
var url = '@url(repository)/commit_comments/_data/' + id;
|
||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.box'));
|
||||
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
|
||||
|
||||
$.get(url,
|
||||
{
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="panel panel-default issue-box">
|
||||
<div class="panel-body">
|
||||
<span id="error-title" class="error"></span>
|
||||
<input type="text" id="issue-title" name="title" class="form-control input-lg" value="" placeholder="Title" style="width: 680px;" autofocus/>
|
||||
<input type="text" id="issue-title" name="title" class="form-control input-lg" value="" placeholder="Title" autofocus/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
@@ -23,7 +23,7 @@
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 680px; height: 200px; max-height: 250px;",
|
||||
style = "height: 200px; max-height: 250px;",
|
||||
elastic = true
|
||||
)
|
||||
<div class="align-right">
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="panel panel-default issue-box">
|
||||
<div class="panel-body">
|
||||
<span class="error" id="error-title"></span>
|
||||
<input type="text" name="title" class="form-control input-lg" style="width: 680px" placeholder="Title"/>
|
||||
<input type="text" name="title" class="form-control input-lg" placeholder="Title"/>
|
||||
@helper.html.preview(
|
||||
repository = repository,
|
||||
content = "",
|
||||
@@ -68,7 +68,7 @@
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = true,
|
||||
style = "width: 680px; height: 200px;"
|
||||
style = "height: 200px;"
|
||||
)
|
||||
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
|
||||
<input type="hidden" name="targetBranch" value="@originId"/>
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
|
||||
</div>
|
||||
<span id="error-message" class="error"></span>
|
||||
<textarea name="message" style="width: 635px; height: 80px;">@issue.title</textarea>
|
||||
<textarea name="message" style="height: 80px;" class="form-control">@issue.title</textarea>
|
||||
<div>
|
||||
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/>
|
||||
<input type="submit" class="btn btn-success" value="Confirm merge"/>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
enableLineBreaks = true,
|
||||
enableTaskList = true,
|
||||
hasWritePermission = hasWritePermission,
|
||||
style = "width: 635px; height: 100px; max-height: 150px;",
|
||||
style = "height: 100px; max-height: 150px;",
|
||||
elastic = true
|
||||
)
|
||||
</div>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
@import context._
|
||||
<span class="error-edit-content-@commentId error"></span>
|
||||
@helper.html.attached(owner, repository){
|
||||
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
<textarea style="height: 100px;" id="edit-content-@commentId" class="form-control">@content</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" class="cancel-comment-@commentId btn btn-small btn-danger" value="Cancel"/>
|
||||
<input type="button" class="update-comment-@commentId btn btn-small pull-right" value="Update comment"/>
|
||||
<input type="button" class="update-comment-@commentId btn btn-small btn-default pull-right" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
@@ -19,7 +19,7 @@ $(function(){
|
||||
}
|
||||
|
||||
$(document).on('click', '.update-comment-@commentId', function(){
|
||||
$box = $(this).closest('.box');
|
||||
$box = $(this).closest('.commit-comment-box');
|
||||
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).attr('disabled', 'disabled');
|
||||
$.ajax({
|
||||
url: '@path/@owner/@repository/commit_comments/edit/@commentId',
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
</a>
|
||||
}
|
||||
} else {
|
||||
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
|
||||
<a href="@url(repository)/blob@{(branch :: pathList).map(encodeRefName).mkString("/", "/", "/")}@{encodeRefName(file.name)}">@file.name</a>
|
||||
}
|
||||
</td>
|
||||
<td class="mute">
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
}
|
||||
<button class="btn btn-default" id="test">Test Hook</button>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label class="strong">Security Token</label>
|
||||
<div></div>
|
||||
<input type="text" name="token" id="token" placeholder="leave blank for no X-Hub-Signature usage" value="@webHook.token" class="form-control" style="display: inline; width: 500px; vertical-align: middle;" />
|
||||
</fieldset>
|
||||
<hr />
|
||||
<label class="strong">Which events would you like to trigger this webhook?</label>
|
||||
<div>
|
||||
@@ -123,6 +128,7 @@ $(function(){
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
var url = this.form.url.value;
|
||||
var token = this.form.token.value;
|
||||
if(!/^https?:\/\/.+/.test(url)){
|
||||
alert("invalid url");
|
||||
return;
|
||||
@@ -132,7 +138,7 @@ $(function(){
|
||||
$("#test-report").hide();
|
||||
$.ajax({
|
||||
method:'POST',
|
||||
url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url),
|
||||
url:'@url(repository)/settings/hooks/test?url=' + encodeURIComponent(url) + '&token=' + encodeURIComponent(token),
|
||||
success: function(e){
|
||||
//console.log(e);
|
||||
$('#test-report-tab a:first').tab('show');
|
||||
|
||||
@@ -330,6 +330,9 @@ div.box-content {
|
||||
li.repo-link, li.page-link {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
div.box-content-bottom {
|
||||
@@ -1022,7 +1025,6 @@ div.commit-comment-box > div.panel-body {
|
||||
|
||||
|
||||
div.issue-comment-box textarea {
|
||||
width: 650px;
|
||||
height: 100px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class CommitStatusServiceSpec extends FunSuite with ServiceSpecBase with CommitS
|
||||
now = fixture1.registeredDate)
|
||||
test("createCommitState can insert and update") { withTestDB { implicit session =>
|
||||
val tester = generateNewAccount(fixture1.creator)
|
||||
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
|
||||
insertRepository(fixture1.repositoryName,fixture1.userName,None,false)
|
||||
val id = generateFixture1(tester:Account)
|
||||
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id) == Some(fixture1.copy(commitStatusId=id)))
|
||||
// other one can update
|
||||
@@ -60,14 +60,14 @@ class CommitStatusServiceSpec extends FunSuite with ServiceSpecBase with CommitS
|
||||
|
||||
test("getCommitStatus can find by commitId and context") { withTestDB { implicit session =>
|
||||
val tester = generateNewAccount(fixture1.creator)
|
||||
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
|
||||
insertRepository(fixture1.repositoryName,fixture1.userName,None,false)
|
||||
val id = generateFixture1(tester:Account)
|
||||
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, fixture1.commitId, fixture1.context) == Some(fixture1.copy(commitStatusId=id)))
|
||||
}}
|
||||
|
||||
test("getCommitStatus can find by commitStatusId") { withTestDB { implicit session =>
|
||||
val tester = generateNewAccount(fixture1.creator)
|
||||
createRepository(fixture1.repositoryName,fixture1.userName,None,false)
|
||||
insertRepository(fixture1.repositoryName,fixture1.userName,None,false)
|
||||
val id = generateFixture1(tester:Account)
|
||||
assert(getCommitStatus(fixture1.userName, fixture1.repositoryName, id) == Some(fixture1.copy(commitStatusId=id)))
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,7 @@ package gitbucket.core.service
|
||||
import gitbucket.core.model._
|
||||
import org.scalatest.FunSpec
|
||||
|
||||
class PullRequestServiceSpec extends FunSpec with ServiceSpecBase with PullRequestService with IssuesService {
|
||||
class PullRequestServiceSpec extends FunSpec with ServiceSpecBase with PullRequestService with IssuesService with AccountService {
|
||||
|
||||
def swap(r: (Issue, PullRequest)) = (r._2 -> r._1)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import org.scalatest.FunSuite
|
||||
class RepositoryServiceSpec extends FunSuite with ServiceSpecBase with RepositoryService with AccountService{
|
||||
test("renameRepository can rename CommitState, ProtectedBranches") { withTestDB { implicit session =>
|
||||
val tester = generateNewAccount("tester")
|
||||
createRepository("repo", "root", None, false)
|
||||
insertRepository("repo", "root", None, false)
|
||||
val service = new CommitStatusService with ProtectedBranchService {}
|
||||
val id = service.createCommitStatus(
|
||||
userName = "root",
|
||||
|
||||
@@ -42,7 +42,7 @@ trait ServiceSpecBase {
|
||||
|
||||
def generateNewUserWithDBRepository(userName:String, repositoryName:String)(implicit s:Session):Account = {
|
||||
val ac = AccountService.getAccountByUserName(userName).getOrElse(generateNewAccount(userName))
|
||||
dummyService.createRepository(repositoryName, userName, None, false)
|
||||
dummyService.insertRepository(repositoryName, userName, None, false)
|
||||
ac
|
||||
}
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
|
||||
val (issue3, pullreq3) = generateNewPullRequest("user3/repo3/master3", "user2/repo2/master2", loginUser="root")
|
||||
val (issue32, pullreq32) = generateNewPullRequest("user3/repo3/master32", "user2/repo2/master2", loginUser="root")
|
||||
generateNewPullRequest("user2/repo2/master2", "user1/repo1/master2")
|
||||
service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user1", "repo1", "webhook1-1", Set(WebHook.PullRequest), Some("key"))
|
||||
service.addWebHook("user1", "repo1", "webhook1-2", Set(WebHook.PullRequest), Some("key"))
|
||||
service.addWebHook("user2", "repo2", "webhook2-1", Set(WebHook.PullRequest), Some("key"))
|
||||
service.addWebHook("user2", "repo2", "webhook2-2", Set(WebHook.PullRequest), Some("key"))
|
||||
service.addWebHook("user3", "repo3", "webhook3-1", Set(WebHook.PullRequest), Some("key"))
|
||||
service.addWebHook("user3", "repo3", "webhook3-2", Set(WebHook.PullRequest), Some("key"))
|
||||
|
||||
assert(service.getPullRequestsByRequestForWebhook("user1","repo1","master1") == Map.empty)
|
||||
|
||||
@@ -43,33 +43,33 @@ class WebHookServiceSpec extends FunSuite with ServiceSpecBase {
|
||||
|
||||
test("add and get and update and delete") { withTestDB { implicit session =>
|
||||
val user1 = generateNewUserWithDBRepository("user1","repo1")
|
||||
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest))
|
||||
assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest))))
|
||||
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.PullRequest))))
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((WebHook("user1","repo1","http://example.com"))))
|
||||
service.addWebHook("user1", "repo1", "http://example.com", Set(WebHook.PullRequest), Some("key"))
|
||||
assert(service.getWebHooks("user1", "repo1") == List((WebHook("user1","repo1","http://example.com", Some("key")),Set(WebHook.PullRequest))))
|
||||
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", Some("key")),Set(WebHook.PullRequest))))
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List((WebHook("user1","repo1","http://example.com", Some("key")))))
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == Nil)
|
||||
assert(service.getWebHook("user1", "repo1", "http://example.com2") == None)
|
||||
assert(service.getWebHook("user2", "repo1", "http://example.com") == None)
|
||||
assert(service.getWebHook("user1", "repo2", "http://example.com") == None)
|
||||
service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues))
|
||||
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com"),Set(WebHook.Push, WebHook.Issues))))
|
||||
service.updateWebHook("user1", "repo1", "http://example.com", Set(WebHook.Push, WebHook.Issues), Some("key"))
|
||||
assert(service.getWebHook("user1", "repo1", "http://example.com") == Some((WebHook("user1","repo1","http://example.com", Some("key")),Set(WebHook.Push, WebHook.Issues))))
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == Nil)
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((WebHook("user1","repo1","http://example.com"))))
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.Push) == List((WebHook("user1","repo1","http://example.com", Some("key")))))
|
||||
service.deleteWebHook("user1", "repo1", "http://example.com")
|
||||
assert(service.getWebHook("user1", "repo1", "http://example.com") == None)
|
||||
} }
|
||||
|
||||
test("getWebHooks, getWebHooksByEvent") { withTestDB { implicit session =>
|
||||
val user1 = generateNewUserWithDBRepository("user1","repo1")
|
||||
service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/1", Set(WebHook.PullRequest), Some("key"))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/2", Set(WebHook.Push), Some("key"))
|
||||
service.addWebHook("user1", "repo1", "http://example.com/3", Set(WebHook.PullRequest,WebHook.Push), Some("key"))
|
||||
assert(service.getWebHooks("user1", "repo1") == List(
|
||||
WebHook("user1","repo1","http://example.com/1")->Set(WebHook.PullRequest),
|
||||
WebHook("user1","repo1","http://example.com/2")->Set(WebHook.Push),
|
||||
WebHook("user1","repo1","http://example.com/3")->Set(WebHook.PullRequest,WebHook.Push)))
|
||||
WebHook("user1","repo1","http://example.com/1", Some("key"))->Set(WebHook.PullRequest),
|
||||
WebHook("user1","repo1","http://example.com/2", Some("key"))->Set(WebHook.Push),
|
||||
WebHook("user1","repo1","http://example.com/3", Some("key"))->Set(WebHook.PullRequest,WebHook.Push)))
|
||||
assert(service.getWebHooksByEvent("user1", "repo1", WebHook.PullRequest) == List(
|
||||
WebHook("user1","repo1","http://example.com/1"),
|
||||
WebHook("user1","repo1","http://example.com/3")))
|
||||
WebHook("user1","repo1","http://example.com/1", Some("key")),
|
||||
WebHook("user1","repo1","http://example.com/3", Some("key"))))
|
||||
} }
|
||||
}
|
||||
Reference in New Issue
Block a user