diff --git a/README.md b/README.md index d794db7..f8b8ec0 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,39 @@ Release Notes -------- +### 2.5 - 4 Nov 2014 +- New Dashboard +- Change datetime format +- Create branch from Web UI +- Task list in Markdown +- Some bug fix and improvements + +### 2.4.1 - 6 Oct 2014 +- Bug fix + +### 2.4 - 6 Oct 2014 +- New UI is applied to Issues and Pull requests +- Side-by-side diff is available +- Fix relative path problem in Markdown links and images +- Plugin System is disabled in default +- Some bug fix and improvements + +### 2.3 - 1 Sep 2014 +- Scala based plugin system +- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp` +- Some bug fix and improvements + +### 2.2.1 - 5 Aug 2014 +- Bug fix + +### 2.2 - 4 Aug 2014 +- Plug-in system is available +- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1 +- tar.gz export for repository contents +- LDAP authentication improvement (mail address became optional) +- Show news feed of a private repository to members +- Some bug fix and improvements + ### 2.1 - 6 Jul 2014 - Upgrade to Slick 2.0 from 1.9 - Base part of the plug-in system is merged diff --git a/build.xml b/build.xml index 1af360d..b0416d8 100644 --- a/build.xml +++ b/build.xml @@ -4,7 +4,7 @@ - + diff --git a/etc/icons.svg b/etc/icons.svg index a1849e3..9372a97 100644 --- a/etc/icons.svg +++ b/etc/icons.svg @@ -1,1703 +1,754 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project/build.properties b/project/build.properties index 37b489c..be6c454 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.1 +sbt.version=0.13.5 diff --git a/project/build.scala b/project/build.scala index f776c9f..dbdff71 100644 --- a/project/build.scala +++ b/project/build.scala @@ -1,57 +1,60 @@ import sbt._ import Keys._ import org.scalatra.sbt._ -import twirl.sbt.TwirlPlugin._ import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys +import play.twirl.sbt.SbtTwirl +import play.twirl.sbt.Import.TwirlKeys._ object MyBuild extends Build { val Organization = "jp.sf.amateras" val Name = "gitbucket" val Version = "0.0.1" - val ScalaVersion = "2.10.3" - val ScalatraVersion = "2.2.1" + val ScalaVersion = "2.11.2" + val ScalatraVersion = "2.3.0" lazy val project = Project ( "gitbucket", - file("."), - settings = Defaults.defaultSettings ++ ScalatraPlugin.scalatraWithJRebel ++ Seq( - sourcesInBase := false, - organization := Organization, - name := Name, - version := Version, - scalaVersion := ScalaVersion, - resolvers ++= Seq( - Classpaths.typesafeReleases, - "amateras-repo" at "http://amateras.sourceforge.jp/mvn/" - ), - scalacOptions := Seq("-deprecation", "-language:postfixOps"), - libraryDependencies ++= Seq( - "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.0.0.201306101825-r", - "org.scalatra" %% "scalatra" % ScalatraVersion, - "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", - "org.scalatra" %% "scalatra-json" % ScalatraVersion, - "org.json4s" %% "json4s-jackson" % "3.2.5", - "jp.sf.amateras" %% "scalatra-forms" % "0.0.14", - "commons-io" % "commons-io" % "2.4", - "org.pegdown" % "pegdown" % "1.4.1", - "org.apache.commons" % "commons-compress" % "1.5", - "org.apache.commons" % "commons-email" % "1.3.1", - "org.apache.httpcomponents" % "httpclient" % "4.3", - "org.apache.sshd" % "apache-sshd" % "0.11.0", - "com.typesafe.slick" %% "slick" % "2.0.2", - "org.mozilla" % "rhino" % "1.7R4", - "com.novell.ldap" % "jldap" % "2009-10-07", - "org.quartz-scheduler" % "quartz" % "2.2.1", - "com.h2database" % "h2" % "1.4.180", - "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", - "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", - "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), - "junit" % "junit" % "4.11" % "test" - ), - EclipseKeys.withSource := true, - javacOptions in compile ++= Seq("-target", "6", "-source", "6"), - testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), - packageOptions += Package.MainClass("JettyLauncher") - ) ++ seq(Twirl.settings: _*) + file(".") ) + .settings(ScalatraPlugin.scalatraWithJRebel: _*) + .settings( + sourcesInBase := false, + organization := Organization, + name := Name, + version := Version, + scalaVersion := ScalaVersion, + resolvers ++= Seq( + Classpaths.typesafeReleases, + "amateras-repo" at "http://amateras.sourceforge.jp/mvn/" + ), + scalacOptions := Seq("-deprecation", "-language:postfixOps"), + libraryDependencies ++= Seq( + "org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r", + "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r", + "org.scalatra" %% "scalatra" % ScalatraVersion, + "org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test", + "org.scalatra" %% "scalatra-json" % ScalatraVersion, + "org.json4s" %% "json4s-jackson" % "3.2.10", + "jp.sf.amateras" %% "scalatra-forms" % "0.1.0", + "commons-io" % "commons-io" % "2.4", + "org.pegdown" % "pegdown" % "1.4.1", + "org.apache.commons" % "commons-compress" % "1.5", + "org.apache.commons" % "commons-email" % "1.3.1", + "org.apache.httpcomponents" % "httpclient" % "4.3", + "org.apache.sshd" % "apache-sshd" % "0.11.0", + "com.typesafe.slick" %% "slick" % "2.1.0", + "com.novell.ldap" % "jldap" % "2009-10-07", + "org.quartz-scheduler" % "quartz" % "2.2.1", + "com.h2database" % "h2" % "1.4.180", + "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime", + "org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided", + "org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"), + "junit" % "junit" % "4.11" % "test", + "com.typesafe.play" %% "twirl-compiler" % "1.0.2" + ), + EclipseKeys.withSource := true, + javacOptions in compile ++= Seq("-target", "7", "-source", "7"), + testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"), + packageOptions += Package.MainClass("JettyLauncher") + ).enablePlugins(SbtTwirl) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 7d7ab3e..de95ab6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,8 +4,6 @@ addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5") -resolvers += "spray repo" at "http://repo.spray.io" - -addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4") diff --git a/sbt-launch-0.13.1.jar b/sbt-launch-0.13.1.jar deleted file mode 100644 index 5c7d052..0000000 --- a/sbt-launch-0.13.1.jar +++ /dev/null Binary files differ diff --git a/sbt-launch-0.13.5.jar b/sbt-launch-0.13.5.jar new file mode 100644 index 0000000..174a7e1 --- /dev/null +++ b/sbt-launch-0.13.5.jar Binary files differ diff --git a/sbt.bat b/sbt.bat index cd356dd..6c83e1a 100644 --- a/sbt.bat +++ b/sbt.bat @@ -1,2 +1,2 @@ set SCRIPT_DIR=%~dp0 -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.1.jar" %* +java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.5.jar" %* diff --git a/sbt.sh b/sbt.sh index 86cf93e..0ffa9fe 100755 --- a/sbt.sh +++ b/sbt.sh @@ -1 +1,2 @@ -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.1.jar "$@" +#!/bin/sh +java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.5.jar "$@" diff --git a/src/main/java/JettyLauncher.java b/src/main/java/JettyLauncher.java index 76500f0..6300547 100644 --- a/src/main/java/JettyLauncher.java +++ b/src/main/java/JettyLauncher.java @@ -1,10 +1,8 @@ -import org.eclipse.jetty.io.EndPoint; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.nio.SelectChannelConnector; import org.eclipse.jetty.webapp.WebAppContext; -import java.io.IOException; +import java.io.File; import java.net.URL; import java.security.ProtectionDomain; @@ -44,6 +42,14 @@ server.addConnector(connector); WebAppContext context = new WebAppContext(); + + File tmpDir = new File(getGitBucketHome(), "tmp"); + if(tmpDir.exists()){ + deleteDirectory(tmpDir); + } + tmpDir.mkdirs(); + context.setTempDirectory(tmpDir); + ProtectionDomain domain = JettyLauncher.class.getProtectionDomain(); URL location = domain.getCodeSource().getLocation(); @@ -59,4 +65,27 @@ server.start(); server.join(); } + + private static File getGitBucketHome(){ + String home = System.getProperty("gitbucket.home"); + if(home != null && home.length() > 0){ + return new File(home); + } + home = System.getenv("GITBUCKET_HOME"); + if(home != null && home.length() > 0){ + return new File(home); + } + return new File(System.getProperty("user.home"), ".gitbucket"); + } + + private static void deleteDirectory(File dir){ + for(File file: dir.listFiles()){ + if(file.isFile()){ + file.delete(); + } else if(file.isDirectory()){ + deleteDirectory(file); + } + } + dir.delete(); + } } diff --git a/src/main/resources/update/2_3.sql b/src/main/resources/update/2_3.sql new file mode 100644 index 0000000..7620092 --- /dev/null +++ b/src/main/resources/update/2_3.sql @@ -0,0 +1,6 @@ +CREATE TABLE PLUGIN ( + PLUGIN_ID VARCHAR(100) NOT NULL, + VERSION VARCHAR(100) NOT NULL +); + +ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID); diff --git a/src/main/scala/app/AccountController.scala b/src/main/scala/app/AccountController.scala index a908d76..6e7d1c3 100644 --- a/src/main/scala/app/AccountController.scala +++ b/src/main/scala/app/AccountController.scala @@ -335,7 +335,7 @@ builder.finish() JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), - loginAccount.fullName, loginAccount.mailAddress, "Initial commit") + Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit") } } diff --git a/src/main/scala/app/ControllerBase.scala b/src/main/scala/app/ControllerBase.scala index b36d53c..c31b90f 100644 --- a/src/main/scala/app/ControllerBase.scala +++ b/src/main/scala/app/ControllerBase.scala @@ -24,8 +24,9 @@ implicit val jsonFormats = DefaultFormats - // Don't set content type via Accept header. - override def format(implicit request: HttpServletRequest) = "" +// TODO Scala 2.11 +// // Don't set content type via Accept header. +// override def format(implicit request: HttpServletRequest) = "" override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = try { val httpRequest = request.asInstanceOf[HttpServletRequest] @@ -125,11 +126,13 @@ } } - override def fullUrl(path: String, params: Iterable[(String, Any)] = Iterable.empty, - includeContextPath: Boolean = true, includeServletPath: Boolean = true) - (implicit request: HttpServletRequest, response: HttpServletResponse) = + // TODO Scala 2.11 + override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty, + includeContextPath: Boolean = true, includeServletPath: Boolean = true, + absolutize: Boolean = true, withSessionId: Boolean = true) + (implicit request: HttpServletRequest, response: HttpServletResponse): String = if (path.startsWith("http")) path - else baseUrl + url(path, params, false, false, false) + else baseUrl + super.url(path, params, false, false, false) } diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index d0a40f0..7bf30c1 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -9,10 +9,11 @@ with UsersAuthenticator trait DashboardControllerBase extends ControllerBase { - self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => + self: IssuesService with PullRequestService with RepositoryService with AccountService + with UsersAuthenticator => get("/dashboard/issues/repos")(usersOnly { - searchIssues("all") + searchIssues("created_by") }) get("/dashboard/issues/assigned")(usersOnly { @@ -23,6 +24,10 @@ searchIssues("created_by") }) + get("/dashboard/issues/mentioned")(usersOnly { + searchIssues("mentioned") + }) + get("/dashboard/pulls")(usersOnly { searchPullRequests("created_by", None) }) @@ -31,6 +36,10 @@ searchPullRequests("created_by", None) }) + get("/dashboard/pulls/mentioned")(usersOnly { + searchPullRequests("mentioned", None) + }) + get("/dashboard/pulls/public")(usersOnly { searchPullRequests("not_created_by", None) }) @@ -54,19 +63,13 @@ val page = IssueSearchCondition.page(request) dashboard.html.issues( - issues.html.listparts( - searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), - page, - countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), - countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), - condition), - countIssue(condition, Map.empty, false, userRepos: _*), - countIssue(condition, Map("assigned" -> userName), false, userRepos: _*), - countIssue(condition, Map("created_by" -> userName), false, userRepos: _*), - countIssueGroupByRepository(condition, filterUser, false, userRepos: _*), - condition, - filter) - + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), + page, + countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), + countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), + condition, + filter, + getGroupNames(userName)) } private def searchPullRequests(filter: String, repository: Option[String]) = { @@ -80,30 +83,18 @@ }.copy(repo = repository)) val userName = context.loginAccount.get.userName - val allRepos = getAllRepositories() - val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) + val allRepos = getAllRepositories(userName) val filterUser = Map(filter -> userName) val page = IssueSearchCondition.page(request) - val counts = countIssueGroupByRepository( - IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*) - dashboard.html.pulls( - pulls.html.listparts( - searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), - page, - countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), - countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), - condition, - None, - false), - getPullRequestCountGroupByUser(condition.state == "closed", None, None), - userRepos.map { case (userName, repoName) => - (userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0)) - }.sortBy(_._3).reverse, + searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), + page, + countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), + countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), condition, - filter) - + filter, + getGroupNames(userName)) } diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index 4f28466..d2bb683 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -9,6 +9,7 @@ import util.ControlUtil._ import org.scalatra.Ok import model.Issue +import plugin.PluginSystem class IssuesController extends IssuesControllerBase with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService @@ -20,7 +21,6 @@ case class IssueCreateForm(title: String, content: Option[String], assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) - case class IssueEditForm(title: String, content: Option[String]) case class CommentForm(issueId: Int, content: String) case class IssueStateForm(issueId: Int, content: Option[String]) @@ -32,10 +32,12 @@ "labelNames" -> trim(optional(text())) )(IssueCreateForm.apply) + val issueTitleEditForm = mapping( + "title" -> trim(label("Title", text(required))) + )(x => x) val issueEditForm = mapping( - "title" -> trim(label("Title", text(required))), - "content" -> trim(optional(text())) - )(IssueEditForm.apply) + "content" -> trim(optional(text())) + )(x => x) val commentForm = mapping( "issueId" -> label("Issue Id", number()), @@ -47,16 +49,8 @@ "content" -> trim(optional(text())) )(IssueStateForm.apply) - get("/:owner/:repository/issues")(referrersOnly { - searchIssues("all", _) - }) - - get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { - searchIssues("assigned", _) - }) - - get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { - searchIssues("created_by", _) + get("/:owner/:repository/issues")(referrersOnly { repository => + searchIssues(repository) }) get("/:owner/:repository/issues/:id")(referrersOnly { repository => @@ -125,14 +119,29 @@ } }) - ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => + ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) => defining(repository.owner, repository.name){ case (owner, name) => getIssue(owner, name, params("id")).map { issue => if(isEditable(owner, name, issue.openedUserName)){ // update issue - updateIssue(owner, name, issue.issueId, form.title, form.content) + updateIssue(owner, name, issue.issueId, title, issue.content) // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + createReferComment(owner, name, issue.copy(title = title), title) + + redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getIssue(owner, name, params("id")).map { issue => + if(isEditable(owner, name, issue.openedUserName)){ + // update issue + updateIssue(owner, name, issue.issueId, issue.title, content) + // extract references and create refer comment + createReferComment(owner, name, issue, content.getOrElse("")) redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") } else Unauthorized @@ -180,13 +189,13 @@ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ params.get("dataType") collect { case t if t == "html" => issues.html.editissue( - x.title, x.content, x.issueId, x.userName, x.repositoryName) + x.content, x.issueId, x.userName, x.repositoryName) } getOrElse { contentType = formats("json") org.json4s.jackson.Serialization.write( Map("title" -> x.title, "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true) + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) )) } } else Unauthorized @@ -203,7 +212,7 @@ contentType = formats("json") org.json4s.jackson.Serialization.write( Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true) + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) )) } } else Unauthorized @@ -234,15 +243,17 @@ milestoneId("milestoneId").map { milestoneId => getMilestonesWithIssueCount(repository.owner, repository.name) .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => - issues.milestones.html.progress(openCount + closeCount, closeCount, false) + issues.milestones.html.progress(openCount + closeCount, closeCount) } getOrElse NotFound } getOrElse Ok() }) post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => defining(params.get("value")){ action => - executeBatch(repository) { - handleComment(_, None, repository)( _ => action) + action match { + case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) } + case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) } + case _ => // TODO BadRequest } } }) @@ -292,7 +303,10 @@ private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { params("checked").split(',') map(_.toInt) foreach execute - redirect(s"/${repository.owner}/${repository.name}/issues") + params("from") match { + case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") + case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls") + } } private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { @@ -318,15 +332,15 @@ val (action, recordActivity) = getAction(issue) .collect { - case "close" => true -> (Some("close") -> - Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) - case "reopen" => false -> (Some("reopen") -> - Some(recordReopenIssueActivity _)) - } + 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 - } + updateClosed(owner, name, issueId, closed) + t + } .getOrElse(None -> None) val commentId = content @@ -336,7 +350,7 @@ case (content, action) => createComment(owner, name, userName, issueId, content, action) } - // record activity + // record comment activity if comment is entered content foreach { (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (owner, name, userName, issueId, _) @@ -369,9 +383,8 @@ } } - private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { + private def searchIssues(repository: RepositoryService.RepositoryInfo) = { defining(repository.owner, repository.name){ case (owner, repoName) => - val filterUser = Map(filter -> params.getOrElse("userName", "")) val page = IssueSearchCondition.page(request) val sessionKey = Keys.Session.Issues(owner, repoName) @@ -382,19 +395,15 @@ ) issues.html.list( - searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), + "issues", + searchIssue(condition, Map.empty, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), page, (getCollaborators(owner, repoName) :+ owner).sorted, getMilestones(owner, repoName), getLabels(owner, repoName), - countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), - countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), - countIssue(condition, Map.empty, false, owner -> repoName), - context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), - context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), - countIssueGroupByLabels(owner, repoName, condition, filterUser), + countIssue(condition.copy(state = "open" ), Map.empty, false, owner -> repoName), + countIssue(condition.copy(state = "closed"), Map.empty, false, owner -> repoName), condition, - filter, repository, hasWritePermission(owner, repoName, context.loginAccount)) } diff --git a/src/main/scala/app/LabelsController.scala b/src/main/scala/app/LabelsController.scala index 2ac47fc..7cd42bf 100644 --- a/src/main/scala/app/LabelsController.scala +++ b/src/main/scala/app/LabelsController.scala @@ -2,51 +2,67 @@ import jp.sf.amateras.scalatra.forms._ import service._ -import util.CollaboratorsAuthenticator +import util.{ReferrerAuthenticator, CollaboratorsAuthenticator} import util.Implicits._ import org.scalatra.i18n.Messages +import org.scalatra.Ok class LabelsController extends LabelsControllerBase - with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator + with LabelsService with IssuesService with RepositoryService with AccountService +with ReferrerAuthenticator with CollaboratorsAuthenticator trait LabelsControllerBase extends ControllerBase { - self: LabelsService with RepositoryService with CollaboratorsAuthenticator => + self: LabelsService with IssuesService with RepositoryService + with ReferrerAuthenticator with CollaboratorsAuthenticator => case class LabelForm(labelName: String, color: String) - val newForm = mapping( - "newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), - "newColor" -> trim(label("Color", text(required, color))) + val labelForm = mapping( + "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), + "labelColor" -> trim(label("Color", text(required, color))) )(LabelForm.apply) - val editForm = mapping( - "editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), - "editColor" -> trim(label("Color", text(required, color))) - )(LabelForm.apply) - - post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) => - createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) - redirect(s"/${repository.owner}/${repository.name}/issues") + get("/:owner/:repository/issues/labels")(referrersOnly { repository => + issues.labels.html.list( + getLabels(repository.owner, repository.name), + countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository => - issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) + ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository => + issues.labels.html.edit(None, repository) }) - ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) => + val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1)) + issues.labels.html.label( + getLabel(repository.owner, repository.name, labelId).get, + // TODO futility + countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) + }) + + ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => issues.labels.html.edit(Some(label), repository) } getOrElse NotFound() }) - ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) => + ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) => updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) - issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) + issues.labels.html.label( + getLabel(repository.owner, repository.name, params("labelId").toInt).get, + // TODO futility + countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + repository, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) - ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository => + ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository => deleteLabel(repository.owner, repository.name, params("labelId").toInt) - issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) + Ok() }) /** diff --git a/src/main/scala/app/PullRequestsController.scala b/src/main/scala/app/PullRequestsController.scala index e7680e9..c079f01 100644 --- a/src/main/scala/app/PullRequestsController.scala +++ b/src/main/scala/app/PullRequestsController.scala @@ -62,10 +62,6 @@ searchPullRequests(None, repository) }) - get("/:owner/:repository/pulls/:userName")(referrersOnly { repository => - searchPullRequests(Some(params("userName")), repository) - }) - get("/:owner/:repository/pull/:id")(referrersOnly { repository => params("id").toIntOpt.flatMap{ issueId => val owner = repository.owner @@ -443,7 +439,7 @@ val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit => new CommitInfo(revCommit) }.toList.splitWith { (commit1, commit2) => - view.helpers.date(commit1.time) == view.helpers.date(commit2.time) + view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) } val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true) @@ -453,7 +449,6 @@ private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = defining(repository.owner, repository.name){ case (owner, repoName) => - val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "") val page = IssueSearchCondition.page(request) val sessionKey = Keys.Session.Pulls(owner, repoName) @@ -463,14 +458,15 @@ else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) ) - pulls.html.list( - searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), - getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)), - userName, + issues.html.list( + "pulls", + searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), page, - countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), - countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), - countIssue(condition, Map.empty, true, owner -> repoName), + (getCollaborators(owner, repoName) :+ owner).sorted, + getMilestones(owner, repoName), + getLabels(owner, repoName), + countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName), + countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName), condition, repository, hasWritePermission(owner, repoName, context.loginAccount)) diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index 81ba552..cce95bb 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -2,10 +2,8 @@ import service._ import util.Directory._ -import util.ControlUtil._ import util.Implicits._ import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator} -import util.JGitUtil.CommitInfo import jp.sf.amateras.scalatra.forms._ import org.apache.commons.io.FileUtils import org.scalatra.i18n.Messages @@ -13,6 +11,7 @@ import util.JGitUtil.CommitInfo import util.ControlUtil._ import org.eclipse.jgit.api.Git +import org.eclipse.jgit.lib.Constants class RepositorySettingsController extends RepositorySettingsControllerBase with RepositoryService with AccountService with WebHookService @@ -71,11 +70,12 @@ * Save the repository options. */ post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) => + val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch saveRepositoryOptions( repository.owner, repository.name, form.description, - if(repository.branchList.isEmpty) "master" else form.defaultBranch, + defaultBranch, repository.repository.parentUserName.map { _ => repository.repository.isPrivate } getOrElse form.isPrivate @@ -93,6 +93,10 @@ FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName)) } } + // Change repository HEAD + using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git => + git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch) + } flash += "info" -> "Repository settings has been updated." redirect(s"/${repository.owner}/${form.repositoryName}/settings/options") }) @@ -131,7 +135,7 @@ * Display the web hook page. */ get("/:owner/:repository/settings/hooks")(ownerOnly { repository => - settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info")) + settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info")) }) /** @@ -153,7 +157,7 @@ /** * Send the test request to registered web hook URLs. */ - get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => + post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => import scala.collection.JavaConverters._ val commits = git.log @@ -161,15 +165,13 @@ .setMaxCount(3) .call.iterator.asScala.map(new CommitInfo(_)) - getWebHookURLs(repository.owner, repository.name) match { - case webHookURLs if(webHookURLs.nonEmpty) => - for(ownerAccount <- getAccountByUserName(repository.owner)){ - callWebHook(repository.owner, repository.name, webHookURLs, - WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)) - } - case _ => + getAccountByUserName(repository.owner).foreach { ownerAccount => + callWebHook(repository.owner, repository.name, + List(model.WebHook(repository.owner, repository.name, form.url)), + WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount) + ) } - + flash += "url" -> form.url flash += "info" -> "Test payload deployed!" } redirect(s"/${repository.owner}/${repository.name}/settings/hooks") diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index ab2cf05..5b5b11b 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -8,11 +8,12 @@ import service._ import org.scalatra._ import java.io.File -import org.eclipse.jgit.api.Git + +import org.eclipse.jgit.api.{ArchiveCommand, Git} +import org.eclipse.jgit.archive.{TgzFormat, ZipFormat} import org.eclipse.jgit.lib._ import org.apache.commons.io.FileUtils import org.eclipse.jgit.treewalk._ -import java.util.zip.{ZipEntry, ZipOutputStream} import jp.sf.amateras.scalatra.forms._ import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.revwalk.RevCommit @@ -22,6 +23,7 @@ with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator + /** * The repository viewer. */ @@ -29,6 +31,9 @@ self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator => + ArchiveCommand.registerFormat("zip", new ZipFormat) + ArchiveCommand.registerFormat("tar.gz", new TgzFormat) + case class EditorForm( branch: String, path: String, @@ -72,7 +77,9 @@ contentType = "text/html" view.helpers.markdown(params("content"), repository, params("enableWikiLink").toBoolean, - params("enableRefsLink").toBoolean) + params("enableRefsLink").toBoolean, + params("enableTaskList").toBoolean, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) /** @@ -106,8 +113,8 @@ case Right((logs, hasNext)) => repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, logs.splitWith{ (commit1, commit2) => - view.helpers.date(commit1.time) == view.helpers.date(commit2.time) - }, page, hasNext) + view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) + }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount)) case Left(_) => NotFound } } @@ -186,6 +193,7 @@ using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) + val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path) getPathObjectId(git, path, revCommit).map { objectId => if(raw){ // Download @@ -195,7 +203,7 @@ } } else { repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId), - new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) + new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount)) } } getOrElse NotFound } @@ -234,6 +242,24 @@ }) /** + * Creates a branch. + */ + post("/:owner/:repository/branches")(collaboratorsOnly { repository => + val newBranchName = params.getOrElse("new", halt(400)) + val fromBranchName = params.getOrElse("from", halt(400)) + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + JGitUtil.createBranch(git, fromBranchName, newBranchName) + } match { + case Right(message) => + flash += "info" -> message + redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}") + case Left(message) => + flash += "error" -> message + redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}") + } + }) + + /** * Deletes branch. */ get("/:owner/:repository/delete/*")(collaboratorsOnly { repository => @@ -259,50 +285,12 @@ * Download repository contents as an archive. */ get("/:owner/:repository/archive/*")(referrersOnly { repository => - val name = multiParams("splat").head - - if(name.endsWith(".zip")){ - val revision = name.stripSuffix(".zip") - val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) - if(workDir.exists){ - FileUtils.deleteDirectory(workDir) - } - workDir.mkdirs - - val zipFile = new File(workDir, repository.name + "-" + - (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + ".zip") - - using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) - using(new TreeWalk(git.getRepository)){ walk => - val reader = walk.getObjectReader - val objectId = new MutableObjectId - - using(new ZipOutputStream(new java.io.FileOutputStream(zipFile))){ out => - walk.addTree(revCommit.getTree) - walk.setRecursive(true) - - while(walk.next){ - val name = walk.getPathString - val mode = walk.getFileMode(0) - if(mode == FileMode.REGULAR_FILE || mode == FileMode.EXECUTABLE_FILE){ - walk.getObjectId(objectId, 0) - val entry = new ZipEntry(name) - val loader = reader.open(objectId) - entry.setSize(loader.getSize) - out.putNextEntry(entry) - loader.copyTo(out) - } - } - } - } - } - - contentType = "application/octet-stream" - response.setHeader("Content-Disposition", s"attachment; filename=${zipFile.getName}") - zipFile - } else { - BadRequest + multiParams("splat").head match { + case name if name.endsWith(".zip") => + archiveRepository(name, ".zip", repository) + case name if name.endsWith(".tar.gz") => + archiveRepository(name, ".tar.gz", repository) + case _ => BadRequest } }) @@ -344,10 +332,10 @@ repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount)) } else { using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => - //val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head) // get specified commit JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) => defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit => + val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) // get files val files = JGitUtil.getFileList(git, revision, path) val parentPath = if (path == ".") Nil else path.split("/").toList @@ -362,8 +350,9 @@ repo.html.files(revision, repository, if(path == ".") Nil else path.split("/").toList, // current path - new JGitUtil.CommitInfo(revCommit), // latest commit - files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit + files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount), + flash.get("info"), flash.get("error")) } } getOrElse NotFound } @@ -383,7 +372,7 @@ val builder = DirCache.newInCore.builder() val inserter = git.getRepository.newObjectInserter() val headName = s"refs/heads/${branch}" - val headTip = git.getRepository.resolve(s"refs/heads/${branch}") + val headTip = git.getRepository.resolve(headName) JGitUtil.processTree(git, headTip){ (path, tree) => if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){ @@ -398,7 +387,7 @@ builder.finish() val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter), - loginAccount.fullName, loginAccount.mailAddress, message) + headName, loginAccount.fullName, loginAccount.mailAddress, message) inserter.flush() inserter.release() @@ -447,4 +436,29 @@ } } + private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = { + val revision = name.stripSuffix(suffix) + val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId) + if(workDir.exists) { + FileUtils.deleteDirectory(workDir) + } + workDir.mkdirs + + val file = new File(workDir, repository.name + "-" + + (if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix) + + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision)) + using(new java.io.FileOutputStream(file)) { out => + git.archive + .setFormat(suffix.tail) + .setTree(revCommit.getTree) + .setOutputStream(out) + .call() + } + contentType = "application/octet-stream" + response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}") + file + } + } } diff --git a/src/main/scala/app/SystemSettingsController.scala b/src/main/scala/app/SystemSettingsController.scala index ecc95b6..27b18fa 100644 --- a/src/main/scala/app/SystemSettingsController.scala +++ b/src/main/scala/app/SystemSettingsController.scala @@ -11,6 +11,7 @@ import java.io.FileInputStream import plugin.{Plugin, PluginSystem} import org.scalatra.Ok +import util.Implicits._ class SystemSettingsController extends SystemSettingsControllerBase with AccountService with AdminAuthenticator @@ -84,41 +85,55 @@ }) get("/admin/plugins")(adminOnly { - val installedPlugins = plugin.PluginSystem.plugins - val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable") - admin.plugins.html.installed(installedPlugins, updatablePlugins) + if(enablePluginSystem){ + val installedPlugins = plugin.PluginSystem.plugins + val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable") + admin.plugins.html.installed(installedPlugins, updatablePlugins) + } else NotFound }) post("/admin/plugins/_update", pluginForm)(adminOnly { form => - deletePlugins(form.pluginIds) - installPlugins(form.pluginIds) - redirect("/admin/plugins") + if(enablePluginSystem){ + deletePlugins(form.pluginIds) + installPlugins(form.pluginIds) + redirect("/admin/plugins") + } else NotFound }) post("/admin/plugins/_delete", pluginForm)(adminOnly { form => - deletePlugins(form.pluginIds) - redirect("/admin/plugins") + if(enablePluginSystem){ + deletePlugins(form.pluginIds) + redirect("/admin/plugins") + } else NotFound }) get("/admin/plugins/available")(adminOnly { - val installedPlugins = plugin.PluginSystem.plugins - val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available") - admin.plugins.html.available(availablePlugins) + if(enablePluginSystem){ + val installedPlugins = plugin.PluginSystem.plugins + val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available") + admin.plugins.html.available(availablePlugins) + } else NotFound }) post("/admin/plugins/_install", pluginForm)(adminOnly { form => - installPlugins(form.pluginIds) - redirect("/admin/plugins") + if(enablePluginSystem){ + installPlugins(form.pluginIds) + redirect("/admin/plugins") + } else NotFound }) get("/admin/plugins/console")(adminOnly { - admin.plugins.html.console() + if(enablePluginSystem){ + admin.plugins.html.console() + } else NotFound }) post("/admin/plugins/console")(adminOnly { - val script = request.getParameter("script") - val result = plugin.JavaScriptPlugin.evaluateJavaScript(script) - Ok(result) + if(enablePluginSystem){ + val script = request.getParameter("script") + val result = plugin.ScalaPlugin.eval(script) + Ok() + } else NotFound }) // TODO Move these methods to PluginSystem or Service? @@ -138,9 +153,10 @@ val installedPlugins = plugin.PluginSystem.plugins getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin => val pluginDir = new java.io.File(PluginHome, plugin.id) - if(!pluginDir.exists){ - FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir) + if(pluginDir.exists){ + FileUtils.deleteDirectory(pluginDir) } + FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir) PluginSystem.installPlugin(plugin.id) } } diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala index 6125039..d98d703 100644 --- a/src/main/scala/app/UserManagementController.scala +++ b/src/main/scala/app/UserManagementController.scala @@ -49,7 +49,7 @@ "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())) + "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) )(EditUserForm.apply) val newGroupForm = mapping( @@ -190,4 +190,14 @@ } } + 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) + Some("You can't disable your account yourself") + else + None + } + } + } } diff --git a/src/main/scala/model/Account.scala b/src/main/scala/model/Account.scala index ca7aff5..012c559 100644 --- a/src/main/scala/model/Account.scala +++ b/src/main/scala/model/Account.scala @@ -21,20 +21,19 @@ val removed = column[Boolean]("REMOVED") def * = (userName, fullName, mailAddress, password, isAdmin, url.?, registeredDate, updatedDate, lastLoginDate.?, image.?, groupAccount, removed) <> (Account.tupled, Account.unapply) } - - case class Account( - - userName: String, - fullName: String, - mailAddress: String, - password: String, - isAdmin: Boolean, - url: Option[String], - registeredDate: java.util.Date, - updatedDate: java.util.Date, - lastLoginDate: Option[java.util.Date], - image: Option[String], - isGroupAccount: Boolean, - isRemoved: Boolean - ) } + +case class Account( + userName: String, + fullName: String, + mailAddress: String, + password: String, + isAdmin: Boolean, + url: Option[String], + registeredDate: java.util.Date, + updatedDate: java.util.Date, + lastLoginDate: Option[java.util.Date], + image: Option[String], + isGroupAccount: Boolean, + isRemoved: Boolean +) diff --git a/src/main/scala/model/Activity.scala b/src/main/scala/model/Activity.scala index 7977047..8e3960e 100644 --- a/src/main/scala/model/Activity.scala +++ b/src/main/scala/model/Activity.scala @@ -15,15 +15,15 @@ val activityDate = column[java.util.Date]("ACTIVITY_DATE") def * = (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply) } - - case class Activity( - userName: String, - repositoryName: String, - activityUserName: String, - activityType: String, - message: String, - additionalInfo: Option[String], - activityDate: java.util.Date, - activityId: Int = 0 - ) } + +case class Activity( + userName: String, + repositoryName: String, + activityUserName: String, + activityType: String, + message: String, + additionalInfo: Option[String], + activityDate: java.util.Date, + activityId: Int = 0 +) diff --git a/src/main/scala/model/BasicTemplate.scala b/src/main/scala/model/BasicTemplate.scala index e0460a9..6266ebc 100644 --- a/src/main/scala/model/BasicTemplate.scala +++ b/src/main/scala/model/BasicTemplate.scala @@ -8,40 +8,40 @@ val repositoryName = column[String]("REPOSITORY_NAME") def byRepository(owner: String, repository: String) = - (userName is owner.bind) && (repositoryName is repository.bind) + (userName === owner.bind) && (repositoryName === repository.bind) def byRepository(userName: Column[String], repositoryName: Column[String]) = - (this.userName is userName) && (this.repositoryName is repositoryName) + (this.userName === userName) && (this.repositoryName === repositoryName) } trait IssueTemplate extends BasicTemplate { self: Table[_] => val issueId = column[Int]("ISSUE_ID") def byIssue(owner: String, repository: String, issueId: Int) = - byRepository(owner, repository) && (this.issueId is issueId.bind) + byRepository(owner, repository) && (this.issueId === issueId.bind) def byIssue(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = - byRepository(userName, repositoryName) && (this.issueId is issueId) + byRepository(userName, repositoryName) && (this.issueId === issueId) } trait LabelTemplate extends BasicTemplate { self: Table[_] => val labelId = column[Int]("LABEL_ID") def byLabel(owner: String, repository: String, labelId: Int) = - byRepository(owner, repository) && (this.labelId is labelId.bind) + byRepository(owner, repository) && (this.labelId === labelId.bind) def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = - byRepository(userName, repositoryName) && (this.labelId is labelId) + byRepository(userName, repositoryName) && (this.labelId === labelId) } trait MilestoneTemplate extends BasicTemplate { self: Table[_] => val milestoneId = column[Int]("MILESTONE_ID") def byMilestone(owner: String, repository: String, milestoneId: Int) = - byRepository(owner, repository) && (this.milestoneId is milestoneId.bind) + byRepository(owner, repository) && (this.milestoneId === milestoneId.bind) def byMilestone(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = - byRepository(userName, repositoryName) && (this.milestoneId is milestoneId) + byRepository(userName, repositoryName) && (this.milestoneId === milestoneId) } } diff --git a/src/main/scala/model/Collaborator.scala b/src/main/scala/model/Collaborator.scala index 7ef52e0..88311e1 100644 --- a/src/main/scala/model/Collaborator.scala +++ b/src/main/scala/model/Collaborator.scala @@ -10,12 +10,12 @@ def * = (userName, repositoryName, collaboratorName) <> (Collaborator.tupled, Collaborator.unapply) def byPrimaryKey(owner: String, repository: String, collaborator: String) = - byRepository(owner, repository) && (collaboratorName is collaborator.bind) + byRepository(owner, repository) && (collaboratorName === collaborator.bind) } - - case class Collaborator( - userName: String, - repositoryName: String, - collaboratorName: String - ) } + +case class Collaborator( + userName: String, + repositoryName: String, + collaboratorName: String +) diff --git a/src/main/scala/model/GroupMembers.scala b/src/main/scala/model/GroupMembers.scala index 966bdba..f0161d3 100644 --- a/src/main/scala/model/GroupMembers.scala +++ b/src/main/scala/model/GroupMembers.scala @@ -11,10 +11,10 @@ val isManager = column[Boolean]("MANAGER") def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply) } - - case class GroupMember( - groupName: String, - userName: String, - isManager: Boolean - ) } + +case class GroupMember( + groupName: String, + userName: String, + isManager: Boolean +) diff --git a/src/main/scala/model/Issue.scala b/src/main/scala/model/Issue.scala index d18c098..85c6014 100644 --- a/src/main/scala/model/Issue.scala +++ b/src/main/scala/model/Issue.scala @@ -31,18 +31,19 @@ def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) } - - case class Issue( - userName: String, - repositoryName: String, - issueId: Int, - openedUserName: String, - milestoneId: Option[Int], - assignedUserName: Option[String], - title: String, - content: Option[String], - closed: Boolean, - registeredDate: java.util.Date, - updatedDate: java.util.Date, - isPullRequest: Boolean) } + +case class Issue( + userName: String, + repositoryName: String, + issueId: Int, + openedUserName: String, + milestoneId: Option[Int], + assignedUserName: Option[String], + title: String, + content: Option[String], + closed: Boolean, + registeredDate: java.util.Date, + updatedDate: java.util.Date, + isPullRequest: Boolean +) diff --git a/src/main/scala/model/IssueComment.scala b/src/main/scala/model/IssueComment.scala index 8d42702..4c8ca6e 100644 --- a/src/main/scala/model/IssueComment.scala +++ b/src/main/scala/model/IssueComment.scala @@ -17,18 +17,18 @@ val updatedDate = column[java.util.Date]("UPDATED_DATE") def * = (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply) - def byPrimaryKey(commentId: Int) = this.commentId is commentId.bind + def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind } - - case class IssueComment( - userName: String, - repositoryName: String, - issueId: Int, - commentId: Int = 0, - action: String, - commentedUserName: String, - content: String, - registeredDate: java.util.Date, - updatedDate: java.util.Date - ) } + +case class IssueComment( + userName: String, + repositoryName: String, + issueId: Int, + commentId: Int = 0, + action: String, + commentedUserName: String, + content: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date +) diff --git a/src/main/scala/model/IssueLabels.scala b/src/main/scala/model/IssueLabels.scala index 413dbd7..5d42272 100644 --- a/src/main/scala/model/IssueLabels.scala +++ b/src/main/scala/model/IssueLabels.scala @@ -8,12 +8,13 @@ class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply) def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = - byIssue(owner, repository, issueId) && (this.labelId is labelId.bind) + byIssue(owner, repository, issueId) && (this.labelId === labelId.bind) } - - case class IssueLabel( - userName: String, - repositoryName: String, - issueId: Int, - labelId: Int) } + +case class IssueLabel( + userName: String, + repositoryName: String, + issueId: Int, + labelId: Int +) diff --git a/src/main/scala/model/Labels.scala b/src/main/scala/model/Labels.scala index 13e66d9..47c6a2b 100644 --- a/src/main/scala/model/Labels.scala +++ b/src/main/scala/model/Labels.scala @@ -14,24 +14,24 @@ def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) def byPrimaryKey(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) = byLabel(userName, repositoryName, labelId) } +} - case class Label( - userName: String, - repositoryName: String, - labelId: Int = 0, - labelName: String, - color: String){ +case class Label( + userName: String, + repositoryName: String, + labelId: Int = 0, + labelName: String, + color: String){ - val fontColor = { - val r = color.substring(0, 2) - val g = color.substring(2, 4) - val b = color.substring(4, 6) + val fontColor = { + val r = color.substring(0, 2) + val g = color.substring(2, 4) + val b = color.substring(4, 6) - if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ - "000000" - } else { - "FFFFFF" - } + if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ + "000000" + } else { + "ffffff" } } } diff --git a/src/main/scala/model/Milestone.scala b/src/main/scala/model/Milestone.scala index 3aa4314..c392219 100644 --- a/src/main/scala/model/Milestone.scala +++ b/src/main/scala/model/Milestone.scala @@ -17,13 +17,14 @@ def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) def byPrimaryKey(userName: Column[String], repositoryName: Column[String], milestoneId: Column[Int]) = byMilestone(userName, repositoryName, milestoneId) } - - case class Milestone( - userName: String, - repositoryName: String, - milestoneId: Int = 0, - title: String, - description: Option[String], - dueDate: Option[java.util.Date], - closedDate: Option[java.util.Date]) } + +case class Milestone( + userName: String, + repositoryName: String, + milestoneId: Int = 0, + title: String, + description: Option[String], + dueDate: Option[java.util.Date], + closedDate: Option[java.util.Date] +) diff --git a/src/main/scala/model/Plugin.scala b/src/main/scala/model/Plugin.scala new file mode 100644 index 0000000..bc85ca0 --- /dev/null +++ b/src/main/scala/model/Plugin.scala @@ -0,0 +1,19 @@ +package model + +trait PluginComponent extends TemplateComponent { self: Profile => + import profile.simple._ + import self._ + + lazy val Plugins = TableQuery[Plugins] + + class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){ + val pluginId = column[String]("PLUGIN_ID", O PrimaryKey) + val version = column[String]("VERSION") + def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply) + } +} + +case class Plugin( + pluginId: String, + version: String +) diff --git a/src/main/scala/model/Profile.scala b/src/main/scala/model/Profile.scala index 3d2a2e7..10e2ab1 100644 --- a/src/main/scala/model/Profile.scala +++ b/src/main/scala/model/Profile.scala @@ -1,7 +1,7 @@ package model trait Profile { - val profile = slick.driver.H2Driver + val profile: slick.driver.JdbcProfile import profile.simple._ // java.util.Date Mapped Column Types @@ -15,3 +15,28 @@ } } + +object Profile extends { + val profile = slick.driver.H2Driver + +} with AccountComponent + with ActivityComponent + with CollaboratorComponent + with GroupMemberComponent + with IssueComponent + with IssueCommentComponent + with IssueLabelComponent + with LabelComponent + with MilestoneComponent + with PullRequestComponent + with RepositoryComponent + with SshKeyComponent + with WebHookComponent + with PluginComponent with Profile { + + /** + * Returns system date. + */ + def currentDate = new java.util.Date() + +} diff --git a/src/main/scala/model/PullRequest.scala b/src/main/scala/model/PullRequest.scala index 500e8ab..3ba87ea 100644 --- a/src/main/scala/model/PullRequest.scala +++ b/src/main/scala/model/PullRequest.scala @@ -17,16 +17,16 @@ def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = byIssue(userName, repositoryName, issueId) def byPrimaryKey(userName: Column[String], repositoryName: Column[String], issueId: Column[Int]) = byIssue(userName, repositoryName, issueId) } - - case class PullRequest( - userName: String, - repositoryName: String, - issueId: Int, - branch: String, - requestUserName: String, - requestRepositoryName: String, - requestBranch: String, - commitIdFrom: String, - commitIdTo: String - ) } + +case class PullRequest( + userName: String, + repositoryName: String, + issueId: Int, + branch: String, + requestUserName: String, + requestRepositoryName: String, + requestBranch: String, + commitIdFrom: String, + commitIdTo: String +) diff --git a/src/main/scala/model/Repository.scala b/src/main/scala/model/Repository.scala index fe0df8a..5a888fc 100644 --- a/src/main/scala/model/Repository.scala +++ b/src/main/scala/model/Repository.scala @@ -21,19 +21,19 @@ def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) } - - case class Repository( - userName: String, - repositoryName: String, - isPrivate: Boolean, - description: Option[String], - defaultBranch: String, - registeredDate: java.util.Date, - updatedDate: java.util.Date, - lastActivityDate: java.util.Date, - originUserName: Option[String], - originRepositoryName: Option[String], - parentUserName: Option[String], - parentRepositoryName: Option[String] - ) } + +case class Repository( + userName: String, + repositoryName: String, + isPrivate: Boolean, + description: Option[String], + defaultBranch: String, + registeredDate: java.util.Date, + updatedDate: java.util.Date, + lastActivityDate: java.util.Date, + originUserName: Option[String], + originRepositoryName: Option[String], + parentUserName: Option[String], + parentRepositoryName: Option[String] +) diff --git a/src/main/scala/model/SshKey.scala b/src/main/scala/model/SshKey.scala index 5106ab2..dcf3463 100644 --- a/src/main/scala/model/SshKey.scala +++ b/src/main/scala/model/SshKey.scala @@ -12,13 +12,13 @@ val publicKey = column[String]("PUBLIC_KEY") def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply) - def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName is userName.bind) && (this.sshKeyId is sshKeyId.bind) + def byPrimaryKey(userName: String, sshKeyId: Int) = (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind) } - - case class SshKey( - userName: String, - sshKeyId: Int = 0, - title: String, - publicKey: String - ) } + +case class SshKey( + userName: String, + sshKeyId: Int = 0, + title: String, + publicKey: String +) diff --git a/src/main/scala/model/WebHook.scala b/src/main/scala/model/WebHook.scala index da7fb8f..4c13c87 100644 --- a/src/main/scala/model/WebHook.scala +++ b/src/main/scala/model/WebHook.scala @@ -9,12 +9,12 @@ val url = column[String]("URL") def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply) - def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url is url.bind) + def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind) } - - case class WebHook( - userName: String, - repositoryName: String, - url: String - ) } + +case class WebHook( + userName: String, + repositoryName: String, + url: String +) diff --git a/src/main/scala/model/package.scala b/src/main/scala/model/package.scala index b806c33..c65e72e 100644 --- a/src/main/scala/model/package.scala +++ b/src/main/scala/model/package.scala @@ -1,21 +1,3 @@ -package object model extends Profile - with AccountComponent - with ActivityComponent - with CollaboratorComponent - with GroupMemberComponent - with IssueComponent - with IssueCommentComponent - with IssueLabelComponent - with LabelComponent - with MilestoneComponent - with PullRequestComponent - with RepositoryComponent - with SshKeyComponent - with WebHookComponent { - - /** - * Returns system date. - */ - def currentDate = new java.util.Date() - +package object model { + type Session = slick.jdbc.JdbcBackend#Session } diff --git a/src/main/scala/plugin/JavaScriptPlugin.scala b/src/main/scala/plugin/JavaScriptPlugin.scala deleted file mode 100644 index adaca5e..0000000 --- a/src/main/scala/plugin/JavaScriptPlugin.scala +++ /dev/null @@ -1,117 +0,0 @@ -package plugin - -import org.mozilla.javascript.{Context => JsContext} -import org.mozilla.javascript.{Function => JsFunction} -import scala.collection.mutable.ListBuffer -import plugin.PluginSystem._ -import util.ControlUtil._ -import plugin.PluginSystem.GlobalMenu -import plugin.PluginSystem.RepositoryAction -import plugin.PluginSystem.Action -import plugin.PluginSystem.RepositoryMenu - -class JavaScriptPlugin(val id: String, val version: String, - val author: String, val url: String, val description: String) extends Plugin { - - private val repositoryMenuList = ListBuffer[RepositoryMenu]() - private val globalMenuList = ListBuffer[GlobalMenu]() - private val repositoryActionList = ListBuffer[RepositoryAction]() - private val globalActionList = ListBuffer[Action]() - - def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList - def globalMenus : List[GlobalMenu] = globalMenuList.toList - def repositoryActions : List[RepositoryAction] = repositoryActionList.toList - def globalActions : List[Action] = globalActionList.toList - - def addRepositoryMenu(label: String, name: String, url: String, icon: String, condition: JsFunction): Unit = { - repositoryMenuList += RepositoryMenu(label, name, url, icon, (context) => { - val context = JsContext.enter() - try { - condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean] - } finally { - JsContext.exit() - } - }) - } - - def addGlobalMenu(label: String, url: String, icon: String, condition: JsFunction): Unit = { - globalMenuList += GlobalMenu(label, url, icon, (context) => { - val context = JsContext.enter() - try { - condition.call(context, condition, condition, Array(context)).asInstanceOf[Boolean] - } finally { - JsContext.exit() - } - }) - } - - def addGlobalAction(path: String, function: JsFunction): Unit = { - globalActionList += Action(path, (request, response) => { - val context = JsContext.enter() - try { - function.call(context, function, function, Array(request, response)) - } finally { - JsContext.exit() - } - }) - } - - def addRepositoryAction(path: String, function: JsFunction): Unit = { - repositoryActionList += RepositoryAction(path, (request, response, repository) => { - val context = JsContext.enter() - try { - function.call(context, function, function, Array(request, response, repository)) - } finally { - JsContext.exit() - } - }) - } - - object db { - // TODO Use JavaScript Map instead of java.util.Map - def select(sql: String): Array[java.util.Map[String, String]] = { - defining(PluginConnectionHolder.threadLocal.get){ conn => - using(conn.prepareStatement(sql)){ stmt => - using(stmt.executeQuery()){ rs => - val list = new java.util.ArrayList[java.util.Map[String, String]]() - while(rs.next){ - defining(rs.getMetaData){ meta => - val map = new java.util.HashMap[String, String]() - Range(1, meta.getColumnCount).map { i => - val name = meta.getColumnName(i) - map.put(name, rs.getString(name)) - } - list.add(map) - } - } - list.toArray(new Array[java.util.Map[String, String]](list.size)) - } - } - } - } - } - -} - -object JavaScriptPlugin { - - def define(id: String, version: String, author: String, url: String, description: String) - = new JavaScriptPlugin(id, version, author, url, description) - - def evaluateJavaScript(script: String, vars: Map[String, Any] = Map.empty): Any = { - val context = JsContext.enter() - try { - val scope = context.initStandardObjects() - scope.put("PluginSystem", scope, PluginSystem) - scope.put("JavaScriptPlugin", scope, this) - vars.foreach { case (key, value) => - scope.put(key, scope, value) - } - val result = context.evaluateString(scope, script, "", 1, null) - result - } finally { - JsContext.exit - } - } - -} \ No newline at end of file diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala index 43ce373..f4a7c1a 100644 --- a/src/main/scala/plugin/Plugin.scala +++ b/src/main/scala/plugin/Plugin.scala @@ -10,10 +10,11 @@ val url: String val description: String - def repositoryMenus : List[RepositoryMenu] - def globalMenus : List[GlobalMenu] - def repositoryActions : List[RepositoryAction] - def globalActions : List[Action] + def repositoryMenus : List[RepositoryMenu] + def globalMenus : List[GlobalMenu] + def repositoryActions : List[RepositoryAction] + def globalActions : List[Action] + def javaScripts : List[JavaScript] } object PluginConnectionHolder { diff --git a/src/main/scala/plugin/PluginSystem.scala b/src/main/scala/plugin/PluginSystem.scala index 6c96ac9..c12c9cd 100644 --- a/src/main/scala/plugin/PluginSystem.scala +++ b/src/main/scala/plugin/PluginSystem.scala @@ -1,20 +1,24 @@ package plugin -import app.Context import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import org.slf4j.LoggerFactory import java.util.concurrent.atomic.AtomicBoolean import util.Directory._ import util.ControlUtil._ -import org.apache.commons.io.FileUtils -import util.JGitUtil -import org.eclipse.jgit.api.Git +import org.apache.commons.io.{IOUtils, FileUtils} +import Security._ +import service.PluginService +import model.Profile._ +import profile.simple._ +import java.io.FileInputStream +import java.sql.Connection +import app.Context import service.RepositoryService.RepositoryInfo /** * Provides extension points to plug-ins. */ -object PluginSystem { +object PluginSystem extends PluginService { private val logger = LoggerFactory.getLogger(PluginSystem.getClass) @@ -28,8 +32,21 @@ def plugins: List[Plugin] = pluginsMap.values.toList - def uninstall(id: String): Unit = { + def uninstall(id: String)(implicit session: Session): Unit = { pluginsMap.remove(id) + + // Delete from PLUGIN table + deletePlugin(id) + + // Drop tables + val pluginDir = new java.io.File(PluginHome) + val sqlFile = new java.io.File(pluginDir, s"${id}/sql/drop.sql") + if(sqlFile.exists){ + val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8") + using(session.conn.createStatement()){ stmt => + stmt.executeUpdate(sql) + } + } } def repositories: List[PluginRepository] = repositoriesList.toList @@ -37,7 +54,7 @@ /** * Initializes the plugin system. Load scripts from GITBUCKET_HOME/plugins. */ - def init(): Unit = { + def init()(implicit session: Session): Unit = { if(initialized.compareAndSet(false, true)){ // Load installed plugins val pluginDir = new java.io.File(PluginHome) @@ -52,42 +69,107 @@ } // TODO Method name seems to not so good. - def installPlugin(id: String): Unit = { - val pluginDir = new java.io.File(PluginHome) - val javaScriptFile = new java.io.File(pluginDir, id + "/plugin.js") + def installPlugin(id: String)(implicit session: Session): Unit = { + val pluginHome = new java.io.File(PluginHome) + val pluginDir = new java.io.File(pluginHome, id) - if(javaScriptFile.exists && javaScriptFile.isFile){ + val scalaFile = new java.io.File(pluginDir, "plugin.scala") + if(scalaFile.exists && scalaFile.isFile){ val properties = new java.util.Properties() - using(new java.io.FileInputStream(new java.io.File(pluginDir, id + "/plugin.properties"))){ in => + using(new java.io.FileInputStream(new java.io.File(pluginDir, "plugin.properties"))){ in => properties.load(in) } - val script = FileUtils.readFileToString(javaScriptFile, "UTF-8") + val pluginId = properties.getProperty("id") + val version = properties.getProperty("version") + val author = properties.getProperty("author") + val url = properties.getProperty("url") + val description = properties.getProperty("description") + + val source = s""" + |val id = "${pluginId}" + |val version = "${version}" + |val author = "${author}" + |val url = "${url}" + |val description = "${description}" + """.stripMargin + FileUtils.readFileToString(scalaFile, "UTF-8") + try { - JavaScriptPlugin.evaluateJavaScript(script, Map( - "id" -> properties.getProperty("id"), - "version" -> properties.getProperty("version"), - "author" -> properties.getProperty("author"), - "url" -> properties.getProperty("url"), - "description" -> properties.getProperty("description") - )) + // Compile and eval Scala source code + ScalaPlugin.eval(pluginDir.listFiles.filter(_.getName.endsWith(".scala.html")).map { file => + ScalaPlugin.compileTemplate( + id.replaceAll("-", ""), + file.getName.replaceAll("\\.scala\\.html$", ""), + IOUtils.toString(new FileInputStream(file))) + }.mkString("\n") + source) + + // Migrate database + val plugin = getPlugin(pluginId) + if(plugin.isEmpty){ + registerPlugin(model.Plugin(pluginId, version)) + migrate(session.conn, pluginId, "0.0") + } else { + updatePlugin(model.Plugin(pluginId, version)) + migrate(session.conn, pluginId, plugin.get.version) + } } catch { - case e: Exception => logger.warn(s"Error in plugin loading for ${javaScriptFile.getAbsolutePath}", e) + case e: Throwable => logger.warn(s"Error in plugin loading for ${scalaFile.getAbsolutePath}", e) } } } - def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList - def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList - def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList - def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList + // TODO Should PluginSystem provide a way to migrate resources other than H2? + private def migrate(conn: Connection, pluginId: String, current: String): Unit = { + val pluginDir = new java.io.File(PluginHome) + + // TODO Is ot possible to use this migration system in GitBucket migration? + val dim = current.split("\\.") + val currentVersion = Version(dim(0).toInt, dim(1).toInt) + + val sqlDir = new java.io.File(pluginDir, s"${pluginId}/sql") + if(sqlDir.exists && sqlDir.isDirectory){ + sqlDir.listFiles.filter(_.getName.endsWith(".sql")).map { file => + val array = file.getName.replaceFirst("\\.sql", "").split("_") + Version(array(0).toInt, array(1).toInt) + } + .sorted.reverse.takeWhile(_ > currentVersion) + .reverse.foreach { version => + val sqlFile = new java.io.File(pluginDir, s"${pluginId}/sql/${version.major}_${version.minor}.sql") + val sql = IOUtils.toString(new FileInputStream(sqlFile), "UTF-8") + using(conn.createStatement()){ stmt => + stmt.executeUpdate(sql) + } + } + } + } + + case class Version(major: Int, minor: Int) extends Ordered[Version] { + + override def compare(that: Version): Int = { + if(major != that.major){ + major.compare(that.major) + } else{ + minor.compare(that.minor) + } + } + + def displayString: String = major + "." + minor + } + + def repositoryMenus : List[RepositoryMenu] = pluginsMap.values.flatMap(_.repositoryMenus).toList + def globalMenus : List[GlobalMenu] = pluginsMap.values.flatMap(_.globalMenus).toList + def repositoryActions : List[RepositoryAction] = pluginsMap.values.flatMap(_.repositoryActions).toList + def globalActions : List[Action] = pluginsMap.values.flatMap(_.globalActions).toList + def javaScripts : List[JavaScript] = pluginsMap.values.flatMap(_.javaScripts).toList // Case classes to hold plug-ins information internally in GitBucket case class PluginRepository(id: String, url: String) case class GlobalMenu(label: String, url: String, icon: String, condition: Context => Boolean) case class RepositoryMenu(label: String, name: String, url: String, icon: String, condition: Context => Boolean) - case class Action(path: String, function: (HttpServletRequest, HttpServletResponse) => Any) - case class RepositoryAction(path: String, function: (HttpServletRequest, HttpServletResponse, RepositoryInfo) => Any) + case class Action(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context) => Any) + case class RepositoryAction(method: String, path: String, security: Security, function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any) + case class Button(label: String, href: String) + case class JavaScript(filter: String => Boolean, script: String) /** * Checks whether the plugin is updatable. @@ -109,17 +191,4 @@ } } - // TODO This is a test -// addGlobalMenu("Google", "http://www.google.co.jp/", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC") -// { context => context.loginAccount.isDefined } -// -// addRepositoryMenu("Board", "board", "/board", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAEvwAABL8BkeKJvAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAIgSURBVEiJtdZNiI1hFAfw36ORhSFFPgYLszOKJAsWRLGzks1gYyFZKFs7C7K2Y2XDRiwmq9kIJWQjJR9Tk48xRtTIRwjH4p473nm99yLNqdNTz/mf//+555x7ektEmEmbNaPs6OkUKKX0YBmWp6/IE8bwIs8xjEfEt0aiiJBl6sEuXMRLfEf8pX/PnIvJ0TPFWxE4+w+Ef/Kzbd5qDx5l8H8tkku7LG17gH7sxWatevdhEUoXsjda5RnDTZzH6jagtMe0lHIa23AJw3iOiSRZlmJ9mfcyfTzFl2AldmI3rkbEkbrAYKrX7S1eVRyWVnxhQ87eiLjQ+o2/mtyve+PuYy3W4+EfsP2/TVGKTHRI+Iz9Fdx8XOmAnZjGWRMYqoF/4ESW4hpOYk1iZ2WsLjDUTeBYBfgeuyux2XiNT5hXud+DD5W8Y90EtifoSfultfjx7MVtrKzcr8No5m7vJtCLx1hQJ8/4IZzClpyoy5ibsYUYQW81Z9o2jYgPeKr15+poEXE9+1XF9WIkOaasaV2P4k4pZUdDbEm+VEQcjIgtEfGxlLIVd/Gs6TX1MhzQquU3HK1t23f4IsuS94fxNXMO/MbXIDBg+tidw5yMbcCmylSdqWEH/kagYLKWeAt9Fcxi3KhhJuXq6SqQBMO15NDalvswmLWux4cbuToIbMS9BpJOfg8bm7imtmmTlVJWaa3hpnU9nufziBjtyDHTny0/AaA7Qnb4AM4aAAAAAElFTkSuQmCC") -// { context => true} -// -// addGlobalAction("/hello"){ (request, response) => -// "Hello World!" -// } - } - - diff --git a/src/main/scala/plugin/ScalaPlugin.scala b/src/main/scala/plugin/ScalaPlugin.scala index e9ccc3b..484e621 100644 --- a/src/main/scala/plugin/ScalaPlugin.scala +++ b/src/main/scala/plugin/ScalaPlugin.scala @@ -1,10 +1,16 @@ package plugin -import app.Context import scala.collection.mutable.ListBuffer -import plugin.PluginSystem._ import javax.servlet.http.{HttpServletResponse, HttpServletRequest} +import app.Context +import plugin.PluginSystem._ +import plugin.PluginSystem.RepositoryMenu +import plugin.Security._ import service.RepositoryService.RepositoryInfo +import scala.reflect.runtime.currentMirror +import scala.tools.reflect.ToolBox +import play.twirl.compiler.TwirlCompiler +import scala.io.Codec // TODO This is a sample implementation for Scala based plug-ins. class ScalaPlugin(val id: String, val version: String, @@ -14,11 +20,13 @@ private val globalMenuList = ListBuffer[GlobalMenu]() private val repositoryActionList = ListBuffer[RepositoryAction]() private val globalActionList = ListBuffer[Action]() + private val javaScriptList = ListBuffer[JavaScript]() - def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList - def globalMenus : List[GlobalMenu] = globalMenuList.toList - def repositoryActions : List[RepositoryAction] = repositoryActionList.toList - def globalActions : List[Action] = globalActionList.toList + def repositoryMenus : List[RepositoryMenu] = repositoryMenuList.toList + def globalMenus : List[GlobalMenu] = globalMenuList.toList + def repositoryActions : List[RepositoryAction] = repositoryActionList.toList + def globalActions : List[Action] = globalActionList.toList + def javaScripts : List[JavaScript] = javaScriptList.toList def addRepositoryMenu(label: String, name: String, url: String, icon: String)(condition: (Context) => Boolean): Unit = { repositoryMenuList += RepositoryMenu(label, name, url, icon, condition) @@ -28,12 +36,42 @@ globalMenuList += GlobalMenu(label, url, icon, condition) } - def addGlobalAction(path: String)(function: (HttpServletRequest, HttpServletResponse) => Any): Unit = { - globalActionList += Action(path, function) + def addGlobalAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = { + globalActionList += Action(method, path, security, function) } - def addRepositoryAction(path: String)(function: (HttpServletRequest, HttpServletResponse, RepositoryInfo) => Any): Unit = { - repositoryActionList += RepositoryAction(path, function) + def addRepositoryAction(method: String, path: String, security: Security = All())(function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = { + repositoryActionList += RepositoryAction(method, path, security, function) } + def addJavaScript(filter: String => Boolean, script: String): Unit = { + javaScriptList += JavaScript(filter, script) + } + +} + +object ScalaPlugin { + + def define(id: String, version: String, author: String, url: String, description: String) + = new ScalaPlugin(id, version, author, url, description) + + def eval(source: String): Any = { + val toolbox = currentMirror.mkToolBox() + val tree = toolbox.parse(source) + toolbox.eval(tree) + } + + def compileTemplate(packageName: String, name: String, source: String): String = { + val result = TwirlCompiler.parseAndGenerateCodeNewParser( + Array(packageName, name), + source.getBytes("UTF-8"), + Codec(scala.util.Properties.sourceEncoding), + "", + "play.twirl.api.HtmlFormat.Appendable", + "play.twirl.api.HtmlFormat", + "", + false) + + result.replaceFirst("package .*", "") + } } diff --git a/src/main/scala/plugin/Security.scala b/src/main/scala/plugin/Security.scala new file mode 100644 index 0000000..c409020 --- /dev/null +++ b/src/main/scala/plugin/Security.scala @@ -0,0 +1,36 @@ +package plugin + +/** + * Defines enum case classes to specify permission for actions which is provided by plugin. + */ +object Security { + + sealed trait Security + + /** + * All users and guests + */ + case class All() extends Security + + /** + * Only signed-in users + */ + case class Login() extends Security + + /** + * Only repository owner and collaborators + */ + case class Member() extends Security + + /** + * Only repository owner and managers of group repository + */ + case class Owner() extends Security + + /** + * Only administrators + */ + case class Admin() extends Security + +} + diff --git a/src/main/scala/plugin/package.scala b/src/main/scala/plugin/package.scala new file mode 100644 index 0000000..15fbad9 --- /dev/null +++ b/src/main/scala/plugin/package.scala @@ -0,0 +1,56 @@ +import java.sql.PreparedStatement +import play.twirl.api.Html +import util.ControlUtil._ +import scala.collection.mutable.ListBuffer + +package object plugin { + + case class Redirect(path: String) + case class Fragment(html: Html) + case class RawData(contentType: String, content: Array[Byte]) + + object db { + // TODO labelled place holder support + def select(sql: String, params: Any*): Seq[Map[String, String]] = { + defining(PluginConnectionHolder.threadLocal.get){ conn => + using(conn.prepareStatement(sql)){ stmt => + setParams(stmt, params: _*) + using(stmt.executeQuery()){ rs => + val list = new ListBuffer[Map[String, String]]() + while(rs.next){ + defining(rs.getMetaData){ meta => + val map = Range(1, meta.getColumnCount + 1).map { i => + val name = meta.getColumnName(i) + (name, rs.getString(name)) + }.toMap + list += map + } + } + list + } + } + } + } + + // TODO labelled place holder support + def update(sql: String, params: Any*): Int = { + defining(PluginConnectionHolder.threadLocal.get){ conn => + using(conn.prepareStatement(sql)){ stmt => + setParams(stmt, params: _*) + stmt.executeUpdate() + } + } + } + + private def setParams(stmt: PreparedStatement, params: Any*): Unit = { + params.zipWithIndex.foreach { case (p, i) => + p match { + case x: String => stmt.setString(i + 1, x) + case x: Int => stmt.setInt(i + 1, x) + case x: Boolean => stmt.setBoolean(i + 1, x) + } + } + } + } + +} diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index 71934f1..c502eb7 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -1,9 +1,10 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.{Account, GroupMember} // TODO [Slick 2.0]NOT import directly? -import model.dateColumnType +import model.Profile.dateColumnType import service.SystemSettingsService.SystemSettings import util.StringUtil._ import util.LDAPUtil @@ -74,16 +75,16 @@ } def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = - Accounts filter(t => (t.userName is userName.bind) && (t.removed is false.bind, !includeRemoved)) firstOption + Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = - Accounts filter(t => (t.mailAddress.toLowerCase is mailAddress.toLowerCase.bind) && (t.removed is false.bind, !includeRemoved)) firstOption + Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption def getAllUsers(includeRemoved: Boolean = true)(implicit s: Session): List[Account] = if(includeRemoved){ Accounts sortBy(_.userName) list } else { - Accounts filter (_.removed is false.bind) sortBy(_.userName) list + Accounts filter (_.removed === false.bind) sortBy(_.userName) list } def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String]) @@ -104,7 +105,7 @@ def updateAccount(account: Account)(implicit s: Session): Unit = Accounts - .filter { a => a.userName is account.userName.bind } + .filter { a => a.userName === account.userName.bind } .map { a => (a.password, a.fullName, a.mailAddress, a.isAdmin, a.url.?, a.registeredDate, a.updatedDate, a.lastLoginDate.?, a.removed) } .update ( account.password, @@ -118,10 +119,10 @@ account.isRemoved) def updateAvatarImage(userName: String, image: Option[String])(implicit s: Session): Unit = - Accounts.filter(_.userName is userName.bind).map(_.image.?).update(image) + Accounts.filter(_.userName === userName.bind).map(_.image.?).update(image) def updateLastLoginDate(userName: String)(implicit s: Session): Unit = - Accounts.filter(_.userName is userName.bind).map(_.lastLoginDate).update(currentDate) + Accounts.filter(_.userName === userName.bind).map(_.lastLoginDate).update(currentDate) def createGroup(groupName: String, url: Option[String])(implicit s: Session): Unit = Accounts insert Account( @@ -139,10 +140,10 @@ isRemoved = false) def updateGroup(groupName: String, url: Option[String], removed: Boolean)(implicit s: Session): Unit = - Accounts.filter(_.userName is groupName.bind).map(t => t.url.? -> t.removed).update(url, removed) + Accounts.filter(_.userName === groupName.bind).map(t => t.url.? -> t.removed).update(url, removed) def updateGroupMembers(groupName: String, members: List[(String, Boolean)])(implicit s: Session): Unit = { - GroupMembers.filter(_.groupName is groupName.bind).delete + GroupMembers.filter(_.groupName === groupName.bind).delete members.foreach { case (userName, isManager) => GroupMembers insert GroupMember (groupName, userName, isManager) } @@ -150,21 +151,26 @@ def getGroupMembers(groupName: String)(implicit s: Session): List[GroupMember] = GroupMembers - .filter(_.groupName is groupName.bind) + .filter(_.groupName === groupName.bind) .sortBy(_.userName) .list def getGroupsByUserName(userName: String)(implicit s: Session): List[String] = GroupMembers - .filter(_.userName is userName.bind) + .filter(_.userName === userName.bind) .sortBy(_.groupName) .map(_.groupName) .list def removeUserRelatedData(userName: String)(implicit s: Session): Unit = { - GroupMembers.filter(_.userName is userName.bind).delete - Collaborators.filter(_.collaboratorName is userName.bind).delete - Repositories.filter(_.userName is userName.bind).delete + GroupMembers.filter(_.userName === userName.bind).delete + Collaborators.filter(_.collaboratorName === userName.bind).delete + Repositories.filter(_.userName === userName.bind).delete + } + + def getGroupNames(userName: String)(implicit s: Session): List[String] = { + List(userName) ++ + Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list } } diff --git a/src/main/scala/service/ActivityService.scala b/src/main/scala/service/ActivityService.scala index d9219dc..76107ae 100644 --- a/src/main/scala/service/ActivityService.scala +++ b/src/main/scala/service/ActivityService.scala @@ -1,7 +1,8 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.Activity trait ActivityService { @@ -10,9 +11,9 @@ .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) .filter { case (t1, t2) => if(isPublic){ - (t1.activityUserName is activityUserName.bind) && (t2.isPrivate is false.bind) + (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) } else { - (t1.activityUserName is activityUserName.bind) + (t1.activityUserName === activityUserName.bind) } } .sortBy { case (t1, t2) => t1.activityId desc } @@ -23,7 +24,7 @@ def getRecentActivities()(implicit s: Session): List[Activity] = Activities .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) - .filter { case (t1, t2) => t2.isPrivate is false.bind } + .filter { case (t1, t2) => t2.isPrivate === false.bind } .sortBy { case (t1, t2) => t1.activityId desc } .map { case (t1, t2) => t1 } .take(30) @@ -32,7 +33,7 @@ def getRecentActivitiesByOwners(owners : Set[String])(implicit s: Session): List[Activity] = Activities .innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) - .filter { case (t1, t2) => (t2.isPrivate is false.bind) || (t2.userName inSetBind owners) } + .filter { case (t1, t2) => (t2.isPrivate === false.bind) || (t2.userName inSetBind owners) } .sortBy { case (t1, t2) => t1.activityId desc } .map { case (t1, t2) => t1 } .take(30) diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 2c4a7fd..ef39a36 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -3,8 +3,9 @@ import scala.slick.jdbc.{StaticQuery => Q} import Q.interpolation -import model._ +import model.Profile._ import profile.simple._ +import model.{Issue, IssueComment, IssueLabel, Label} import util.Implicits._ import util.StringUtil._ @@ -42,7 +43,6 @@ * Returns the count of the search result against issues. * * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param repos Tuple of the repository owner and the repository name * @return the count of the search result @@ -57,7 +57,6 @@ * @param owner the repository owner * @param repository the repository name * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) * @return the Map which contains issue count for each labels (key is label name, value is issue count) */ def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, @@ -78,47 +77,47 @@ } .toMap } - /** - * Returns list which contains issue count for each repository. - * If the issue does not exist, its repository is not included in the result. - * - * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name) - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return list which contains issue count for each repository - */ - def countIssueGroupByRepository( - condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { - searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) - .groupBy { t => - t.userName -> t.repositoryName - } - .map { case (repo, t) => - (repo._1, repo._2, t.length) - } - .sortBy(_._3 desc) - .list - } + +// /** +// * Returns list which contains issue count for each repository. +// * If the issue does not exist, its repository is not included in the result. +// * +// * @param condition the search condition +// * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. +// * @param repos Tuple of the repository owner and the repository name +// * @return list which contains issue count for each repository +// */ +// def countIssueGroupByRepository( +// condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, +// repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { +// searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) +// .groupBy { t => +// t.userName -> t.repositoryName +// } +// .map { case (repo, t) => +// (repo._1, repo._2, t.length) +// } +// .sortBy(_._3 desc) +// .list +// } /** * Returns the search result against issues. * * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. + * @param filterUser the filter user name (key is "all", "assigned", "created_by", "not_created_by" or "mentioned", value is the user name) + * @param pullRequest if true then returns only pull requests, false then returns only issues. * @param offset the offset for pagination * @param limit the limit for pagination * @param repos Tuple of the repository owner and the repository name * @return the search result (list of tuples which contain issue, labels and comment count) */ - def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, + def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*) - (implicit s: Session): List[(Issue, List[Label], Int)] = { + (implicit s: Session): List[IssueInfo] = { // get issues and comment count and labels - searchIssueQuery(repos, condition, filterUser, onlyPullRequest) + searchIssueQuery(repos, condition, filterUser, pullRequest) .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .sortBy { case (t1, t2) => (condition.sort match { @@ -135,21 +134,23 @@ .drop(offset).take(limit) .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } - .map { case (((t1, t2), t3), t4) => - (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) + .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } + .map { case ((((t1, t2), t3), t4), t5) => + (t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?) } .list .splitWith { (c1, c2) => - c1._1.userName == c2._1.userName && + c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && - c1._1.issueId == c2._1.issueId + c1._1.issueId == c2._1.issueId } .map { issues => issues.head match { - case (issue, commentCount, _,_,_) => - (issue, + case (issue, commentCount, _, _, _, milestone) => + IssueInfo(issue, issues.flatMap { t => t._3.map ( Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) )} toList, + milestone, commentCount) }} toList } @@ -158,20 +159,23 @@ * Assembles query for conditional issue searching. */ private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, - filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) = + filterUser: Map[String, String], pullRequest: Boolean)(implicit s: Session) = Issues filter { t1 => condition.repo .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } .getOrElse (repos) .map { case (owner, repository) => t1.byRepository(owner, repository) } .foldLeft[Column[Boolean]](false) ( _ || _ ) && - (t1.closed is (condition.state == "closed").bind) && - (t1.milestoneId is condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && - (t1.milestoneId isNull, condition.milestoneId == Some(None)) && - (t1.assignedUserName is filterUser("assigned").bind, filterUser.get("assigned").isDefined) && - (t1.openedUserName is filterUser("created_by").bind, filterUser.get("created_by").isDefined) && - (t1.openedUserName isNot filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && - (t1.pullRequest is true.bind, onlyPullRequest) && + (t1.closed === (condition.state == "closed").bind) && + (t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && + (t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) && + (t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) && + (t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) && + (t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && + (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) && + (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && + (t1.pullRequest === pullRequest.bind) && + // Label filter (IssueLabels filter { t2 => (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.labelId in @@ -179,7 +183,19 @@ (t3.byRepository(t1.userName, t1.repositoryName)) && (t3.labelName inSetBind condition.labels) } map(_.labelId))) - } exists, condition.labels.nonEmpty) + } exists, condition.labels.nonEmpty) && + // Visibility filter + (Repositories filter { t2 => + (t2.byRepository(t1.userName, t1.repositoryName)) && + (t2.isPrivate === (condition.visibility == Some("private")).bind) + } exists, condition.visibility.nonEmpty) && + // Organization (group) filter + (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && + // Mentioned filter + ((t1.openedUserName === filterUser("mentioned").bind) || t1.assignedUserName === filterUser("mentioned").bind || + (IssueComments filter { t2 => + (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === filterUser("mentioned").bind) + } exists), filterUser.get("mentioned").isDefined) } def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], @@ -336,10 +352,21 @@ case class IssueSearchCondition( labels: Set[String] = Set.empty, milestoneId: Option[Option[Int]] = None, + author: Option[String] = None, + assigned: Option[String] = None, repo: Option[String] = None, state: String = "open", sort: String = "created", - direction: String = "desc"){ + direction: String = "desc", + visibility: Option[String] = None, + groups: Set[String] = Set.empty){ + + def isEmpty: Boolean = { + labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty && + state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty + } + + def nonEmpty: Boolean = !isEmpty def toURL: String = "?" + List( @@ -348,10 +375,15 @@ case Some(x) => x.toString case None => "none" })}, + author .map(x => "author=" + urlEncode(x)), + assigned.map(x => "assigned=" + urlEncode(x)), repo.map("for=" + urlEncode(_)), Some("state=" + urlEncode(state)), Some("sort=" + urlEncode(sort)), - Some("direction=" + urlEncode(direction))).flatten.mkString("&") + Some("direction=" + urlEncode(direction)), + visibility.map(x => "visibility=" + urlEncode(x)), + if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(","))) + ).flatten.mkString("&") } @@ -365,14 +397,19 @@ def apply(request: HttpServletRequest): IssueSearchCondition = IssueSearchCondition( param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), - param(request, "milestone").map{ + param(request, "milestone").map { case "none" => None case x => x.toIntOpt }, + param(request, "author"), + param(request, "assigned"), param(request, "for"), param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), - param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) + param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), + param(request, "visibility"), + param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) + ) def page(request: HttpServletRequest) = try { val i = param(request, "page").getOrElse("1").toInt @@ -382,4 +419,6 @@ } } + case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int) + } diff --git a/src/main/scala/service/LabelsService.scala b/src/main/scala/service/LabelsService.scala index 251e5fd..de1dcb8 100644 --- a/src/main/scala/service/LabelsService.scala +++ b/src/main/scala/service/LabelsService.scala @@ -1,7 +1,8 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.Label trait LabelsService { @@ -11,8 +12,8 @@ def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption - def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit = - Labels insert Label( + def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int = + Labels returning Labels.map(_.labelId) += Label( userName = owner, repositoryName = repository, labelName = labelName, diff --git a/src/main/scala/service/MilestonesService.scala b/src/main/scala/service/MilestonesService.scala index 2f73f22..476e0c4 100644 --- a/src/main/scala/service/MilestonesService.scala +++ b/src/main/scala/service/MilestonesService.scala @@ -1,9 +1,10 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.Milestone // TODO [Slick 2.0]NOT import directly? -import model.dateColumnType +import model.Profile.dateColumnType trait MilestonesService { @@ -40,7 +41,7 @@ def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = { val counts = Issues - .filter { t => (t.byRepository(owner, repository)) && (t.milestoneId isNotNull) } + .filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) } .groupBy { t => t.milestoneId -> t.closed } .map { case (t1, t2) => t1._1 -> t1._2 -> t2.length } .toMap diff --git a/src/main/scala/service/PluginService.scala b/src/main/scala/service/PluginService.scala new file mode 100644 index 0000000..d1bb9d8 --- /dev/null +++ b/src/main/scala/service/PluginService.scala @@ -0,0 +1,24 @@ +package service + +import model.Profile._ +import profile.simple._ +import model.Plugin + +trait PluginService { + + def getPlugins()(implicit s: Session): List[Plugin] = + Plugins.sortBy(_.pluginId).list + + def registerPlugin(plugin: Plugin)(implicit s: Session): Unit = + Plugins.insert(plugin) + + def updatePlugin(plugin: Plugin)(implicit s: Session): Unit = + Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version) + + def deletePlugin(pluginId: String)(implicit s: Session): Unit = + Plugins.filter(_.pluginId === pluginId.bind).delete + + def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] = + Plugins.filter(_.pluginId === pluginId.bind).firstOption + +} diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala index c91abe1..9a3239b 100644 --- a/src/main/scala/service/PullRequestService.scala +++ b/src/main/scala/service/PullRequestService.scala @@ -1,7 +1,8 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.{PullRequest, Issue} trait PullRequestService { self: IssuesService => import PullRequestService._ @@ -25,9 +26,9 @@ PullRequests .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .filter { case (t1, t2) => - (t2.closed is closed.bind) && - (t1.userName is owner.get.bind, owner.isDefined) && - (t1.repositoryName is repository.get.bind, repository.isDefined) + (t2.closed === closed.bind) && + (t1.userName === owner.get.bind, owner.isDefined) && + (t1.repositoryName === repository.get.bind, repository.isDefined) } .groupBy { case (t1, t2) => t2.openedUserName } .map { case (userName, t) => userName -> t.length } @@ -35,6 +36,24 @@ .list .map { x => PullRequestCount(x._1, x._2) } +// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] = +// PullRequests +// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } +// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) } +// .filter { case ((t1, t2), t3) => +// (t2.closed === closed.bind) && +// ( +// (t3.isPrivate === false.bind) || +// (t3.userName === userName.bind) || +// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists) +// ) +// } +// .groupBy { case ((t1, t2), t3) => t2.openedUserName } +// .map { case (userName, t) => userName -> t.length } +// .sortBy(_._2 desc) +// .list +// .map { x => PullRequestCount(x._1, x._2) } + def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit = @@ -54,10 +73,10 @@ PullRequests .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } .filter { case (t1, t2) => - (t1.requestUserName is userName.bind) && - (t1.requestRepositoryName is repositoryName.bind) && - (t1.requestBranch is branch.bind) && - (t2.closed is closed.bind) + (t1.requestUserName === userName.bind) && + (t1.requestRepositoryName === repositoryName.bind) && + (t1.requestBranch === branch.bind) && + (t2.closed === closed.bind) } .map { case (t1, t2) => t1 } .list diff --git a/src/main/scala/service/RepositorySearchService.scala b/src/main/scala/service/RepositorySearchService.scala index 292507c..e31d6da 100644 --- a/src/main/scala/service/RepositorySearchService.scala +++ b/src/main/scala/service/RepositorySearchService.scala @@ -7,7 +7,7 @@ import org.eclipse.jgit.treewalk.TreeWalk import org.eclipse.jgit.lib.FileMode import org.eclipse.jgit.api.Git -import model._ +import model.Profile._ import profile.simple._ trait RepositorySearchService { self: IssuesService => @@ -107,7 +107,7 @@ case class SearchResult( files : List[(String, String)], - issues: List[(Issue, Int, String)]) + issues: List[(model.Issue, Int, String)]) case class IssueSearchResult( issueId: Int, diff --git a/src/main/scala/service/RepositoryService.scala b/src/main/scala/service/RepositoryService.scala index 5a9fec6..31e3637 100644 --- a/src/main/scala/service/RepositoryService.scala +++ b/src/main/scala/service/RepositoryService.scala @@ -1,7 +1,8 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.{Repository, Account, Collaborator} import util.JGitUtil trait RepositoryService { self: AccountService => @@ -57,15 +58,15 @@ val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list Repositories.filter { t => - (t.originUserName is oldUserName.bind) && (t.originRepositoryName is oldRepositoryName.bind) + (t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) Repositories.filter { t => - (t.parentUserName is oldUserName.bind) && (t.parentRepositoryName is oldRepositoryName.bind) + (t.parentUserName === oldUserName.bind) && (t.parentRepositoryName === oldRepositoryName.bind) }.map { t => t.originUserName -> t.originRepositoryName }.update(newUserName, newRepositoryName) PullRequests.filter { t => - t.requestRepositoryName is oldRepositoryName.bind + t.requestRepositoryName === oldRepositoryName.bind }.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) deleteRepository(oldUserName, oldRepositoryName) @@ -101,7 +102,7 @@ }.map { t => t.activityId -> t.message }.list updateActivities.foreach { case (activityId, message) => - Activities.filter(_.activityId is activityId.bind).map(_.message).update( + Activities.filter(_.activityId === activityId.bind).map(_.message).update( message .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") .replace(s"[branch:${oldUserName}/${oldRepositoryName}#" ,s"[branch:${newUserName}/${newRepositoryName}#") @@ -135,7 +136,7 @@ * @return the list of repository names */ def getRepositoryNamesOfUser(userName: String)(implicit s: Session): List[String] = - Repositories filter(_.userName is userName.bind) map (_.repositoryName) list + Repositories filter(_.userName === userName.bind) map (_.repositoryName) list /** * Returns the specified repository information. @@ -149,7 +150,7 @@ (Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository => // for getting issue count and pull request count val issues = Issues.filter { t => - t.byRepository(repository.userName, repository.repositoryName) && (t.closed is false.bind) + t.byRepository(repository.userName, repository.repositoryName) && (t.closed === false.bind) }.map(_.pullRequest).list new RepositoryInfo( @@ -165,8 +166,19 @@ } } - def getAllRepositories()(implicit s: Session): List[(String, String)] = { - Repositories.sortBy(_.lastActivityDate desc).map{ t => + /** + * Returns the repositories without private repository that user does not have access right. + * Include public repository, private own repository and private but collaborator repository. + * + * @param userName the user name of collaborator + * @return the repository infomation list + */ + def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = { + Repositories.filter { t1 => + (t1.isPrivate === false.bind) || + (t1.userName === userName.bind) || + (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) + }.sortBy(_.lastActivityDate desc).map{ t => (t.userName, t.repositoryName) }.list } @@ -174,8 +186,8 @@ def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false) (implicit s: Session): List[RepositoryInfo] = { Repositories.filter { t1 => - (t1.userName is userName.bind) || - (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName is userName.bind)} exists) + (t1.userName === userName.bind) || + (Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists) }.sortBy(_.lastActivityDate desc).list.map{ repository => new RepositoryInfo( if(withoutPhysicalInfo){ @@ -211,13 +223,13 @@ case Some(x) if(x.isAdmin) => Repositories // for Normal Users case Some(x) if(!x.isAdmin) => - Repositories filter { t => (t.isPrivate is false.bind) || (t.userName is x.userName) || - (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName is x.userName.bind)} exists) + Repositories filter { t => (t.isPrivate === false.bind) || (t.userName === x.userName) || + (Collaborators.filter { t2 => t2.byRepository(t.userName, t.repositoryName) && (t2.collaboratorName === x.userName.bind)} exists) } // for Guests - case None => Repositories filter(_.isPrivate is false.bind) + case None => Repositories filter(_.isPrivate === false.bind) }).filter { t => - repositoryUserName.map { userName => t.userName is userName.bind } getOrElse LiteralColumn(true) + repositoryUserName.map { userName => t.userName === userName.bind } getOrElse LiteralColumn(true) }.sortBy(_.lastActivityDate desc).list.map{ repository => new RepositoryInfo( if(withoutPhysicalInfo){ @@ -306,13 +318,13 @@ private def getForkedCount(userName: String, repositoryName: String)(implicit s: Session): Int = Query(Repositories.filter { t => - (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) + (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) }.length).first def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[(String, String)] = Repositories.filter { t => - (t.originUserName is userName.bind) && (t.originRepositoryName is repositoryName.bind) + (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) } .sortBy(_.userName asc).map(t => t.userName -> t.repositoryName).list diff --git a/src/main/scala/service/RequestCache.scala b/src/main/scala/service/RequestCache.scala index 34c5ef3..4ff502b 100644 --- a/src/main/scala/service/RequestCache.scala +++ b/src/main/scala/service/RequestCache.scala @@ -1,7 +1,6 @@ package service -import model._ -import slick.jdbc.JdbcBackend +import model.{Account, Issue, Session} import util.Implicits.request2Session /** @@ -12,7 +11,7 @@ */ trait RequestCache extends SystemSettingsService with AccountService with IssuesService { - private implicit def context2Session(implicit context: app.Context): JdbcBackend#Session = + private implicit def context2Session(implicit context: app.Context): Session = request2Session(context.request) def getIssue(userName: String, repositoryName: String, issueId: String) diff --git a/src/main/scala/service/SshKeyService.scala b/src/main/scala/service/SshKeyService.scala index d38804a..4446084 100644 --- a/src/main/scala/service/SshKeyService.scala +++ b/src/main/scala/service/SshKeyService.scala @@ -1,7 +1,8 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.SshKey trait SshKeyService { @@ -9,7 +10,7 @@ SshKeys insert SshKey(userName = userName, title = title, publicKey = publicKey) def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] = - SshKeys.filter(_.userName is userName.bind).sortBy(_.sshKeyId).list + SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit = SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete diff --git a/src/main/scala/service/SystemSettingsService.scala b/src/main/scala/service/SystemSettingsService.scala index 2d8f1d3..9fbd78f 100644 --- a/src/main/scala/service/SystemSettingsService.scala +++ b/src/main/scala/service/SystemSettingsService.scala @@ -191,4 +191,7 @@ else value } + // TODO temporary flag + val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean + } diff --git a/src/main/scala/service/WebHookService.scala b/src/main/scala/service/WebHookService.scala index 9061a07..a2dafbf 100644 --- a/src/main/scala/service/WebHookService.scala +++ b/src/main/scala/service/WebHookService.scala @@ -1,7 +1,8 @@ package service -import model._ +import model.Profile._ import profile.simple._ +import model.{WebHook, Account} import org.slf4j.LoggerFactory import service.RepositoryService.RepositoryInfo import util.JGitUtil @@ -43,7 +44,7 @@ val httpClient = HttpClientBuilder.create.build webHookURLs.foreach { webHookUrl => - val f = future { + val f = Future { logger.debug(s"start web hook invocation for ${webHookUrl}") val httpPost = new HttpPost(webHookUrl.url) @@ -89,15 +90,15 @@ WebHookCommit( id = commit.id, message = commit.fullMessage, - timestamp = commit.time.toString, + timestamp = commit.commitTime.toString, url = commitUrl, added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath }, removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath }, modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath }, author = WebHookUser( - name = commit.committer, - email = commit.mailAddress + name = commit.committerName, + email = commit.committerEmailAddress ) ) }, diff --git a/src/main/scala/service/WikiService.scala b/src/main/scala/service/WikiService.scala index f634e34..add5021 100644 --- a/src/main/scala/service/WikiService.scala +++ b/src/main/scala/service/WikiService.scala @@ -64,7 +64,7 @@ if(!JGitUtil.isEmpty(git)){ JGitUtil.getFileList(git, "master", ".").find(_.name == pageName + ".md").map { file => WikiPageInfo(file.name, StringUtil.convertFromByteArray(git.getRepository.open(file.id).getBytes), - file.committer, file.time, file.commitId) + file.author, file.time, file.commitId) } } else None } @@ -182,7 +182,8 @@ } builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + Constants.HEAD, committer.fullName, committer.mailAddress, pageName match { case Some(x) => s"Revert ${from} ... ${to} on ${x}" case None => s"Revert ${from} ... ${to}" @@ -229,7 +230,8 @@ if(created || updated || removed){ builder.add(JGitUtil.createDirCacheEntry(newPageName + ".md", FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")))) builder.finish() - val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer.fullName, committer.mailAddress, + val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + Constants.HEAD, committer.fullName, committer.mailAddress, if(message.trim.length == 0) { if(removed){ s"Rename ${currentPageName} to ${newPageName}" @@ -269,7 +271,8 @@ } if(removed){ builder.finish() - JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), committer, mailAddress, message) + JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter), + Constants.HEAD, committer, mailAddress, message) } } } diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala index ea82d41..911c05a 100644 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ b/src/main/scala/servlet/AutoUpdateListener.scala @@ -11,6 +11,7 @@ import org.eclipse.jgit.api.Git import util.Directory import plugin.PluginUpdateJob +import service.SystemSettingsService object AutoUpdate { @@ -52,6 +53,30 @@ * The history of versions. A head of this sequence is the current BitBucket version. */ val versions = Seq( + new Version(2, 5), + new Version(2, 4), + new Version(2, 3) { + override def update(conn: Connection): Unit = { + super.update(conn) + using(conn.createStatement.executeQuery("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'")){ rs => + while(rs.next) { + val info = rs.getString("ADDITIONAL_INFO") + val newInfo = info.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n") + if (info != newInfo) { + val id = rs.getString("ACTIVITY_ID") + using(conn.prepareStatement("UPDATE ACTIVITY SET ADDITIONAL_INFO=? WHERE ACTIVITY_ID=?")) { sql => + sql.setString(1, newInfo) + sql.setLong(2, id.toLong) + sql.executeUpdate + } + } + } + } + FileUtils.deleteDirectory(Directory.getPluginCacheDir()) + FileUtils.deleteDirectory(new File(Directory.PluginHome)) + } + }, + new Version(2, 2), new Version(2, 1), new Version(2, 0){ override def update(conn: Connection): Unit = { @@ -146,24 +171,23 @@ */ class AutoUpdateListener extends ServletContextListener { import org.quartz.impl.StdSchedulerFactory - import org.quartz.JobBuilder._ - import org.quartz.TriggerBuilder._ - import org.quartz.SimpleScheduleBuilder._ import AutoUpdate._ private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) private val scheduler = StdSchedulerFactory.getDefaultScheduler override def contextInitialized(event: ServletContextEvent): Unit = { - val datadir = event.getServletContext.getInitParameter("gitbucket.home") - if(datadir != null){ - System.setProperty("gitbucket.home", datadir) + val dataDir = event.getServletContext.getInitParameter("gitbucket.home") + if(dataDir != null){ + System.setProperty("gitbucket.home", dataDir) } org.h2.Driver.load() - event.getServletContext.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true") - logger.debug("Start schema update") + val context = event.getServletContext + context.setInitParameter("db.url", s"jdbc:h2:${DatabaseHome};MVCC=true") + defining(getConnection(event.getServletContext)){ conn => + logger.debug("Start schema update") try { defining(getCurrentVersion()){ currentVersion => if(currentVersion == headVersion){ @@ -173,7 +197,6 @@ } else { versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn)) FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") - conn.commit() logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}") } } @@ -184,17 +207,29 @@ conn.rollback() } } + logger.debug("End schema update") } - logger.debug("End schema update") - logger.debug("Starting plugin system...") - plugin.PluginSystem.init() + if(SystemSettingsService.enablePluginSystem){ + getDatabase(context).withSession { implicit session => + logger.debug("Starting plugin system...") + try { + plugin.PluginSystem.init() - scheduler.start() - PluginUpdateJob.schedule(scheduler) - logger.debug("PluginUpdateJob is started.") + scheduler.start() + PluginUpdateJob.schedule(scheduler) + logger.debug("PluginUpdateJob is started.") - logger.debug("Plugin system is initialized.") + logger.debug("Plugin system is initialized.") + } catch { + case ex: Throwable => { + logger.error("Failed to initialize plugin system", ex) + ex.printStackTrace() + throw ex + } + } + } + } } def contextDestroyed(sce: ServletContextEvent): Unit = { @@ -207,4 +242,10 @@ servletContext.getInitParameter("db.user"), servletContext.getInitParameter("db.password")) + private def getDatabase(servletContext: ServletContext): scala.slick.jdbc.JdbcBackend.Database = + slick.jdbc.JdbcBackend.Database.forURL( + servletContext.getInitParameter("db.url"), + servletContext.getInitParameter("db.user"), + servletContext.getInitParameter("db.password")) + } diff --git a/src/main/scala/servlet/BasicAuthenticationFilter.scala b/src/main/scala/servlet/BasicAuthenticationFilter.scala index 64d85ad..8272c7a 100644 --- a/src/main/scala/servlet/BasicAuthenticationFilter.scala +++ b/src/main/scala/servlet/BasicAuthenticationFilter.scala @@ -5,7 +5,6 @@ import service.{SystemSettingsService, AccountService, RepositoryService} import model._ import org.slf4j.LoggerFactory -import slick.jdbc.JdbcBackend import util.Implicits._ import util.ControlUtil._ import util.Keys @@ -67,7 +66,7 @@ } private def getWritableUser(username: String, password: String, repository: RepositoryService.RepositoryInfo) - (implicit session: JdbcBackend#Session): Option[Account] = + (implicit session: Session): Option[Account] = authenticate(loadSystemSettings(), username, password) match { case x @ Some(account) if(hasWritePermission(repository.owner, repository.name, x)) => x case _ => None diff --git a/src/main/scala/servlet/GitRepositoryServlet.scala b/src/main/scala/servlet/GitRepositoryServlet.scala index 0b19858..012c6ea 100644 --- a/src/main/scala/servlet/GitRepositoryServlet.scala +++ b/src/main/scala/servlet/GitRepositoryServlet.scala @@ -17,7 +17,7 @@ import org.eclipse.jgit.api.Git import util.JGitUtil.CommitInfo import service.IssuesService.IssueSearchCondition -import slick.jdbc.JdbcBackend +import model.Session /** * Provides Git repository via HTTP. @@ -95,7 +95,7 @@ import scala.collection.JavaConverters._ -class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: JdbcBackend#Session) +class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session) extends PostReceiveHook with PreReceiveHook with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService { @@ -205,7 +205,7 @@ private def createIssueComment(commit: CommitInfo) = { StringUtil.extractIssueId(commit.fullMessage).foreach { issueId => if(getIssue(owner, repository, issueId).isDefined){ - getAccountByMailAddress(commit.mailAddress).foreach { account => + getAccountByMailAddress(commit.committerEmailAddress).foreach { account => createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit") } } diff --git a/src/main/scala/servlet/PluginActionInvokeFilter.scala b/src/main/scala/servlet/PluginActionInvokeFilter.scala index 6a35121..b74ac73 100644 --- a/src/main/scala/servlet/PluginActionInvokeFilter.scala +++ b/src/main/scala/servlet/PluginActionInvokeFilter.scala @@ -3,11 +3,13 @@ import javax.servlet._ import javax.servlet.http.{HttpServletResponse, HttpServletRequest} import org.apache.commons.io.IOUtils -import twirl.api.Html +import play.twirl.api.Html import service.{AccountService, RepositoryService, SystemSettingsService} -import model.Account +import model.{Account, Session} import util.{JGitUtil, Keys} -import plugin.PluginConnectionHolder +import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect} +import service.RepositoryService.RepositoryInfo +import plugin.Security._ class PluginActionInvokeFilter extends Filter with SystemSettingsService with RepositoryService with AccountService { @@ -19,7 +21,7 @@ (req, res) match { case (request: HttpServletRequest, response: HttpServletResponse) => { Database(req.getServletContext) withTransaction { implicit session => - val path = req.asInstanceOf[HttpServletRequest].getRequestURI + val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length) if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){ chain.doFilter(req, res) } @@ -28,56 +30,54 @@ } } - private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse): Boolean = { - plugin.PluginSystem.globalActions.find(_.path == path).map { action => - val result = action.function(request, response) - result match { - case x: String => { - response.setContentType("text/html; charset=UTF-8") - val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] - implicit val context = app.Context(loadSystemSettings(), Option(loginAccount), request) - val html = _root_.html.main("GitBucket", None)(Html(x)) - IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream) - } - case x => { - // TODO returns as JSON? - response.setContentType("application/json; charset=UTF-8") + private def processGlobalAction(path: String, request: HttpServletRequest, response: HttpServletResponse) + (implicit session: Session): Boolean = { + plugin.PluginSystem.globalActions.find(x => + x.method.toLowerCase == request.getMethod.toLowerCase && path.matches(x.path) + ).map { action => + val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] + val systemSettings = loadSystemSettings() + implicit val context = app.Context(systemSettings, Option(loginAccount), request) + if(authenticate(action.security, context)){ + val result = try { + PluginConnectionHolder.threadLocal.set(session.conn) + action.function(request, response, context) + } finally { + PluginConnectionHolder.threadLocal.remove() } + processActionResult(result, request, response, context) + } else { + // TODO NotFound or Error? } true } getOrElse false } private def processRepositoryAction(path: String, request: HttpServletRequest, response: HttpServletResponse) - (implicit session: model.profile.simple.Session): Boolean = { + (implicit session: Session): Boolean = { val elements = path.split("/") if(elements.length > 3){ val owner = elements(1) val name = elements(2) val remain = elements.drop(3).mkString("/", "/", "") - val systemSettings = loadSystemSettings() - getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository => - plugin.PluginSystem.repositoryActions.find(_.path == remain).map { action => - val result = try { - PluginConnectionHolder.threadLocal.set(session.conn) - action.function(request, response, repository) - } finally { - PluginConnectionHolder.threadLocal.remove() - } - result match { - case x: String => { - response.setContentType("text/html; charset=UTF-8") - val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] - implicit val context = app.Context(systemSettings, Option(loginAccount), request) - val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(x))) // TODO specify active side menu - IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream) - } - case x => { - // TODO returns as JSON? - response.setContentType("application/json; charset=UTF-8") + val loginAccount = request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] + val systemSettings = loadSystemSettings() + implicit val context = app.Context(systemSettings, Option(loginAccount), request) + + getRepository(owner, name, systemSettings.baseUrl(request)).flatMap { repository => + plugin.PluginSystem.repositoryActions.find(x => remain.matches(x.path)).map { action => + if(authenticate(action.security, context, repository)){ + val result = try { + PluginConnectionHolder.threadLocal.set(session.conn) + action.function(request, response, context, repository) + } finally { + PluginConnectionHolder.threadLocal.remove() } + processActionResult(result, request, response, context) + } else { + // TODO NotFound or Error? } true } @@ -85,4 +85,108 @@ } else false } + private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse, + context: app.Context): Unit = { + result match { + case null|None => renderError(request, response, context, 404) + case x: String => renderGlobalHtml(request, response, context, x) + case Some(x: String) => renderGlobalHtml(request, response, context, x) + case x: Html => renderGlobalHtml(request, response, context, x.toString) + case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString) + case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString) + case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString) + case x: RawData => renderRawData(request, response, context, x) + case Some(x: RawData) => renderRawData(request, response, context, x) + case x: Redirect => response.sendRedirect(x.path) + case Some(x: Redirect) => response.sendRedirect(x.path) + case x: AnyRef => renderJson(request, response, x) + } + } + + /** + * Authentication for global action + */ + private def authenticate(security: Security, context: app.Context)(implicit session: Session): Boolean = { + // Global Action + security match { + case All() => true + case Login() => context.loginAccount.isDefined + case Admin() => context.loginAccount.exists(_.isAdmin) + case _ => false // TODO throw Exception? + } + } + + /** + * Authenticate for repository action + */ + private def authenticate(security: Security, context: app.Context, repository: RepositoryInfo)(implicit session: Session): Boolean = { + if(repository.repository.isPrivate){ + // Private Repository + security match { + case Admin() => context.loginAccount.exists(_.isAdmin) + case Owner() => context.loginAccount.exists { account => + account.userName == repository.owner || + getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager) + } + case _ => context.loginAccount.exists { account => + account.isAdmin || account.userName == repository.owner || + getCollaborators(repository.owner, repository.name).contains(account.userName) + } + } + } else { + // Public Repository + security match { + case All() => true + case Login() => context.loginAccount.isDefined + case Owner() => context.loginAccount.exists { account => + account.userName == repository.owner || + getGroupMembers(repository.owner).exists(m => m.userName == account.userName && m.isManager) + } + case Member() => context.loginAccount.exists { account => + account.userName == repository.owner || + getCollaborators(repository.owner, repository.name).contains(account.userName) + } + case Admin() => context.loginAccount.exists(_.isAdmin) + } + } + } + + private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = { + response.sendError(error) + } + + private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = { + response.setContentType("text/html; charset=UTF-8") + val html = _root_.html.main("GitBucket", None)(Html(body))(context) + IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream) + } + + private def renderRepositoryHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, repository: RepositoryInfo, body: String): Unit = { + response.setContentType("text/html; charset=UTF-8") + val html = _root_.html.main("GitBucket", None)(_root_.html.menu("", repository)(Html(body))(context))(context) // TODO specify active side menu + IOUtils.write(html.toString.getBytes("UTF-8"), response.getOutputStream) + } + + private def renderFragmentHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = { + response.setContentType("text/html; charset=UTF-8") + IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream) + } + + private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = { + response.setContentType(rawData.contentType) + IOUtils.write(rawData.content, response.getOutputStream) + } + + private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = { + import org.json4s._ + import org.json4s.jackson.Serialization + import org.json4s.jackson.Serialization.write + implicit val formats = Serialization.formats(NoTypeHints) + + val json = write(obj) + + response.setContentType("application/json; charset=UTF-8") + IOUtils.write(json.getBytes("UTF-8"), response.getOutputStream) + } + } diff --git a/src/main/scala/ssh/GitCommand.scala b/src/main/scala/ssh/GitCommand.scala index eeb269e..d1d4233 100644 --- a/src/main/scala/ssh/GitCommand.scala +++ b/src/main/scala/ssh/GitCommand.scala @@ -12,7 +12,7 @@ import service.{AccountService, RepositoryService, SystemSettingsService} import org.eclipse.jgit.errors.RepositoryNotFoundException import javax.servlet.ServletContext -import model.profile.simple.Session +import model.Session object GitCommand { val CommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index f4ced75..5ea43a3 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -4,6 +4,7 @@ import util.Directory._ import util.StringUtil._ import util.ControlUtil._ +import scala.annotation.tailrec import scala.collection.JavaConverters._ import org.eclipse.jgit.lib._ import org.eclipse.jgit.revwalk._ @@ -13,7 +14,7 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import java.util.Date -import org.eclipse.jgit.api.errors.NoHeadException +import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException} import service.RepositoryService import org.eclipse.jgit.dircache.DirCacheEntry import org.slf4j.LoggerFactory @@ -47,38 +48,45 @@ * @param id the object id * @param isDirectory whether is it directory * @param name the file (or directory) name - * @param time the last modified time * @param message the last commit message * @param commitId the last commit id - * @param committer the last committer name + * @param time the last modified time + * @param author the last committer name * @param mailAddress the committer's mail address * @param linkUrl the url of submodule */ - case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, time: Date, message: String, commitId: String, - committer: String, mailAddress: String, linkUrl: Option[String]) + case class FileInfo(id: ObjectId, isDirectory: Boolean, name: String, message: String, commitId: String, + time: Date, author: String, mailAddress: String, linkUrl: Option[String]) /** * The commit data. * * @param id the commit id - * @param time the commit time - * @param committer the committer name - * @param mailAddress the mail address of the committer * @param shortMessage the short message * @param fullMessage the full message * @param parents the list of parent commit id + * @param authorTime the author time + * @param authorName the author name + * @param authorEmailAddress the mail address of the author + * @param commitTime the commit time + * @param committerName the committer name + * @param committerEmailAddress the mail address of the committer */ - case class CommitInfo(id: String, time: Date, committer: String, mailAddress: String, - shortMessage: String, fullMessage: String, parents: List[String]){ + case class CommitInfo(id: String, shortMessage: String, fullMessage: String, parents: List[String], + authorTime: Date, authorName: String, authorEmailAddress: String, + commitTime: Date, committerName: String, committerEmailAddress: String){ def this(rev: org.eclipse.jgit.revwalk.RevCommit) = this( rev.getName, - rev.getCommitterIdent.getWhen, - rev.getCommitterIdent.getName, - rev.getCommitterIdent.getEmailAddress, rev.getShortMessage, rev.getFullMessage, - rev.getParents().map(_.name).toList) + rev.getParents().map(_.name).toList, + rev.getAuthorIdent.getWhen, + rev.getAuthorIdent.getName, + rev.getAuthorIdent.getEmailAddress, + rev.getCommitterIdent.getWhen, + rev.getCommitterIdent.getName, + rev.getCommitterIdent.getEmailAddress) val summary = getSummaryMessage(fullMessage, shortMessage) @@ -87,6 +95,8 @@ Some(fullMessage.trim.substring(i).trim) } else None } + + def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress } case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String]) @@ -181,38 +191,23 @@ * @return HTML of the file list */ def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = { - val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] + var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] using(new RevWalk(git.getRepository)){ revWalk => val objectId = git.getRepository.resolve(revision) val revCommit = revWalk.parseCommit(objectId) - using(new TreeWalk(git.getRepository)){ treeWalk => + val treeWalk = if (path == ".") { + val treeWalk = new TreeWalk(git.getRepository) treeWalk.addTree(revCommit.getTree) - if(path != "."){ - treeWalk.setRecursive(true) - treeWalk.setFilter(new TreeFilter(){ + treeWalk + } else { + val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree) + treeWalk.enterSubtree() + treeWalk + } - var stopRecursive = false - - def include(walker: TreeWalk): Boolean = { - val targetPath = walker.getPathString - if((path + "/").startsWith(targetPath)){ - true - } else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){ - stopRecursive = true - treeWalk.setRecursive(false) - true - } else { - false - } - } - - def shouldBeRecursive(): Boolean = !stopRecursive - - override def clone: TreeFilter = return this - }) - } + using(treeWalk) { treeWalk => while (treeWalk.next()) { // submodule val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){ @@ -221,6 +216,31 @@ list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl)) } + + list = list.map(tuple => + if (tuple._2 != FileMode.TREE) + tuple + else + simplifyPath(tuple) + ) + + @tailrec + def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = { + val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])] + using(new TreeWalk(git.getRepository)) { walk => + walk.addTree(tuple._1) + while (walk.next() && list.size < 2) { + val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) { + getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url) + } else None + list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl)) + } + } + if (list.size != 1 || list.exists(_._2 != FileMode.TREE)) + tuple + else + simplifyPath(list(0)) + } } } @@ -231,11 +251,11 @@ objectId, fileMode == FileMode.TREE || fileMode == FileMode.GITLINK, name, - commit.getCommitterIdent.getWhen, getSummaryMessage(commit.getFullMessage, commit.getShortMessage), commit.getName, - commit.getCommitterIdent.getName, - commit.getCommitterIdent.getEmailAddress, + commit.getAuthorIdent.getWhen, + commit.getAuthorIdent.getName, + commit.getAuthorIdent.getEmailAddress, linkUrl) } }.sortWith { (file1, file2) => @@ -487,6 +507,17 @@ }.find(_._1 != null) } + def createBranch(git: Git, fromBranch: String, newBranch: String) = { + try { + git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call() + Right("Branch created.") + } catch { + case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.") + // JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists. + case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.") + } + } + def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = { val entry = new DirCacheEntry(path) entry.setFileMode(mode) @@ -495,7 +526,7 @@ } def createNewCommit(git: Git, inserter: ObjectInserter, headId: AnyObjectId, treeId: AnyObjectId, - fullName: String, mailAddress: String, message: String): ObjectId = { + ref: String, fullName: String, mailAddress: String, message: String): ObjectId = { val newCommit = new CommitBuilder() newCommit.setCommitter(new PersonIdent(fullName, mailAddress)) newCommit.setAuthor(new PersonIdent(fullName, mailAddress)) @@ -509,7 +540,7 @@ inserter.flush() inserter.release() - val refUpdate = git.getRepository.updateRef(Constants.HEAD) + val refUpdate = git.getRepository.updateRef(ref) refUpdate.setNewObjectId(newHeadId) refUpdate.update() @@ -643,4 +674,15 @@ }.head.id } + /** + * Returns the last modified commit of specified path + * @param git the Git object + * @param startCommit the search base commit id + * @param path the path of target file or directory + * @return the last modified commit of specified path + */ + def getLastModifiedCommit(git: Git, startCommit: RevCommit, path: String): RevCommit = { + return git.log.add(startCommit).addPath(path).setMaxCount(1).call.iterator.next + } + } diff --git a/src/main/scala/util/Notifier.scala b/src/main/scala/util/Notifier.scala index afc65aa..95ec6bf 100644 --- a/src/main/scala/util/Notifier.scala +++ b/src/main/scala/util/Notifier.scala @@ -6,11 +6,11 @@ import org.slf4j.LoggerFactory import app.Context +import model.Session import service.{AccountService, RepositoryService, IssuesService, SystemSettingsService} import servlet.Database import SystemSettingsService.Smtp import _root_.util.ControlUtil.defining -import model.profile.simple.Session trait Notifier extends RepositoryService with AccountService with IssuesService { def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String) @@ -69,7 +69,7 @@ (msg: String => String)(implicit context: Context) = { val database = Database(context.request.getServletContext) - val f = future { + val f = Future { database withSession { implicit session => getIssue(r.owner, r.name, issueId.toString) foreach { issue => defining( diff --git a/src/main/scala/view/AvatarImageProvider.scala b/src/main/scala/view/AvatarImageProvider.scala index b41023f..8e610af 100644 --- a/src/main/scala/view/AvatarImageProvider.scala +++ b/src/main/scala/view/AvatarImageProvider.scala @@ -1,7 +1,7 @@ package view import service.RequestCache -import twirl.api.Html +import play.twirl.api.Html import util.StringUtil trait AvatarImageProvider { self: RequestCache => diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index 8a50865..08c3dd4 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -9,6 +9,7 @@ import org.pegdown.LinkRenderer.Rendering import java.text.Normalizer import java.util.Locale +import java.util.regex.Pattern import scala.collection.JavaConverters._ import service.{RequestCache, WikiService} @@ -18,17 +19,23 @@ * Converts Markdown of Wiki pages to HTML. */ def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { + enableWikiLink: Boolean, enableRefsLink: Boolean, + enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = { // escape issue id - val source = if(enableRefsLink){ + val s = if(enableRefsLink){ markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") } else markdown + // escape task list + val source = if(enableTaskList){ + GitBucketHtmlSerializer.escapeTaskList(s) + } else s + val rootNode = new PegDownProcessor( Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS ).parseMarkdown(source.toCharArray) - new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) + new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode) } } @@ -82,15 +89,18 @@ markdown: String, repository: service.RepositoryService.RepositoryInfo, enableWikiLink: Boolean, - enableRefsLink: Boolean + enableRefsLink: Boolean, + enableTaskList: Boolean, + hasWritePermission: Boolean )(implicit val context: app.Context) extends ToHtmlSerializer( new GitBucketLinkRender(context, repository, enableWikiLink), Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava ) with LinkConverter with RequestCache { - override protected def printImageTag(imageNode: SuperNode, url: String): Unit = - printer.print("") - .print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") + override protected def printImageTag(imageNode: SuperNode, url: String): Unit = { + printer.print("") + .print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") + } override protected def printLink(rendering: LinkRenderer.Rendering): Unit = { printer.print('<').print('a') @@ -101,9 +111,21 @@ printer.print('>').print(rendering.text).print("") } - private def fixUrl(url: String): String = { - if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){ + private def fixUrl(url: String, isImage: Boolean = false): String = { + if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){ url + } else if(!enableWikiLink){ + if(context.currentPath.contains("/blob/")){ + url + (if(isImage) "?raw=true" else "") + } else if(context.currentPath.contains("/tree/")){ + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } else { + val paths = context.currentPath.split("/") + val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch + repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") + } } else { repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url } @@ -130,7 +152,10 @@ override def visit(node: TextNode): Unit = { // convert commit id and username to link. - val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + + // convert task list to checkbox. + val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t if (abbreviations.isEmpty) { printer.print(text) @@ -138,6 +163,28 @@ printWithAbbreviations(text) } } + + override def visit(node: BulletListNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println().print("""
    """).indent(+2) + visitChildren(node) + printer.indent(-2).println().print("
") + } else { + printIndentedTag(node, "ul") + } + } + + override def visit(node: ListItemNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println() + printer.print("""
  • """) + visitChildren(node) + printer.print("
  • ") + } else { + printer.println() + printTag(node, "li") + } + } } object GitBucketHtmlSerializer { @@ -150,4 +197,14 @@ val noSpecialChars = StringUtil.urlEncode(normalized) noSpecialChars.toLowerCase(Locale.ENGLISH) } + + def escapeTaskList(text: String): String = { + Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") + } + + def convertCheckBox(text: String, hasWritePermission: Boolean): String = { + val disabled = if (hasWritePermission) "" else "disabled" + text.replaceAll("task:x:", """") + .replaceAll("task: :", """") + } } diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index ddf4a0a..19dd2e8 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -1,7 +1,7 @@ package view -import java.util.Date +import java.util.{Locale, Date, TimeZone} import java.text.SimpleDateFormat -import twirl.api.Html +import play.twirl.api.Html import util.StringUtil import service.RequestCache @@ -15,10 +15,55 @@ */ def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + val timeUnits = List( + (1000L, "second"), + (1000L * 60, "minute"), + (1000L * 60 * 60, "hour"), + (1000L * 60 * 60 * 24, "day"), + (1000L * 60 * 60 * 24 * 30, "month"), + (1000L * 60 * 60 * 24 * 365, "year") + ).reverse + + /** + * Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago" + */ + def datetimeAgo(date: Date): String = { + val duration = new Date().getTime - date.getTime + timeUnits.find(tuple => duration / tuple._1 > 0) match { + case Some((unitValue, unitString)) => + val value = duration / unitValue + s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + case None => "just now" + } + } + + /** + * + * Format java.util.Date to "x {seconds/minutes/hours/days} ago" + * If duration over 1 month, format to "d MMM (yyyy)" + * + */ + def datetimeAgoRecentOnly(date: Date): String = { + val duration = new Date().getTime - date.getTime + timeUnits.find(tuple => duration / tuple._1 > 0) match { + case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}" + case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}" + case Some((unitValue, unitString)) => + val value = duration / unitValue + s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + case None => "just now" + } + } + + /** * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". */ - def datetimeRFC3339(date: Date): String = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'").format(date).replaceAll("(\\d\\d)(\\d\\d)$","$1:$2") + def datetimeRFC3339(date: Date): String = { + val sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + sf.setTimeZone(TimeZone.getTimeZone("UTC")) + sf.format(date) + } /** * Format java.util.Date to "yyyy-MM-dd". @@ -44,8 +89,8 @@ * Converts Markdown of Wiki pages to HTML. */ def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = - Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) + enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html = + Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission)) def renderMarkup(filePath: List[String], fileContent: String, branch: String, repository: service.RepositoryService.RepositoryInfo, @@ -74,7 +119,7 @@ * This method looks up Gravatar if avatar icon has not been configured in user settings. */ def avatar(commit: util.JGitUtil.CommitInfo, size: Int)(implicit context: app.Context): Html = - getAvatarImageHtml(commit.committer, size, commit.mailAddress) + getAvatarImageHtml(commit.authorName, size, commit.authorEmailAddress) /** * Converts commit id, issue id and username to the link. diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html index 1ad52df..8a3706b 100644 --- a/src/main/twirl/account/repositories.scala.html +++ b/src/main/twirl/account/repositories.scala.html @@ -25,7 +25,7 @@ @if(repository.repository.description.isDefined){
    @repository.repository.description
    } -
    Last updated: @datetime(repository.repository.lastActivityDate)
    +
    Updated @helper.html.datetimeago(repository.repository.lastActivityDate)
    } diff --git a/src/main/twirl/admin/menu.scala.html b/src/main/twirl/admin/menu.scala.html index 25cda19..1e48858 100644 --- a/src/main/twirl/admin/menu.scala.html +++ b/src/main/twirl/admin/menu.scala.html @@ -11,9 +11,11 @@ System Settings - - Plugins - + @if(service.SystemSettingsService.enablePluginSystem){ + + Plugins + + }
  • H2 Console
  • diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html index fb022c0..1f25857 100644 --- a/src/main/twirl/admin/users/user.scala.html +++ b/src/main/twirl/admin/users/user.scala.html @@ -16,6 +16,9 @@ Disable +
    + +
    } @if(account.map(_.password.nonEmpty).getOrElse(true)){ diff --git a/src/main/twirl/dashboard/header.scala.html b/src/main/twirl/dashboard/header.scala.html new file mode 100644 index 0000000..01c5bfc --- /dev/null +++ b/src/main/twirl/dashboard/header.scala.html @@ -0,0 +1,74 @@ +@(openCount: Int, + closedCount: Int, + condition: service.IssuesService.IssueSearchCondition, + groups: List[String])(implicit context: app.Context) +@import context._ +@import view.helpers._ + + + + @openCount Open +    + + + @closedCount Closed + + + \ No newline at end of file diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html index 0329e46..16e3564 100644 --- a/src/main/twirl/dashboard/issues.scala.html +++ b/src/main/twirl/dashboard/issues.scala.html @@ -1,50 +1,15 @@ -@(listparts: twirl.api.Html, - allCount: Int, - assignedCount: Int, - createdByCount: Int, - repositories: List[(String, String, Int)], +@(issues: List[service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - filter: String)(implicit context: app.Context) + filter: String, + groups: List[String])(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main("Your Issues"){ -
    +@html.main("Issues"){ @dashboard.html.tab("issues") -
    - - @listparts +
    + @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    -
    } diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html new file mode 100644 index 0000000..79c5517 --- /dev/null +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -0,0 +1,65 @@ +@(issues: List[service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, + condition: service.IssuesService.IssueSearchCondition, + filter: String, + groups: List[String])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@import service.IssuesService.IssueInfo + + + + + + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => + + + + } +
    + @dashboard.html.header(openCount, closedCount, condition, groups) +
    + @if(issue.isPullRequest){ + + } else { + + } + @issue.repositoryName ・ + @if(issue.isPullRequest){ + @issue.title + } else { + @issue.title + } + @labels.map { label => + @label.labelName + } + + @issue.assignedUserName.map { userName => + @avatar(userName, 20, tooltip = true) + } + @if(commentCount > 0){ + + @commentCount + + } else { + + @commentCount + + } + +
    + #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) + @milestone.map { milestone => + @milestone + } +
    +
    +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) +
    diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html index b6c75f9..614dc9a 100644 --- a/src/main/twirl/dashboard/pulls.scala.html +++ b/src/main/twirl/dashboard/pulls.scala.html @@ -1,42 +1,15 @@ -@(listparts: twirl.api.Html, - counts: List[service.PullRequestService.PullRequestCount], - repositories: List[(String, String, Int)], +@(issues: List[service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - filter: String)(implicit context: app.Context) + filter: String, + groups: List[String])(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main("Your Issues"){ -
    +@html.main("Pull Requests"){ @dashboard.html.tab("pulls") -
    - - @listparts +
    + @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    -
    } diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html new file mode 100644 index 0000000..fbddac5 --- /dev/null +++ b/src/main/twirl/dashboard/pullslist.scala.html @@ -0,0 +1,47 @@ +@(issues: List[service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, + condition: service.IssuesService.IssueSearchCondition, + filter: String, + groups: List[String])(implicit context: app.Context) +@import context._ +@import view.helpers._ +@import service.IssuesService.IssueInfo + + + + + + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => + + + + } +
    + @dashboard.html.header(openCount, closedCount, condition, groups) +
    + + @issue.title + #@issue.issueId +
    + @issue.content.map { content => + @cut(content, 90) + }.getOrElse { + No description available + } +
    +
    + @avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)  + @if(commentCount > 0){ + @commentCount @plural(commentCount, "comment") + } +
    +
    +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL) +
    diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html index 1a43265..aca044f 100644 --- a/src/main/twirl/dashboard/tab.scala.html +++ b/src/main/twirl/dashboard/tab.scala.html @@ -1,13 +1,47 @@ @(active: String = "")(implicit context: app.Context) @import context._ @import view.helpers._ - +
    +
    + + + News Feed + + @if(loginAccount.isDefined){ + + + Pull Requests + + + + Issues + + } +
    +
    + \ No newline at end of file diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html index 980eaf5..9bb3f8d 100644 --- a/src/main/twirl/helper/activities.scala.html +++ b/src/main/twirl/helper/activities.scala.html @@ -62,7 +62,7 @@ @detailActivity(activity: model.Activity, image: String) = {
    -
    @datetime(activity.activityDate)
    +
    @helper.html.datetimeago(activity.activityDate)
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) @@ -76,7 +76,7 @@ @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
    -
    @datetime(activity.activityDate)
    +
    @helper.html.datetimeago(activity.activityDate)
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) @@ -91,7 +91,7 @@
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) - @datetime(activity.activityDate) + @helper.html.datetimeago(activity.activityDate)
    } diff --git a/src/main/twirl/helper/branchcontrol.scala.html b/src/main/twirl/helper/branchcontrol.scala.html new file mode 100644 index 0000000..5381688 --- /dev/null +++ b/src/main/twirl/helper/branchcontrol.scala.html @@ -0,0 +1,62 @@ +@(branch: String = "", + repository: service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(body: Html)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@helper.html.dropdown( + value = if(branch.length == 40) branch.substring(0, 10) else branch, + prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", + mini = true +) { +
  • Switch branches
  • +
  • + @body + @if(hasWritePermission) { + + } +} + diff --git a/src/main/twirl/helper/copy.scala.html b/src/main/twirl/helper/copy.scala.html index b8f99c0..7a09a52 100644 --- a/src/main/twirl/helper/copy.scala.html +++ b/src/main/twirl/helper/copy.scala.html @@ -6,6 +6,24 @@ \ No newline at end of file + diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 1ffbae0..b351a1d 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -5,20 +5,36 @@ pullreq: Option[model.PullRequest] = None)(implicit context: app.Context) @import context._ @import view.helpers._ +
    @avatar(issue.openedUserName, 48)
    +
    +
    + @user(issue.openedUserName, styleClass="username strong") commented @helper.html.datetimeago(issue.registeredDate) + + @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ + + } + +
    +
    + @markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission) +
    +
    + @comments.map { comment => @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
    @avatar(comment.commentedUserName, 48)
    - @user(comment.commentedUserName, styleClass="username strong") - @if(comment.action == "comment"){ - commented - } else { - @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } - } + + @if(comment.action == "comment"){ + commented + } else { + @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } + } + @helper.html.datetimeago(comment.registeredDate) + - @datetime(comment.registeredDate) @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){   @@ -30,7 +46,7 @@ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @defining(comment.content.substring(comment.content.length - 40)){ id => - @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) + @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission) } } else { @if(comment.action == "refer"){ @@ -38,7 +54,7 @@ Issue #@issueId: @rest.mkString(":") } } else { - @markdown(comment.content, repository, false, true) + @markdown(comment.content, repository, false, true, true, hasWritePermission) } }
    @@ -54,7 +70,7 @@ } else { @pullreq.map(_.userName):@pullreq.map(_.branch) to @pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch) } - @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "close" || comment.action == "close_comment"){ @@ -62,9 +78,9 @@ Closed @avatar(comment.commentedUserName, 20) @if(issue.isPullRequest){ - @user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate) } else { - @user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate) }
    } @@ -72,27 +88,36 @@
    Reopened @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "delete_branch"){
    Deleted @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @helper.html.datetimeago(comment.registeredDate)
    } } \ No newline at end of file + diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index 95c5091..7080667 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -7,7 +7,8 @@ @import view.helpers._ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.menu("issues", repository){ - @tab("", true, repository) + @tab("issues", false, repository) +


    @@ -65,7 +66,7 @@
    @if(hasWritePermission){ - Add Labels + Labels