diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..898e785 --- /dev/null +++ b/build.sbt @@ -0,0 +1,160 @@ +val Organization = "gitbucket" +val Name = "gitbucket" +val GitBucketVersion = "3.11.0-SNAPSHOT" +val ScalatraVersion = "2.4.0" +val JettyVersion = "9.3.6.v20151106" + +lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin) + +sourcesInBase := false +organization := Organization +name := Name +version := GitBucketVersion +scalaVersion := "2.11.6" + +// dependency settings +resolvers ++= Seq( + Classpaths.typesafeReleases, + "amateras-repo" at "http://amateras.sourceforge.jp/mvn/", + "amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/" +) +libraryDependencies ++= Seq( + "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r", + "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r", + "org.scalatra" %% "scalatra" % ScalatraVersion, + "org.scalatra" %% "scalatra-json" % ScalatraVersion, + "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" % "solidbase" % "1.0.0-SNAPSHOT", + "org.apache.commons" % "commons-compress" % "1.10", + "org.apache.commons" % "commons-email" % "1.4", + "org.apache.httpcomponents" % "httpclient" % "4.5.1", + "org.apache.sshd" % "apache-sshd" % "1.0.0", + "org.apache.tika" % "tika-core" % "1.11", + "com.typesafe.slick" %% "slick" % "2.1.0", + "com.novell.ldap" % "jldap" % "2009-10-07", + "com.h2database" % "h2" % "1.4.190", + "ch.qos.logback" % "logback-classic" % "1.1.1", + "com.mchange" % "c3p0" % "0.9.5.2", + "com.typesafe" % "config" % "1.3.0", + "com.typesafe.akka" %% "akka-actor" % "2.3.14", + "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", + "junit" % "junit" % "4.12" % "test", + "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", + "org.specs2" %% "specs2-junit" % "3.6.6" % "test" +) + +// Twirl settings +play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._" + +// Compiler settings +scalacOptions := Seq("-deprecation", "-language:postfixOps") +javacOptions in compile ++= Seq("-target", "7", "-source", "7") +javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" +testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console") +javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test" +testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ) +fork in Test := true +packageOptions += Package.MainClass("JettyLauncher") + +// Assembly settings +test in assembly := {} +assemblyMergeStrategy in assembly := { + case PathList("META-INF", xs @ _*) => + (xs map {_.toLowerCase}) match { + case ("manifest.mf" :: Nil) => MergeStrategy.discard + case _ => MergeStrategy.discard + } + case x => MergeStrategy.first +} + +// JRebel +jrebel.webLinks += (target in webappPrepare).value +jrebel.enabled := System.getenv().get("JREBEL") != null +javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path => + Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}") +} +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" +) + +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 log = streams.value.log + log info s"building executable webapp in ${workDir}" + + // initialize temp directory + val temp = workDir / "webapp" + IO delete temp + + // include jetty classes + val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name) + jettyJars foreach { jar => + IO unzip (jar, temp, (name:String) => + (name startsWith "javax/") || + (name startsWith "org/") + ) + } + + // include original war file + 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" */) + 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 + IO jar (contentMappings, outputFile, manifest) + + // generate checksums + Seq("md5", "sha1") foreach { algorithm => + IO.write( + workDir / (warName + "." + algorithm), + ChecksumHelper computeAsString (outputFile, algorithm) + ) + } + + // done + log info s"built executable webapp ${outputFile}" + outputFile +} +/* +Keys.artifact in (Compile, executableKey) ~= { + _ copy (`type` = "war", extension = "war")) +} +addArtifact(Keys.artifact in (Compile, executableKey), executableKey) +*/ diff --git a/doc/release.md b/doc/release.md index 7c97c39..da0c003 100644 --- a/doc/release.md +++ b/doc/release.md @@ -37,11 +37,10 @@ ### Make release war file -Run `release/make-release-war.sh`. The release war file is generated into `target/scala-2.11/gitbucket.war`. +Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`. ```bash -$ cd release -$ ./make-release-war.sh +$sbt executable ``` ### Deploy assembly jar file diff --git a/embed-jetty/javax.servlet-3.0.0.v201112011016.jar b/embed-jetty/javax.servlet-3.0.0.v201112011016.jar deleted file mode 100644 index b135409..0000000 --- a/embed-jetty/javax.servlet-3.0.0.v201112011016.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/javax.servlet-api-3.1.0.jar b/embed-jetty/javax.servlet-api-3.1.0.jar deleted file mode 100644 index 6b14c3d..0000000 --- a/embed-jetty/javax.servlet-api-3.1.0.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-continuation-9.3.6.v20151106.jar b/embed-jetty/jetty-continuation-9.3.6.v20151106.jar deleted file mode 100644 index 8f788c6..0000000 --- a/embed-jetty/jetty-continuation-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-http-9.3.6.v20151106.jar b/embed-jetty/jetty-http-9.3.6.v20151106.jar deleted file mode 100644 index 867ab9c..0000000 --- a/embed-jetty/jetty-http-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-io-9.3.6.v20151106.jar b/embed-jetty/jetty-io-9.3.6.v20151106.jar deleted file mode 100644 index 50de7ef..0000000 --- a/embed-jetty/jetty-io-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-security-9.3.6.v20151106.jar b/embed-jetty/jetty-security-9.3.6.v20151106.jar deleted file mode 100644 index 992fc05..0000000 --- a/embed-jetty/jetty-security-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-server-9.3.6.v20151106.jar b/embed-jetty/jetty-server-9.3.6.v20151106.jar deleted file mode 100644 index 6311e29..0000000 --- a/embed-jetty/jetty-server-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-servlet-9.3.6.v20151106.jar b/embed-jetty/jetty-servlet-9.3.6.v20151106.jar deleted file mode 100644 index 9409f61..0000000 --- a/embed-jetty/jetty-servlet-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-util-9.3.6.v20151106.jar b/embed-jetty/jetty-util-9.3.6.v20151106.jar deleted file mode 100644 index 9a686b6..0000000 --- a/embed-jetty/jetty-util-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-webapp-9.3.6.v20151106.jar b/embed-jetty/jetty-webapp-9.3.6.v20151106.jar deleted file mode 100644 index 85d2ac6..0000000 --- a/embed-jetty/jetty-webapp-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/jetty-xml-9.3.6.v20151106.jar b/embed-jetty/jetty-xml-9.3.6.v20151106.jar deleted file mode 100644 index 15941e4..0000000 --- a/embed-jetty/jetty-xml-9.3.6.v20151106.jar +++ /dev/null Binary files differ diff --git a/embed-jetty/update.sh b/embed-jetty/update.sh deleted file mode 100755 index 7d1cfd7..0000000 --- a/embed-jetty/update.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -version=$1 -output_dir=`dirname $0` -git rm -f ${output_dir}/jetty-*.jar -for name in 'io' 'servlet' 'xml' 'continuation' 'security' 'util' 'http' 'server' 'webapp' -do - jar_filename="jetty-${name}-${version}.jar" - wget "http://repo1.maven.org/maven2/org/eclipse/jetty/jetty-${name}/${version}/${jar_filename}" -O ${output_dir}/${jar_filename} -done -git add ${output_dir}/*.jar -git commit diff --git a/project/build.scala b/project/build.scala deleted file mode 100644 index 493c620..0000000 --- a/project/build.scala +++ /dev/null @@ -1,89 +0,0 @@ -import com.earldouglas.xwp.JettyPlugin -import play.twirl.sbt.SbtTwirl -import sbt.Keys._ -import sbt._ -import sbtassembly.AssemblyKeys._ -import sbtassembly._ -import JettyPlugin.autoImport._ -import fi.gekkio.sbtplugins.jrebel.JRebelPlugin._ -import com.earldouglas.xwp.WebappPlugin.autoImport.webappPrepare - -object MyBuild extends Build { - val Organization = "gitbucket" - val Name = "gitbucket" - val Version = "3.11.0-SNAPSHOT" - val ScalaVersion = "2.11.6" - val ScalatraVersion = "2.4.0" - - lazy val project = Project ( - "gitbucket", - file(".") - ) -// .settings(ScalatraPlugin.scalatraWithJRebel: _*) - .settings( - test in assembly := {}, - assemblyMergeStrategy in assembly := { - case PathList("META-INF", xs @ _*) => - (xs map {_.toLowerCase}) match { - case ("manifest.mf" :: Nil) => MergeStrategy.discard - case _ => MergeStrategy.discard - } - case x => MergeStrategy.first - } - ) - .settings( - sourcesInBase := false, - organization := Organization, - name := Name, - version := Version, - scalaVersion := ScalaVersion, - resolvers ++= Seq( - Classpaths.typesafeReleases, - "amateras-repo" at "http://amateras.sourceforge.jp/mvn/", - "amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/" - ), - scalacOptions := Seq("-deprecation", "-language:postfixOps"), - libraryDependencies ++= Seq( - "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r", - "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r", - "org.scalatra" %% "scalatra" % ScalatraVersion, - "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", - "org.scalatra" %% "scalatra-json" % ScalatraVersion, - "org.specs2" %% "specs2-junit" % "3.6.6" % "test", - "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-SNAPSHOT", - "io.github.gitbucket" % "solidbase" % "1.0.0-SNAPSHOT", - "org.apache.commons" % "commons-compress" % "1.10", - "org.apache.commons" % "commons-email" % "1.4", - "org.apache.httpcomponents" % "httpclient" % "4.5.1", - "org.apache.sshd" % "apache-sshd" % "1.0.0", - "org.apache.tika" % "tika-core" % "1.11", - "com.typesafe.slick" %% "slick" % "2.1.0", - "com.novell.ldap" % "jldap" % "2009-10-07", - "com.h2database" % "h2" % "1.4.190", - "ch.qos.logback" % "logback-classic" % "1.1.1", - "org.eclipse.jetty" % "jetty-webapp" % "9.3.6.v20151106" % "provided", - "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", - "junit" % "junit" % "4.12" % "test", - "com.mchange" % "c3p0" % "0.9.5.2", - "com.typesafe" % "config" % "1.3.0", - "com.typesafe.akka" %% "akka-actor" % "2.3.14", - "com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0") - ), - play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._", - javacOptions in compile ++= Seq("-target", "7", "-source", "7"), - javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml", - testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), - javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test", - testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ), - fork in Test := true, - packageOptions += Package.MainClass("JettyLauncher") - ).settings(jrebelSettings: _*).settings( - jrebel.webLinks += (target in webappPrepare).value, - jrebel.enabled := System.getenv().get("JREBEL") != null, - javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap(path => - Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")) - ).enablePlugins(SbtTwirl, JettyPlugin) -} diff --git a/release/build.xml b/release/build.xml deleted file mode 100644 index 8cce345..0000000 --- a/release/build.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/release/make-release-war.sh b/release/make-release-war.sh deleted file mode 100755 index 5a803e0..0000000 --- a/release/make-release-war.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -D="$(dirname "$0")" -D="$(cd "${D}"; pwd)" -DD="$(dirname "${D}")" -( - for f in "${D}/env.sh" "${D}/build.xml"; do - if [ ! -s "${f}" ]; then - echo >&2 "$0: Unable to access file '${f}'" - exit 1 - fi - done - . "${D}/env.sh" - cd "${DD}" - ant -f "${D}/build.xml" all -) diff --git a/src/main/scala/gitbucket/core/api/ApiLabel.scala b/src/main/scala/gitbucket/core/api/ApiLabel.scala new file mode 100644 index 0000000..2d1842b --- /dev/null +++ b/src/main/scala/gitbucket/core/api/ApiLabel.scala @@ -0,0 +1,21 @@ +package gitbucket.core.api + +import gitbucket.core.model.Label +import gitbucket.core.util.RepositoryName + +/** + * https://developer.github.com/v3/issues/labels/ + */ +case class ApiLabel( + name: String, + color: String)(repositoryName: RepositoryName){ + var url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/labels/${name}") +} + +object ApiLabel{ + def apply(label:Label, repositoryName: RepositoryName): ApiLabel = + ApiLabel( + name = label.labelName, + color = label.color + )(repositoryName) +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/api/CreateALabel.scala b/src/main/scala/gitbucket/core/api/CreateALabel.scala new file mode 100644 index 0000000..cfd71d1 --- /dev/null +++ b/src/main/scala/gitbucket/core/api/CreateALabel.scala @@ -0,0 +1,18 @@ +package gitbucket.core.api + +/** + * https://developer.github.com/v3/issues/labels/#create-a-label + * api form + */ +case class CreateALabel( + name: String, + color: String +) { + def isValid: Boolean = { + name.length<=100 && + !name.startsWith("_") && + !name.startsWith("-") && + color.length==6 && + color.matches("[a-fA-F0-9+_.]+") + } +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/core/api/JsonFormat.scala b/src/main/scala/gitbucket/core/api/JsonFormat.scala index 0733ba4..611db3b 100644 --- a/src/main/scala/gitbucket/core/api/JsonFormat.scala +++ b/src/main/scala/gitbucket/core/api/JsonFormat.scala @@ -31,6 +31,7 @@ FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]() + + FieldSerializer[ApiLabel]() + ApiBranchProtection.enforcementLevelSerializer def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format => diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index 29d8400..4074c61 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -1,12 +1,13 @@ 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.{ReferrerAuthenticator, CollaboratorsAuthenticator} +import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator} import gitbucket.core.util.Implicits._ import io.github.gitbucket.scalatra.forms._ import org.scalatra.i18n.Messages -import org.scalatra.Ok +import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok} class LabelsController extends LabelsControllerBase with LabelsService with IssuesService with RepositoryService with AccountService @@ -19,7 +20,7 @@ case class LabelForm(labelName: String, color: String) val labelForm = mapping( - "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), + "labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))), "labelColor" -> trim(label("Color", text(required, color))) )(LabelForm.apply) @@ -31,6 +32,26 @@ 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) }) @@ -45,6 +66,31 @@ 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) @@ -61,12 +107,51 @@ hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) + /** + * Update a label + * https://developer.github.com/v3/issues/labels/#update-a-label + */ + patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + (for{ + data <- extractFromJsonBody[CreateALabel] if data.isValid + } yield { + LockUtil.lock(RepositoryName(repository).fullName) { + getLabel(repository.owner, repository.name, params("labelName")).map { label => + if (getLabel(repository.owner, repository.name, data.name).isEmpty) { + updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color) + JsonFormat(ApiLabel( + getLabel(repository.owner, repository.name, label.labelId).get, + RepositoryName(repository))) + } else { + // TODO ApiError should support errors field to enhance compatibility of GitHub API + UnprocessableEntity(ApiError( + "Validation Failed", + Some("https://developer.github.com/v3/issues/labels/#create-a-label"))) + } + } getOrElse NotFound() + } + }) getOrElse NotFound() + }) + ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => deleteLabel(repository.owner, repository.name, params("labelId").toInt) Ok() }) /** + * Delete a label + * https://developer.github.com/v3/issues/labels/#delete-a-label + */ + delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository => + LockUtil.lock(RepositoryName(repository).fullName) { + getLabel(repository.owner, repository.name, params("labelName")).map { label => + deleteLabel(repository.owner, repository.name, label.labelId) + NoContent() + } getOrElse NotFound() + } + }) + + /** * Constraint for the identifier such as user name, repository name or page name. */ private def labelName: Constraint = new Constraint(){ @@ -80,4 +165,12 @@ } } + private def uniqueLabelName: Constraint = new Constraint(){ + override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = { + val owner = params("owner") + val repository = params("repository") + getLabel(owner, repository, value).map(_ => "Name has already been taken.") + } + } + } diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala index 2b13764..97fcb75 100644 --- a/src/main/scala/gitbucket/core/model/BasicTemplate.scala +++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala @@ -26,12 +26,16 @@ trait LabelTemplate extends BasicTemplate { self: Table[_] => val labelId = column[Int]("LABEL_ID") + val labelName = column[String]("LABEL_NAME") def byLabel(owner: String, repository: String, labelId: Int) = byRepository(owner, repository) && (this.labelId === labelId.bind) def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byRepository(userName, repositoryName) && (this.labelId === labelId) + + def byLabel(owner: String, repository: String, labelName: String) = + byRepository(userName, repositoryName) && (this.labelName === labelName.bind) } trait MilestoneTemplate extends BasicTemplate { self: Table[_] => diff --git a/src/main/scala/gitbucket/core/model/Labels.scala b/src/main/scala/gitbucket/core/model/Labels.scala index 0143c9e..84a4e6d 100644 --- a/src/main/scala/gitbucket/core/model/Labels.scala +++ b/src/main/scala/gitbucket/core/model/Labels.scala @@ -7,7 +7,7 @@ class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate { override val labelId = column[Int]("LABEL_ID", O AutoInc) - val labelName = column[String]("LABEL_NAME") + override val labelName = column[String]("LABEL_NAME") val color = column[String]("COLOR") def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply) diff --git a/src/main/scala/gitbucket/core/service/LabelsService.scala b/src/main/scala/gitbucket/core/service/LabelsService.scala index 35b5d2d..f8026e0 100644 --- a/src/main/scala/gitbucket/core/service/LabelsService.scala +++ b/src/main/scala/gitbucket/core/service/LabelsService.scala @@ -12,6 +12,9 @@ def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption + def getLabel(owner: String, repository: String, labelName: String)(implicit s: Session): Option[Label] = + Labels.filter(_.byLabel(owner, repository, labelName)).firstOption + def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int = Labels returning Labels.map(_.labelId) += Label( userName = owner, diff --git a/src/main/scala/gitbucket/core/ssh/GitCommand.scala b/src/main/scala/gitbucket/core/ssh/GitCommand.scala index 43502dd..8e19255 100644 --- a/src/main/scala/gitbucket/core/ssh/GitCommand.scala +++ b/src/main/scala/gitbucket/core/ssh/GitCommand.scala @@ -23,10 +23,10 @@ abstract class GitCommand() extends Command { private val logger = LoggerFactory.getLogger(classOf[GitCommand]) - protected var err: OutputStream = null - protected var in: InputStream = null - protected var out: OutputStream = null - protected var callback: ExitCallback = null + @volatile protected var err: OutputStream = null + @volatile protected var in: InputStream = null + @volatile protected var out: OutputStream = null + @volatile protected var callback: ExitCallback = null protected def runTask(user: String)(implicit session: Session): Unit diff --git a/src/main/scala/gitbucket/core/util/RepoitoryName.scala b/src/main/scala/gitbucket/core/util/RepoitoryName.scala deleted file mode 100644 index 415b20b..0000000 --- a/src/main/scala/gitbucket/core/util/RepoitoryName.scala +++ /dev/null @@ -1,18 +0,0 @@ -package gitbucket.core.util - -case class RepositoryName(owner:String, name:String){ - val fullName = s"${owner}/${name}" -} - -object RepositoryName{ - def apply(fullName: String): RepositoryName = { - fullName.split("/").toList match { - case owner :: name :: Nil => RepositoryName(owner, name) - case _ => throw new IllegalArgumentException(s"${fullName} is not repositoryName (only 'owner/name')") - } - } - def apply(repository: gitbucket.core.model.Repository): RepositoryName = RepositoryName(repository.userName, repository.repositoryName) - def apply(repository: gitbucket.core.util.JGitUtil.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name) - def apply(repository: gitbucket.core.service.RepositoryService.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name) - def apply(repository: gitbucket.core.model.CommitStatus): RepositoryName = RepositoryName(repository.userName, repository.repositoryName) -} diff --git a/src/main/scala/gitbucket/core/util/RepositoryName.scala b/src/main/scala/gitbucket/core/util/RepositoryName.scala new file mode 100644 index 0000000..415b20b --- /dev/null +++ b/src/main/scala/gitbucket/core/util/RepositoryName.scala @@ -0,0 +1,18 @@ +package gitbucket.core.util + +case class RepositoryName(owner:String, name:String){ + val fullName = s"${owner}/${name}" +} + +object RepositoryName{ + def apply(fullName: String): RepositoryName = { + fullName.split("/").toList match { + case owner :: name :: Nil => RepositoryName(owner, name) + case _ => throw new IllegalArgumentException(s"${fullName} is not repositoryName (only 'owner/name')") + } + } + def apply(repository: gitbucket.core.model.Repository): RepositoryName = RepositoryName(repository.userName, repository.repositoryName) + def apply(repository: gitbucket.core.util.JGitUtil.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name) + def apply(repository: gitbucket.core.service.RepositoryService.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name) + def apply(repository: gitbucket.core.model.CommitStatus): RepositoryName = RepositoryName(repository.userName, repository.repositoryName) +} diff --git a/src/main/scala/gitbucket/core/view/Markdown.scala b/src/main/scala/gitbucket/core/view/Markdown.scala index 5acaa1d..2d57d1c 100644 --- a/src/main/scala/gitbucket/core/view/Markdown.scala +++ b/src/main/scala/gitbucket/core/view/Markdown.scala @@ -68,7 +68,7 @@ if(enableAnchor){ out.append(" class=\"markdown-head\">") - out.append("") + out.append("") out.append("") } else { out.append(">") diff --git a/src/main/twirl/gitbucket/core/main.scala.html b/src/main/twirl/gitbucket/core/main.scala.html index cb79802..067eedc 100644 --- a/src/main/twirl/gitbucket/core/main.scala.html +++ b/src/main/twirl/gitbucket/core/main.scala.html @@ -75,8 +75,7 @@
- - +
- @avatar(loginAccount.get.userName, 16) - + + @avatar(loginAccount.get.userName, 16)