diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b7d5dc9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @takezoe @xuwei-k diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..d5bae3f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: build + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + java: [11] + steps: + - uses: actions/checkout@v5 + - name: Cache + uses: actions/cache@v4 + env: + cache-name: cache-sbt-libs + with: + path: | + ~/.ivy2/cache + ~/.sbt + ~/.coursier + key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }} + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + - uses: sbt/setup-sbt@v1 + - name: Run tests + run: | + git clone https://github.com/gitbucket/gitbucket.git + cd gitbucket + sbt publishLocal + cd ../ + sbt test + - name: Assembly + run: sbt assembly + - name: Upload artifacts + uses: actions/upload-artifact@v5 + with: + name: gitbucket-gist-plugin-java${{ matrix.java }}-${{ github.sha }} + path: ./target/scala-2.13/*.jar + + diff --git a/.gitignore b/.gitignore index 938d6b5..ae82060 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ lib_managed/ src_managed/ project/boot/ project/plugins/project/ +.bsp/ # Scala-IDE specific .scala_dependencies @@ -22,4 +23,11 @@ project/plugins/project/ # Ensime .ensime -.ensime_cache/ \ No newline at end of file +.ensime_cache/ + +# Metals +.bloop/ +.metals/ +.vscode/ +**/metals.sbt + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index ad1e835..2f9d5fa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,50 @@ -# gitbucket-gist-plugin +# gitbucket-gist-plugin [![build](https://github.com/gitbucket/gitbucket-gist-plugin/workflows/build/badge.svg?branch=master)](https://github.com/gitbucket/gitbucket-gist-plugin/actions?query=workflow%3Abuild+branch%3Amaster) -This is an example of GitBucket plug-in. This plug-in provides code snippet repository like Gist. +This is a GitBucket plug-in which provides code snippet repository like Gist. -## Instllation +| Plugin version | GitBucket version | +|:---------------|:------------------| +| 4.23.x | 4.40.x - | +| 4.22.x | 4.37.x - | +| 4.21.x | 4.36.x - | +| 4.20.x | 4.35.x - | +| 4.19.x | 4.34.x - | +| 4.18.x | 4.32.x - 4.33.x | +| 4.17.x | 4.30.x - 4.31.x | +| 4.16.x | 4.26.x - 4.29.x | +| 4.15.x | 4.25.x | +| 4.13.x, 4.14.x | 4.23.x - 4.24.x | +| 4.12.x | 4.21.x - 4.22.x | +| 4.11.x | 4.19.x - 4.20.x | +| 4.10.x | 4.15.x - 4.18.x | +| 4.9.x | 4.14.x | +| 4.8.x | 4.11.x - 4.13.x | +| 4.7.x | 4.11.x | +| 4.6.x | 4.10.x | +| 4.5.x | 4.9.x | +| 4.4.x | 4.8.x | +| 4.2.x, 4.3.x | 4.2.x - 4.7.x | +| 4.0.x | 4.0.x - 4.1.x | +| 3.13.x | 3.13.x | +| 3.12.x | 3.12.x | +| 3.11.x | 3.11.x | +| 3.10.x | 3.10.x | +| 3.7.x | 3.7.x - 3.9.x | +| 3.6.x | 3.6.x | -1. Hit `./sbt.sh package` in the root directory of this repository. -2. Copy `target/scala-2.11/gitbucket-gist-plugin_2.11-1.3.jar` into `GITBUCKET_HOME/plugins`. -3. Restart GitBucket. +## Installation + +Download jar file from [Releases page](https://github.com/gitbucket/gitbucket-gist-plugin/releases) and put into `GITBUCKET_HOME/plugins`. + +**Note:** If you had used this plugin with GitBucket 3.x, it does not work after upgrading to GitBucket 4.x. Solution is below: + +1. `UPDATE VERSIONS SET VERSION='2.0.0' WHERE MODULE_ID='gist';` +2. restart gitbucket +3. can open snippets page +4. `SELECT VERSION FROM VERSIONS WHERE MODULE_ID='gist'` -> `4.2.0` + +See [Connect to H2 database](https://github.com/gitbucket/gitbucket/wiki/Connect-to-H2-database) to know how to execute SQL on the GitBucket database. + +## Build from source + +Run `sbt assembly` and copy generated `/target/scala-2.13/gitbucket-gist-plugin-x.x.x.jar` to `~/.gitbucket/plugins/` (If the directory does not exist, create it by hand before copying the jar), or just run `sbt install`. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..49c6ffd --- /dev/null +++ b/build.sbt @@ -0,0 +1,10 @@ +organization := "io.github.gitbucket" +name := "gitbucket-gist-plugin" +version := "4.23.0" +scalaVersion := "2.13.18" +gitbucketVersion := "4.44.0" + +scalacOptions := Seq("-deprecation", "-feature", "-language:postfixOps") +Compile / javacOptions ++= Seq("-target", "11", "-source", "11") + +useJCenter := true diff --git a/project/build.properties b/project/build.properties index 858f009..a360cca 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version = 0.13.5 \ No newline at end of file +sbt.version = 1.11.7 diff --git a/project/build.scala b/project/build.scala deleted file mode 100755 index 1dc48bb..0000000 --- a/project/build.scala +++ /dev/null @@ -1,35 +0,0 @@ -import sbt._ -import Keys._ -import play.twirl.sbt.SbtTwirl -import play.twirl.sbt.Import.TwirlKeys._ - -object MyBuild extends Build { - - val Organization = "jp.sf.amateras" - val Name = "gitbucket-gist-plugin" - val Version = "1.3" - val ScalaVersion = "2.11.6" - - lazy val project = Project ( - "gitbucket-gist-plugin", - file(".") - ) - .settings( - sourcesInBase := false, - organization := Organization, - name := Name, - version := Version, - scalaVersion := ScalaVersion, - scalacOptions := Seq("-deprecation", "-language:postfixOps"), - resolvers ++= Seq( - "amateras-repo" at "http://amateras.sourceforge.jp/mvn/" - ), - libraryDependencies ++= Seq( - "gitbucket" % "gitbucket-assembly" % "3.5.0" % "provided", - "com.typesafe.play" %% "twirl-compiler" % "1.0.4" % "provided", - "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided" - ), - javacOptions in compile ++= Seq("-target", "7", "-source", "7") - ).enablePlugins(SbtTwirl) - -} diff --git a/project/plugins.sbt b/project/plugins.sbt index 048ef34..3fcddcc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1 @@ -logLevel := Level.Warn - -addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2") \ No newline at end of file +addSbtPlugin("io.github.gitbucket" % "sbt-gitbucket-plugin" % "1.6.0") \ No newline at end of file diff --git a/sbt-launch-0.13.5.jar b/sbt-launch-0.13.5.jar deleted file mode 100644 index 174a7e1..0000000 Binary files a/sbt-launch-0.13.5.jar and /dev/null differ diff --git a/sbt.bat b/sbt.bat deleted file mode 100644 index 086b986..0000000 --- a/sbt.bat +++ /dev/null @@ -1,2 +0,0 @@ -set SCRIPT_DIR=%~dp0 -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar "%SCRIPT_DIR%\sbt-launch-0.13.5.jar" %* diff --git a/sbt.sh b/sbt.sh deleted file mode 100755 index 7737f13..0000000 --- a/sbt.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch-0.13.5.jar "$@" diff --git a/src/main/twirl/gitbucket/gist/style.scala.html b/src/main/resources/gitbucket/gist/assets/style.css similarity index 64% rename from src/main/twirl/gitbucket/gist/style.scala.html rename to src/main/resources/gitbucket/gist/assets/style.css index 2709d84..2226b23 100644 --- a/src/main/twirl/gitbucket/gist/style.scala.html +++ b/src/main/resources/gitbucket/gist/assets/style.css @@ -1,11 +1,14 @@ -@() - \ No newline at end of file + +@media (min-width: 767px) { + div.gist-content { + width: 980px; + margin: auto; + } +} diff --git a/src/main/resources/update/1_0.sql b/src/main/resources/update/1_0.sql deleted file mode 100644 index e4e5b04..0000000 --- a/src/main/resources/update/1_0.sql +++ /dev/null @@ -1,27 +0,0 @@ -CREATE TABLE GIST ( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - PRIVATE BOOLEAN NOT NULL, - TITLE VARCHAR(100) NOT NULL, - DESCRIPTION TEXT, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL -); - -CREATE TABLE GIST_COMMENT( - USER_NAME VARCHAR(100) NOT NULL, - REPOSITORY_NAME VARCHAR(100) NOT NULL, - COMMENT_ID INT AUTO_INCREMENT, - COMMENTED_USER_NAME VARCHAR(100) NOT NULL, - CONTENT TEXT NOT NULL, - REGISTERED_DATE TIMESTAMP NOT NULL, - UPDATED_DATE TIMESTAMP NOT NULL -); - -ALTER TABLE GIST ADD CONSTRAINT IDX_GIST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME); -ALTER TABLE GIST ADD CONSTRAINT IDX_GIST_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME); - -ALTER TABLE GIST_COMMENT ADD CONSTRAINT IDX_GIST_COMMENT_PK PRIMARY KEY (COMMENT_ID); -ALTER TABLE GIST_COMMENT ADD CONSTRAINT IDX_GIST_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMENT_ID); -ALTER TABLE GIST_COMMENT ADD CONSTRAINT IDX_GIST_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES GIST (USER_NAME, REPOSITORY_NAME); -ALTER TABLE GIST_COMMENT ADD CONSTRAINT IDX_GIST_COMMENT_FK1 FOREIGN KEY (COMMENTED_USER_NAME) REFERENCES ACCOUNT (USER_NAME); diff --git a/src/main/resources/update/1_3.sql b/src/main/resources/update/1_3.sql deleted file mode 100644 index 723d64a..0000000 --- a/src/main/resources/update/1_3.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE GIST ADD COLUMN ORIGIN_USER_NAME VARCHAR(100); -ALTER TABLE GIST ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100); diff --git a/src/main/resources/update/gitbucket-gist_2.0.xml b/src/main/resources/update/gitbucket-gist_2.0.xml new file mode 100644 index 0000000..c8b843e --- /dev/null +++ b/src/main/resources/update/gitbucket-gist_2.0.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/update/gitbucket-gist_4.2.xml b/src/main/resources/update/gitbucket-gist_4.2.xml new file mode 100644 index 0000000..50621af --- /dev/null +++ b/src/main/resources/update/gitbucket-gist_4.2.xml @@ -0,0 +1,8 @@ + + + + + + UPDATE GIST SET MODE='SECRET' WHERE PRIVATE = TRUE + + \ No newline at end of file diff --git a/src/main/scala/Plugin.scala b/src/main/scala/Plugin.scala index 990ac39..830e64c 100755 --- a/src/main/scala/Plugin.scala +++ b/src/main/scala/Plugin.scala @@ -1,9 +1,11 @@ +import gitbucket.core.controller.Context import gitbucket.core.model._ import gitbucket.core.service.AccountService import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.gist.controller.GistController import gitbucket.core.plugin._ -import gitbucket.core.util.Version +import io.github.gitbucket.solidbase.migration.LiquibaseMigration +import io.github.gitbucket.solidbase.model.Version import java.io.File import javax.servlet.ServletContext import gitbucket.gist.util.Configurations._ @@ -17,9 +19,33 @@ class Plugin extends gitbucket.core.plugin.Plugin { override val description: String = "Provides Gist feature on GitBucket." override val versions: List[Version] = List( - Version(1, 3), - Version(1, 2), - Version(1, 0) + new Version("2.0.0", // This is mistake in 4.0.0 but it can't be fixed for migration. + new LiquibaseMigration("update/gitbucket-gist_2.0.xml") + ), + new Version("4.2.0", + new LiquibaseMigration("update/gitbucket-gist_4.2.xml") + ), + new Version("4.4.0"), + new Version("4.5.0"), + new Version("4.6.0"), + new Version("4.7.0"), + new Version("4.8.0"), + new Version("4.9.0"), + new Version("4.9.1"), + new Version("4.10.0"), + new Version("4.11.0"), + new Version("4.12.0"), + new Version("4.13.0"), + new Version("4.14.0"), + new Version("4.15.0"), + new Version("4.16.0"), + new Version("4.17.0"), + new Version("4.18.0"), + new Version("4.19.0"), + new Version("4.20.0"), + new Version("4.21.0"), + new Version("4.22.0"), + new Version("4.23.0") ) override def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = { @@ -31,7 +57,6 @@ class Plugin extends gitbucket.core.plugin.Plugin { rootdir.mkdirs() } - println("-- Gist plug-in initialized --") } override val repositoryRoutings = Seq( @@ -42,17 +67,29 @@ class Plugin extends gitbucket.core.plugin.Plugin { "/*" -> new GistController() ) - override def javaScripts(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = { - // Add Snippet link to the header - val path = settings.baseUrl.getOrElse(context.getContextPath) - Seq( - ".*" -> s""" - |$$('a.global-header-menu:last').after( - | $$('Gist') - |); - """.stripMargin - ) - } + override val globalMenus = Seq( + (context: Context) => Some(Link("snippets", "Snippets", "gist")) + ) + override val profileTabs = Seq( + (account: Account, context: Context) => Some(Link("snippets", "Snippets", s"gist/${account.userName}/_profile")) + ) + override val assetsMappings = Seq("/gist" -> "/gitbucket/gist/assets") + +// override def javaScripts(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = { +// // Add Snippet link to the header +// val path = settings.baseUrl.getOrElse(context.getContextPath) +// Seq( +// ".*" -> s""" +// |var accountName = $$('div.account-username').text(); +// |if(accountName != ''){ +// | var active = location.href.endsWith('_profile'); +// | $$('li:has(a:contains(Public Activity))').after( +// | $$('Snippets') +// | ); +// |} +// """.stripMargin +// ) +// } } class GistRepositoryFilter extends GitRepositoryFilter with AccountService { diff --git a/src/main/scala/gitbucket/gist/controller/GistController.scala b/src/main/scala/gitbucket/gist/controller/GistController.scala index 55578f5..988ef1c 100644 --- a/src/main/scala/gitbucket/gist/controller/GistController.scala +++ b/src/main/scala/gitbucket/gist/controller/GistController.scala @@ -2,14 +2,12 @@ package gitbucket.gist.controller import java.io.File import gitbucket.core.view.helpers -import jp.sf.amateras.scalatra.forms._ +import org.scalatra.forms._ import gitbucket.core.controller.ControllerBase import gitbucket.core.service.AccountService import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.util._ -import gitbucket.core.util.Directory._ -import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.Implicits._ import gitbucket.core.view.helpers._ @@ -19,11 +17,14 @@ import gitbucket.gist.util._ import gitbucket.gist.util.GistUtils._ import gitbucket.gist.util.Configurations._ import gitbucket.gist.html +import gitbucket.gist.js -import org.apache.commons.io.FileUtils import org.eclipse.jgit.api.Git import org.eclipse.jgit.lib._ import org.scalatra.Ok +import play.twirl.api.Html +import play.twirl.api.JavaScript +import scala.util.Using class GistController extends GistControllerBase with GistService with GistCommentService with AccountService with GistEditorAuthenticator with UsersAuthenticator @@ -44,34 +45,40 @@ trait GistControllerBase extends ControllerBase { //////////////////////////////////////////////////////////////////////////////// get("/gist"){ - if(context.loginAccount.isDefined){ - val gists = getRecentGists(context.loginAccount.get.userName, 0, 4) - html.edit(gists, None, Seq(("", JGitUtil.ContentInfo("text", None, Some("UTF-8"))))) - } else { - val page = request.getParameter("page") match { - case ""|null => 1 - case s => s.toInt - } - val result = getPublicGists((page - 1) * Limit, Limit) - val count = countPublicGists() - - val gists: Seq[(Gist, GistInfo)] = result.map { gist => - val userName = gist.userName - val repoName = gist.repositoryName - val files = getGistFiles(userName, repoName) - val (fileName, source) = files.head + val page = request.getParameter("page") match { + case ""|null => 1 + case s => s.toInt + } + val result = getVisibleGists((page - 1) * Limit, Limit, context.loginAccount) + val count = countVisibleGists(context.loginAccount) - (gist, GistInfo(fileName, source, files.length, getForkedCount(userName, repoName), getCommentCount(userName, repoName))) - } + val gists: Seq[(Gist, GistInfo)] = result.map { gist => + val userName = gist.userName + val repoName = gist.repositoryName + val files = getGistFiles(userName, repoName) + val (fileName, source) = files.head - html.list(None, gists, page, page * Limit < count) + (gist, GistInfo(fileName, getLines(fileName, source), files.length, getForkedCount(userName, repoName), getCommentCount(userName, repoName))) } + + html.list(None, gists, page, page * Limit < count) } get("/gist/:userName/:repoName"){ _gist(params("userName"), Some(params("repoName"))) } + get("/gist/:userName/:repoName.js"){ + val userName = params("userName") + val repoName = params("repoName") + getGist(userName, repoName) match { + case Some(gist) => + _embedJs(gist, userName, repoName, "master") + case None => + NotFound() + } + } + get("/gist/:userName/:repoName/:revision"){ _gist(params("userName"), Some(params("repoName")), params("revision")) } @@ -81,50 +88,51 @@ trait GistControllerBase extends ControllerBase { val repoName = params("repoName") val gitdir = new File(GistRepoDir, userName + "/" + repoName) if(gitdir.exists){ - using(Git.open(gitdir)){ git => + Using.resource(Git.open(gitdir)){ git => val files: Seq[(String, JGitUtil.ContentInfo)] = JGitUtil.getFileList(git, "master", ".").map { file => - (if(isGistFile(file.name)) "" else file.name) -> JGitUtil.getContentInfo(git, file.name, file.id) + (if(isGistFile(file.name)) "" else file.name) -> JGitUtil.getContentInfo(git, file.name, file.id, true) } - html.edit(Nil, getGist(userName, repoName), files) + html.edit(getGist(userName, repoName), files, None) } } }) post("/gist/_new")(usersOnly { - if(context.loginAccount.isDefined){ - val loginAccount = context.loginAccount.get - val files = getFileParameters(true) + val loginAccount = context.loginAccount.get + val userName = params.getOrElse("userName", loginAccount.userName) + if(isEditable(userName, loginUserGroups)) { + val files = getFileParameters() if(files.isEmpty){ redirect(s"/gist") } else { - val isPrivate = params("private").toBoolean - val description = params("description") + val mode = Mode.from(params("mode")) + val description = params("description") // Create new repository - val repoName = StringUtil.md5(loginAccount.userName + " " + datetime(new java.util.Date())) - val gitdir = new File(GistRepoDir, loginAccount.userName + "/" + repoName) + val repoName = StringUtil.md5(userName + " " + datetime(new java.util.Date())) + val gitdir = new File(GistRepoDir, userName + "/" + repoName) gitdir.mkdirs() - JGitUtil.initRepository(gitdir) + JGitUtil.initRepository(gitdir, "master") // Insert record registerGist( - loginAccount.userName, + userName, repoName, - isPrivate, getTitle(files.head._1, repoName), - description + description, + mode ) // Commit files - using(Git.open(gitdir)){ git => + Using.resource(Git.open(gitdir)){ git => commitFiles(git, loginAccount, "Initial commit", files) } - redirect(s"/gist/${loginAccount.userName}/${repoName}") + redirect(s"/gist/${userName}/${repoName}") } - } + } else Unauthorized() }) post("/gist/:userName/:repoName/edit")(editorOnly { @@ -132,20 +140,22 @@ trait GistControllerBase extends ControllerBase { val repoName = params("repoName") val loginAccount = context.loginAccount.get - val files = getFileParameters(true) + val files = getFileParameters() val description = params("description") + val mode = Mode.from(params("mode")) // Update database updateGist( userName, repoName, getTitle(files.head._1, repoName), - description + description, + mode ) // Commit files val gitdir = new File(GistRepoDir, userName + "/" + repoName) - using(Git.open(gitdir)){ git => + Using.resource(Git.open(gitdir)){ git => val commitId = commitFiles(git, loginAccount, "Update", files) // update refs @@ -157,14 +167,14 @@ trait GistControllerBase extends ControllerBase { refUpdate.update() } - redirect(s"/gist/${loginAccount.userName}/${repoName}") + redirect(s"/gist/${userName}/${repoName}") }) get("/gist/:userName/:repoName/delete")(editorOnly { val userName = params("userName") val repoName = params("repoName") - if(isEditable(userName)){ + if(isEditable(userName, loginUserGroups)){ deleteGist(userName, repoName) val gitdir = new File(GistRepoDir, userName + "/" + repoName) @@ -174,42 +184,16 @@ trait GistControllerBase extends ControllerBase { } }) - get("/gist/:userName/:repoName/secret")(editorOnly { - val userName = params("userName") - val repoName = params("repoName") - - if(isEditable(userName)){ - updateGistAccessibility(userName, repoName, true) - } - - redirect(s"/gist/${userName}/${repoName}") - }) - - get("/gist/:userName/:repoName/public")(editorOnly { - val userName = params("userName") - val repoName = params("repoName") - - if(isEditable(userName)){ - updateGistAccessibility(userName, repoName, false) - } - - redirect(s"/gist/${userName}/${repoName}") - }) - get("/gist/:userName/:repoName/revisions"){ val userName = params("userName") val repoName = params("repoName") val gitdir = new File(GistRepoDir, userName + "/" + repoName) - using(Git.open(gitdir)){ git => + Using.resource(Git.open(gitdir)){ git => JGitUtil.getCommitLog(git, "master") match { case Right((revisions, hasNext)) => { val commits = revisions.map { revision => - defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision.id))){ revCommit => - JGitUtil.getDiffs(git, revision.id) match { case (diffs, oldCommitId) => - (revision, diffs) - } - } + (revision, JGitUtil.getDiffs(git, None, revision.id, true, false)) } val gist = getGist(userName, repoName).get @@ -220,11 +204,11 @@ trait GistControllerBase extends ControllerBase { gist, getForkedCount(originUserName, originRepoName), GistRepositoryURL(gist, baseUrl, context.settings), - isEditable(userName), + isEditable(userName, loginUserGroups), commits ) } - case Left(_) => NotFound + case Left(_) => NotFound() } } } @@ -236,18 +220,17 @@ trait GistControllerBase extends ControllerBase { val fileName = params("fileName") val gitdir = new File(GistRepoDir, userName + "/" + repoName) if(gitdir.exists){ - using(Git.open(gitdir)){ git => + Using.resource(Git.open(gitdir)){ git => val gist = getGist(userName, repoName).get - if(!gist.isPrivate || context.loginAccount.exists(x => x.isAdmin || x.userName == userName)){ + if(gist.mode == "PUBLIC" || context.loginAccount.exists(x => x.isAdmin || x.userName == userName)){ JGitUtil.getFileList(git, revision, ".").find(_.name == fileName).map { file => - defining(JGitUtil.getContentFromId(git, file.id, false).get){ bytes => - RawData(FileUtil.getContentType(file.name, bytes), bytes) - } - } getOrElse NotFound - } else Unauthorized + val bytes = JGitUtil.getContentFromId(git, file.id, false).get + RawData(FileUtil.getMimeType(file.name, bytes), bytes) + } getOrElse NotFound() + } else Unauthorized() } - } else NotFound + } else NotFound() } get("/gist/:userName/:repoName/download/*"){ @@ -259,13 +242,7 @@ trait GistControllerBase extends ControllerBase { val userName = params("userName") val repoName = params("repoName") - val workDir = getDownloadWorkDir(userName, repoName, session.getId) - if(workDir.exists) { - FileUtils.deleteDirectory(workDir) - } - workDir.mkdirs - - using(Git.open(new File(GistRepoDir, userName + "/" + repoName))){ git => + Using.resource(Git.open(new File(GistRepoDir, userName + "/" + repoName))){ git => val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve("master")) contentType = "application/octet-stream" @@ -278,18 +255,48 @@ trait GistControllerBase extends ControllerBase { .setOutputStream(response.getOutputStream) .call() - Unit + () } } + get("/gist/:userName/_profile"){ + val userName = params("userName") + + val result: (Seq[Gist], Int) = ( + getUserGists(userName, context.loginAccount.map(_.userName), 0, Limit), + countUserGists(userName, context.loginAccount.map(_.userName)) + ) + + val createSnippet = context.loginAccount.exists { loginAccount => + loginAccount.userName == userName || getGroupsByUserName(loginAccount.userName).contains(userName) + } + + getAccountByUserName(userName).map { account => + html.profile( + account = account, + groupNames = if(account.isGroupAccount) Nil else getGroupsByUserName(userName), + extraMailAddresses = getAccountExtraMailAddresses(userName), + gists = result._1, + createSnippet = createSnippet + ) + } getOrElse NotFound() + } get("/gist/:userName"){ _gist(params("userName")) } + get("/gist/_new")(usersOnly { + val userName = params.get("userName") + + if(isEditable(userName.getOrElse(context.loginAccount.get.userName), loginUserGroups)){ + html.edit(None, Seq(("", JGitUtil.ContentInfo("text", None, None, Some("UTF-8")))), userName) + } else Unauthorized() + }) + get("/gist/_add"){ val count = params("count").toInt - html.editor(count, "", JGitUtil.ContentInfo("text", None, Some("UTF-8"))) + html.editor(count, "", JGitUtil.ContentInfo("text", None, None, Some("UTF-8"))) } //////////////////////////////////////////////////////////////////////////////// @@ -311,7 +318,7 @@ trait GistControllerBase extends ControllerBase { val originUserName = gist.originUserName.getOrElse(gist.userName) val originRepoName = gist.originRepositoryName.getOrElse(gist.repositoryName) - registerGist(loginAccount.userName, repoName, gist.isPrivate, gist.title, gist.description, + registerGist(loginAccount.userName, repoName, gist.title, gist.description, Mode.from(gist.mode), Some(originUserName), Some(originRepoName)) // Clone repository @@ -322,7 +329,7 @@ trait GistControllerBase extends ControllerBase { redirect(s"/gist/${loginAccount.userName}/${repoName}") - } getOrElse NotFound + } getOrElse NotFound() } }) @@ -336,9 +343,9 @@ trait GistControllerBase extends ControllerBase { getForkedCount(userName, repoName), GistRepositoryURL(gist, baseUrl, context.settings), getForkedGists(userName, repoName), - isEditable(userName) + isEditable(userName, loginUserGroups) ) - } getOrElse NotFound + } getOrElse NotFound() } //////////////////////////////////////////////////////////////////////////////// @@ -352,20 +359,27 @@ trait GistControllerBase extends ControllerBase { val repoName = params("repoName") contentType = "text/html" - helpers.markdown(params("content"), - RepositoryInfo( - owner = userName, - name = repoName, - httpUrl = "", - repository = null, - issueCount = 0, - pullCount = 0, - commitCount = 0, - forkedCount = 0, - branchList = Nil, - tags = Nil, - managers = Nil - ), false, false, false, false) + helpers.markdown( + markdown = params("content"), + repository = RepositoryInfo( + owner = userName, + name = repoName, + repository = null, + issueCount = 0, + pullCount = 0, + forkedCount = 0, + milestoneCount = 0, + branchList = Nil, + tags = Nil, + managers = Nil + ), + branch = "master", + enableWikiLink = false, + enableRefsLink = false, + enableLineBreaks = false, + enableAnchor = false, + enableTaskList = true + ) } post("/gist/:userName/:repoName/_comment", commentForm)(usersOnly { form => @@ -376,7 +390,7 @@ trait GistControllerBase extends ControllerBase { getGist(userName, repoName).map { gist => registerGistComment(userName, repoName, form.content, loginAccount.userName) redirect(s"/gist/${userName}/${repoName}") - } getOrElse NotFound + } getOrElse NotFound() }) ajaxPost("/gist/:userName/:repoName/_comments/:commentId/_delete")(usersOnly { @@ -398,17 +412,23 @@ trait GistControllerBase extends ControllerBase { getGist(userName, repoName).flatMap { gist => getGistComment(userName, repoName, commentId).map { comment => params.get("dataType") collect { - case t if t == "html" => gitbucket.gist.html.commentedit( - comment.content, comment.commentId, comment.userName, comment.repositoryName) + case t if t == "html" => gitbucket.gist.html.commentedit(gist, comment.content, comment.commentId) } getOrElse { contentType = formats("json") org.json4s.jackson.Serialization.write( - Map("content" -> gitbucket.core.view.Markdown.toHtml(comment.content, - gist.toRepositoryInfo, false, true, true, true) // TODO isEditableこれでいいのか? + Map("content" -> gitbucket.core.view.Markdown.toHtml( + markdown = comment.content, + repository = gist.toRepositoryInfo, + branch = "master", + enableWikiLink = false, + enableRefsLink = true, + enableAnchor = true, + enableLineBreaks = true )) + ) } } - } getOrElse NotFound + } getOrElse NotFound() }) ajaxPost("/gist/:userName/:repoName/_comments/:commentId/_update", commentForm)(usersOnly { form => @@ -428,7 +448,8 @@ trait GistControllerBase extends ControllerBase { // //////////////////////////////////////////////////////////////////////////////// - private def _gist(userName: String, repoName: Option[String] = None, revision: String = "master") = { + + private def _gist(userName: String, repoName: Option[String] = None, revision: String = "master"): Any = { repoName match { case None => { val page = params.get("page") match { @@ -445,55 +466,80 @@ trait GistControllerBase extends ControllerBase { val repoName = gist.repositoryName val files = getGistFiles(userName, repoName, revision) val (fileName, source) = files.head - (gist, GistInfo(fileName, source, files.length, getForkedCount(userName, repoName), getCommentCount(userName, repoName))) + (gist, GistInfo(fileName, getLines(fileName, source), files.length, getForkedCount(userName, repoName), getCommentCount(userName, repoName))) } val fullName = getAccountByUserName(userName).get.fullName html.list(Some(GistUser(userName, fullName)), gists, page, page * Limit < result._2) } case Some(repoName) => { - val gist = getGist(userName, repoName).get - val originUserName = gist.originUserName.getOrElse(userName) - val originRepoName = gist.originRepositoryName.getOrElse(repoName) - - html.detail( - gist, - getForkedCount(originUserName, originRepoName), - GistRepositoryURL(gist, baseUrl, context.settings), - revision, - getGistFiles(userName, repoName, revision), - getGistComments(userName, repoName), - isEditable(userName) - ) + getGist(userName, repoName) match { + case Some(gist) => + if(gist.mode == "PRIVATE"){ + context.loginAccount match { + case Some(x) if(x.userName == userName) => _gistDetail(gist, userName, repoName, revision) + case _ => Unauthorized() + } + } else { + _gistDetail(gist, userName, repoName, revision) + } + case None => + NotFound() + } } } } + private def _embedJs(gist: Gist, userName: String, repoName: String, revision: String): JavaScript = { + val originUserName = gist.originUserName.getOrElse(userName) + val originRepoName = gist.originRepositoryName.getOrElse(repoName) + + js.detail( + gist, + GistRepositoryURL(gist, baseUrl, context.settings), + revision, + getGistFiles(userName, repoName, revision) + ) + } + + private def _gistDetail(gist: Gist, userName: String, repoName: String, revision: String): Html = { + val originUserName = gist.originUserName.getOrElse(userName) + val originRepoName = gist.originRepositoryName.getOrElse(repoName) + + html.detail( + gist, + getForkedCount(originUserName, originRepoName), + GistRepositoryURL(gist, baseUrl, context.settings), + revision, + getGistFiles(userName, repoName, revision), + getGistComments(userName, repoName), + isEditable(userName, loginUserGroups) + ) + } + private def getGistFiles(userName: String, repoName: String, revision: String = "master"): Seq[(String, String)] = { val gitdir = new File(GistRepoDir, userName + "/" + repoName) - using(Git.open(gitdir)){ git => + Using.resource(Git.open(gitdir)){ git => JGitUtil.getFileList(git, revision, ".").map { file => file.name -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(git, file.id, true).get) } } } - private def getFileParameters(flatten: Boolean): Seq[(String, String)] = { + private def getFileParameters(): Seq[(String, String)] = { val count = params("count").toInt - if(flatten){ - (0 to count - 1).flatMap { i => - (params.get(s"fileName-${i}"), params.get(s"content-${i}")) match { - case (Some(fileName), Some(content)) if(content.nonEmpty) => Some((if(fileName.isEmpty) s"gistfile${i + 1}.txt" else fileName, content)) - case _ => None - } - } - } else { - (0 to count - 1).map { i => - val fileName = request.getParameter(s"fileName-${i}") - val content = request.getParameter(s"content-${i}") - (if(fileName.isEmpty) s"gistfile${i + 1}.txt" else fileName, content) + (0 to count - 1).flatMap { i => + (params.get(s"fileName-${i}"), params.get(s"content-${i}")) match { + case (Some(fileName), Some(content)) if(content.nonEmpty) => Some((if(fileName.isEmpty) s"gistfile${i + 1}.txt" else fileName, content)) + case _ => None } } } + private def loginUserGroups: Seq[String] = { + context.loginAccount.map { account => + getGroupsByUserName(account.userName) + }.getOrElse(Nil) + } + } diff --git a/src/main/scala/gitbucket/gist/model/Gist.scala b/src/main/scala/gitbucket/gist/model/Gist.scala index 30731e4..6f06b0c 100644 --- a/src/main/scala/gitbucket/gist/model/Gist.scala +++ b/src/main/scala/gitbucket/gist/model/Gist.scala @@ -1,7 +1,9 @@ package gitbucket.gist.model +import gitbucket.core.issues.milestones.html.milestone + trait GistComponent { self: gitbucket.core.model.Profile => - import profile.simple._ + import profile.api._ import self._ lazy val Gists = TableQuery[Gists] @@ -9,41 +11,40 @@ trait GistComponent { self: gitbucket.core.model.Profile => class Gists(tag: Tag) extends Table[Gist](tag, "GIST") { val userName = column[String]("USER_NAME") val repositoryName = column[String]("REPOSITORY_NAME") - val isPrivate = column[Boolean]("PRIVATE") val title = column[String]("TITLE") val description = column[String]("DESCRIPTION") val registeredDate = column[java.util.Date]("REGISTERED_DATE") val updatedDate = column[java.util.Date]("UPDATED_DATE") val originUserName = column[String]("ORIGIN_USER_NAME") val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME") - def * = (userName, repositoryName, isPrivate, title, description, registeredDate, updatedDate, originUserName.?, originRepositoryName.?) <> (Gist.tupled, Gist.unapply) + val mode = column[String]("MODE") + def * = (userName, repositoryName, title, description, registeredDate, updatedDate, originUserName.?, originRepositoryName.?, mode).<>(Gist.tupled, Gist.unapply) } } case class Gist( userName: String, repositoryName: String, - isPrivate: Boolean, title: String, description: String, registeredDate: java.util.Date, updatedDate: java.util.Date, originUserName: Option[String], - originRepositoryName: Option[String] + originRepositoryName: Option[String], + mode: String ){ def toRepositoryInfo = { gitbucket.core.service.RepositoryService.RepositoryInfo( - owner = userName, - name = repositoryName, - httpUrl = "", - repository = null, - issueCount = 0, - pullCount = 0, - commitCount = 0, - forkedCount = 0, - branchList = Nil, - tags = Nil, - managers = Nil + owner = userName, + name = repositoryName, + repository = null, + issueCount = 0, + pullCount = 0, + forkedCount = 0, + milestoneCount = 0, + branchList = Nil, + tags = Nil, + managers = Nil ) } } diff --git a/src/main/scala/gitbucket/gist/model/GistComment.scala b/src/main/scala/gitbucket/gist/model/GistComment.scala index c14b598..5265476 100644 --- a/src/main/scala/gitbucket/gist/model/GistComment.scala +++ b/src/main/scala/gitbucket/gist/model/GistComment.scala @@ -1,7 +1,7 @@ package gitbucket.gist.model trait GistCommentComponent { self: gitbucket.core.model.Profile => - import profile.simple._ + import profile.api._ import self._ lazy val GistComments = new TableQuery(tag => new GistComments(tag)){ @@ -16,7 +16,7 @@ trait GistCommentComponent { self: gitbucket.core.model.Profile => val content = column[String]("CONTENT") val registeredDate = column[java.util.Date]("REGISTERED_DATE") val updatedDate = column[java.util.Date]("UPDATED_DATE") - def * = (userName, repositoryName, commentId, commentedUserName, content, registeredDate, updatedDate) <> (GistComment.tupled, GistComment.unapply) + def * = (userName, repositoryName, commentId, commentedUserName, content, registeredDate, updatedDate).<>(GistComment.tupled, GistComment.unapply) } } diff --git a/src/main/scala/gitbucket/gist/model/Mode.scala b/src/main/scala/gitbucket/gist/model/Mode.scala new file mode 100644 index 0000000..a7cb9a6 --- /dev/null +++ b/src/main/scala/gitbucket/gist/model/Mode.scala @@ -0,0 +1,29 @@ +package gitbucket.gist.model + +sealed trait Mode { + val code: String +} + +object Mode { + + def from(code: String): Mode = { + code match { + case Public.code => Public + case Secret.code => Secret + case Private.code => Private + } + } + + case object Public extends Mode { + val code = "PUBLIC" + } + + case object Secret extends Mode { + val code = "SECRET" + } + + case object Private extends Mode { + val code = "PRIVATE" + } + +} \ No newline at end of file diff --git a/src/main/scala/gitbucket/gist/service/GistCommentService.scala b/src/main/scala/gitbucket/gist/service/GistCommentService.scala index 03fc6ab..2258482 100644 --- a/src/main/scala/gitbucket/gist/service/GistCommentService.scala +++ b/src/main/scala/gitbucket/gist/service/GistCommentService.scala @@ -1,8 +1,10 @@ package gitbucket.gist.service +import scala.language.reflectiveCalls import gitbucket.gist.model.GistComment import gitbucket.gist.model.Profile._ -import profile.simple._ +import gitbucket.gist.model.Profile.profile.blockingApi._ +import gitbucket.gist.model.Profile.dateColumnType trait GistCommentService { diff --git a/src/main/scala/gitbucket/gist/service/GistService.scala b/src/main/scala/gitbucket/gist/service/GistService.scala index a7f46d0..5ef7229 100644 --- a/src/main/scala/gitbucket/gist/service/GistService.scala +++ b/src/main/scala/gitbucket/gist/service/GistService.scala @@ -1,34 +1,42 @@ package gitbucket.gist.service +import gitbucket.core.model.Account import gitbucket.gist.model.Gist +import gitbucket.gist.model.Mode import gitbucket.gist.model.Profile._ -import profile.simple._ +import gitbucket.gist.model.Profile.profile.blockingApi._ +import gitbucket.gist.model.Profile.dateColumnType trait GistService { - def getRecentGists(userName: String, offset: Int, limit: Int)(implicit s: Session): Seq[Gist] = - Gists.filter(_.userName === userName.bind).sortBy(_.registeredDate desc).drop(offset).take(limit).list + def getVisibleGists(offset: Int, limit: Int, account: Option[Account])(implicit s: Session): Seq[Gist] = + visibleHistsQuery(account).sortBy(_.registeredDate desc).drop(offset).take(limit).list - def getPublicGists(offset: Int, limit: Int)(implicit s: Session): Seq[Gist] = - Gists.filter(_.isPrivate === false.bind).sortBy(_.registeredDate desc).drop(offset).take(limit).list + def countVisibleGists(account: Option[Account])(implicit s: Session): Int = + Query(visibleHistsQuery(account).length).first - def countPublicGists()(implicit s: Session): Int = - Query(Gists.filter(_.isPrivate === false.bind).length).first + private def visibleHistsQuery(account: Option[Account]): Query[Gists, Gists#TableElementType, Seq] = { + account.map { x => + Gists.filter { t => (t.mode === "PUBLIC".bind) || (t.userName === x.userName.bind) } + } getOrElse { + Gists.filter { t => (t.mode === "PUBLIC".bind) } + } + } def getUserGists(userName: String, loginUserName: Option[String], offset: Int, limit: Int)(implicit s: Session): Seq[Gist] = - (if(loginUserName.isDefined){ - Gists filter(t => (t.userName === userName.bind) && ((t.userName === loginUserName.bind) || (t.isPrivate === false.bind))) - } else { - Gists filter(t => (t.userName === userName.bind) && (t.isPrivate === false.bind)) - }).sortBy(_.registeredDate desc).drop(offset).take(limit).list + userGistsQuery(userName, loginUserName).sortBy(_.registeredDate desc).drop(offset).take(limit).list def countUserGists(userName: String, loginUserName: Option[String])(implicit s: Session): Int = - Query((if(loginUserName.isDefined){ - Gists.filter(t => (t.userName === userName.bind) && ((t.userName === loginUserName.bind) || (t.isPrivate === false.bind))) + Query(userGistsQuery(userName, loginUserName).length).first + + private def userGistsQuery(userName: String, loginUserName: Option[String]): Query[Gists, Gists#TableElementType, Seq] = { + if (loginUserName.isDefined) { + Gists filter (t => (t.userName === userName.bind) && ((t.userName === loginUserName.bind) || (t.mode === "PUBLIC".bind))) } else { - Gists.filter(t => (t.userName === userName.bind) && (t.isPrivate === false.bind)) - }).length).first + Gists filter (t => (t.userName === userName.bind) && (t.mode === "PUBLIC".bind)) + } + } def getGist(userName: String, repositoryName: String)(implicit s: Session): Option[Gist] = Gists.filter(t => (t.userName === userName.bind) && (t.repositoryName === repositoryName.bind)).firstOption @@ -39,23 +47,16 @@ trait GistService { def getForkedGists(userName: String, repositoryName: String)(implicit s: Session): Seq[Gist] = Gists.filter(t => (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)).sortBy(_.userName).list - def registerGist(userName: String, repositoryName: String, isPrivate: Boolean, title: String, description: String, + def registerGist(userName: String, repositoryName: String, title: String, description: String, mode: Mode, originUserName: Option[String] = None, originRepositoryName: Option[String] = None)(implicit s: Session): Unit = - Gists.insert(Gist(userName, repositoryName, isPrivate, title, description, new java.util.Date(), new java.util.Date(), - originUserName, originRepositoryName)) + Gists.insert(Gist(userName, repositoryName, title, description, new java.util.Date(), new java.util.Date(), + originUserName, originRepositoryName, mode.code)) - def updateGist(userName: String, repositoryName: String, title: String, description: String)(implicit s: Session): Unit = + def updateGist(userName: String, repositoryName: String, title: String, description: String, mode: Mode)(implicit s: Session): Unit = Gists .filter(t => (t.userName === userName.bind) && (t.repositoryName === repositoryName.bind)) - .map(t => (t.title, t.description, t.updatedDate)) - .update(title, description, new java.util.Date()) - - def updateGistAccessibility(userName: String, repositoryName: String, isPrivate: Boolean)(implicit s: Session): Unit = - Gists - .filter(t => (t.userName === userName.bind) && (t.repositoryName === repositoryName.bind)) - .map(t => (t.isPrivate)) - .update(isPrivate) - + .map(t => (t.title, t.description, t.updatedDate, t.mode)) + .update(title, description, new java.util.Date(), mode.code) def deleteGist(userName: String, repositoryName: String)(implicit s: Session): Unit = { GistComments.filter(t => (t.userName === userName.bind) && (t.repositoryName === repositoryName.bind)).delete diff --git a/src/main/scala/gitbucket/gist/util/GistAuthenticator.scala b/src/main/scala/gitbucket/gist/util/GistAuthenticator.scala index 9db97b0..7c7bc99 100644 --- a/src/main/scala/gitbucket/gist/util/GistAuthenticator.scala +++ b/src/main/scala/gitbucket/gist/util/GistAuthenticator.scala @@ -1,26 +1,25 @@ package gitbucket.gist.util import gitbucket.core.controller.ControllerBase -import gitbucket.core.util.ControlUtil._ +import gitbucket.core.service.AccountService import gitbucket.core.util.Implicits._ /** * Allows only editor of the accessed snippet. */ -trait GistEditorAuthenticator { self: ControllerBase => +trait GistEditorAuthenticator { self: ControllerBase with AccountService => protected def editorOnly(action: => Any) = { authenticate(action) } protected def editorOnly[T](action: T => Any) = (form: T) => { authenticate(action(form)) } private def authenticate(action: => Any) = { { - defining(request.paths){ paths => - if(context.loginAccount.map { loginAccount => - loginAccount.isAdmin || loginAccount.userName == paths(1) - }.getOrElse(false)){ - action - } else { - Unauthorized() - } + val paths = request.paths + if(context.loginAccount.map { loginAccount => + loginAccount.isAdmin || loginAccount.userName == paths(1) || getGroupsByUserName(loginAccount.userName).contains(paths(1)) + }.getOrElse(false)){ + action + } else { + Unauthorized() } } } diff --git a/src/main/scala/gitbucket/gist/util/GistUtils.scala b/src/main/scala/gitbucket/gist/util/GistUtils.scala index b91901e..b5f7f93 100644 --- a/src/main/scala/gitbucket/gist/util/GistUtils.scala +++ b/src/main/scala/gitbucket/gist/util/GistUtils.scala @@ -12,9 +12,9 @@ import org.eclipse.jgit.lib.{FileMode, Constants, ObjectId} object GistUtils { - def isEditable(userName: String)(implicit context: Context): Boolean = { + def isEditable(userName: String, groupNames: Seq[String])(implicit context: Context): Boolean = { context.loginAccount.map { loginAccount => - loginAccount.isAdmin || loginAccount.userName == userName + loginAccount.isAdmin || loginAccount.userName == userName || groupNames.contains(userName) }.getOrElse(false) } @@ -33,11 +33,21 @@ object GistUtils { Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, message) inserter.flush() - inserter.release() + inserter.close() commitId } + def getLines(fileName: String, source: String): String = { + val lines = source.split("\n").map(_.trim).take(10) + + (if((fileName.endsWith(".md") || fileName.endsWith(".markdown")) && lines.count(_ == "```") % 2 != 0) { + lines :+ "```" + } else { + lines + }).mkString("\n") + } + def isGistFile(fileName: String): Boolean = fileName.matches("gistfile[0-9]+\\.txt") def getTitle(fileName: String, repoName: String): String = if(isGistFile(fileName)) repoName else fileName @@ -46,11 +56,13 @@ object GistUtils { def httpUrl: String = s"${baseUrl}/git/gist/${gist.userName}/${gist.repositoryName}.git" - def sshUrl(loginUser: String): String = { - val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1) - s"ssh://${loginUser}@${host}:${settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort)}/gist/${gist.userName}/${gist.repositoryName}.git" - } + def embedUrl: String = s"${baseUrl}/gist/${gist.userName}/${gist.repositoryName}.js" + def sshUrl: Option[String] = { + settings.sshUrl.map { sshUrl => + s"${sshUrl}/gist/${gist.userName}/${gist.repositoryName}.git" + } + } } } diff --git a/src/main/twirl/gitbucket/gist/commentedit.scala.html b/src/main/twirl/gitbucket/gist/commentedit.scala.html index b18c124..6a6da7a 100644 --- a/src/main/twirl/gitbucket/gist/commentedit.scala.html +++ b/src/main/twirl/gitbucket/gist/commentedit.scala.html @@ -1,12 +1,15 @@ -@(content: String, commentId: Int, userName: String, repoName: String)(implicit context: gitbucket.core.controller.Context) -@import context._ +@(gist: gitbucket.gist.model.Gist, content: String, commentId: Int)(implicit context: gitbucket.core.controller.Context) -@gitbucket.core.helper.html.attached(userName, repoName){ - -} -
- - +@gitbucket.gist.html.commentpreview( + gist = gist, + content = content, + style = "height: 100px; max-height: 150px;", + elastic = true, + uid = commentId +) +
+ +
- - - + + + + -} \ No newline at end of file +} diff --git a/src/main/twirl/gitbucket/gist/editor.scala.html b/src/main/twirl/gitbucket/gist/editor.scala.html index 61fc45b..b45b561 100644 --- a/src/main/twirl/gitbucket/gist/editor.scala.html +++ b/src/main/twirl/gitbucket/gist/editor.scala.html @@ -1,28 +1,24 @@ @(i: Int, fileName: String, content: gitbucket.core.util.JGitUtil.ContentInfo)(implicit context: gitbucket.core.controller.Context) -@import context._ -@import gitbucket.core.view.helpers._ - - - - - - - -
-
- -
- -
-
-
+@import gitbucket.core.view.helpers +
+
+
+ +
+ + +
+
+
+
+
@@ -34,15 +30,30 @@ var editor = ace.edit('editor-@i'); editor.setTheme("ace/theme/monokai"); + if(localStorage.getItem('gitbucket:editor:wrap') == 'true'){ + editor.getSession().setUseWrapMode(true); + $('#wrap-@i').val('true'); + } + @if(fileName.nonEmpty){ - editor.getSession().setMode("ace/mode/@editorType(fileName)"); + var modelist = ace.require("ace/ext/modelist"); + var mode = modelist.getModeForPath("@fileName"); + editor.getSession().setMode(mode.mode); } $('#wrap-@i').change(function(){ if($('#wrap-@i option:selected').val() == 'true'){ editor.getSession().setUseWrapMode(true); + localStorage.setItem('gitbucket:editor:wrap', 'true'); } else { editor.getSession().setUseWrapMode(false); + localStorage.setItem('gitbucket:editor:wrap', 'false'); + } + }); + + $('#remove-@i').click(function(){ + if(confirm('Remove this file. Are you sure?')){ + $('#editor-area-@i').remove(); } }); }); diff --git a/src/main/twirl/gitbucket/gist/forks.scala.html b/src/main/twirl/gitbucket/gist/forks.scala.html index c8b274a..b4951ee 100644 --- a/src/main/twirl/gitbucket/gist/forks.scala.html +++ b/src/main/twirl/gitbucket/gist/forks.scala.html @@ -3,22 +3,26 @@ repositoryUrl: gitbucket.gist.util.GistUtils.GistRepositoryURL, forkedGists: Seq[gitbucket.gist.model.Gist], editable: Boolean)(implicit context: gitbucket.core.controller.Context) -@import context._ -@import gitbucket.core.view.helpers._ +@import gitbucket.core.view.helpers @gitbucket.core.html.main("Snippets"){ - @gitbucket.gist.html.header(gist, forkedCount, editable) -
- @gitbucket.gist.html.menu("forks", gist, repositoryUrl) -
- @forkedGists.map { forkedGist => -
- @avatar(forkedGist.userName, 20) - @forkedGist.userName -
- View Fork -
+ +
+
+ @gitbucket.gist.html.header(gist, forkedCount, editable) +
+ @gitbucket.gist.html.menu("forks", gist, repositoryUrl) +
+ @forkedGists.map { forkedGist => +
+ @helpers.avatar(forkedGist.userName, 20) + @forkedGist.userName +
+ View Fork +
+
+ }
- } +
} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/gist/header.scala.html b/src/main/twirl/gitbucket/gist/header.scala.html index 5a7608c..48be5ea 100644 --- a/src/main/twirl/gitbucket/gist/header.scala.html +++ b/src/main/twirl/gitbucket/gist/header.scala.html @@ -1,48 +1,45 @@ @(gist: gitbucket.gist.model.Gist, forkedCount: Int, editable: Boolean)(implicit context: gitbucket.core.controller.Context) -@import context._ -@import gitbucket.core.view.helpers._ -
-
- @avatar(gist.userName, 24) - @gist.userName / - @gist.title - @if(gist.isPrivate){ - Secret +@import gitbucket.gist.model.Mode +@import gitbucket.core.view.helpers +
+ @helpers.avatar(gist.userName, 24) + @gist.userName / + @gist.title + @if(gist.mode == Mode.Secret.code){ + Secret + } + @if(gist.mode == Mode.Private.code){ + Private + } +
+ @if(editable){ + Edit + Delete } -
- @if(editable){ - Edit - Delete + @if(gist.originUserName.isEmpty){ + @if(context.loginAccount.isEmpty){ + Fork @forkedCount + } else { + Fork @forkedCount } - @if(gist.originUserName.isEmpty){ -
- @if(loginAccount.isEmpty){ - Fork - } else { - Fork - } - @forkedCount -
- @if(loginAccount.isDefined){ -
-
- } + @if(context.loginAccount.isDefined){ +
+
} -
+ }
-
+
Created at @gist.registeredDate @if(gist.originUserName.isDefined){ - - forked from @gist.originUserName/@gist.originRepositoryName + - forked from @gist.originUserName/@gist.originRepositoryName }
-
-} \ No newline at end of file + @if(context.settings.ssh.enabled && context.loginAccount.isDefined){ + @repositoryUrl.sshUrl.map { sshUrl => + $('#repository-url-ssh').click(function(){ + $('#repository-url-proto').text('SSH'); + $('#repository-url').val('@sshUrl'); + $('#repository-url-copy').attr('data-clipboard-text', $('#repository-url').val()); + }); + } + } +}); + diff --git a/src/main/twirl/gitbucket/gist/profile.scala.html b/src/main/twirl/gitbucket/gist/profile.scala.html new file mode 100644 index 0000000..ef3f630 --- /dev/null +++ b/src/main/twirl/gitbucket/gist/profile.scala.html @@ -0,0 +1,34 @@ +@(account: gitbucket.core.model.Account, groupNames: List[String], extraMailAddresses: List[String], + gists: Seq[gitbucket.gist.model.Gist], createSnippet: Boolean)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.gist.model.Mode +@gitbucket.core.account.html.main(account, groupNames, "snippets", extraMailAddresses){ + @if(createSnippet){ + + } + @if(gists.isEmpty){ + No snippets + } else { + @gists.map { gist => +
+
+ +
+
+
+ @gist.title + @if(gist.mode == Mode.Secret.code){ + Secret + } + @if(gist.mode == Mode.Private.code){ + Private + } +
+
@gist.description
+
Updated @gitbucket.core.helper.html.datetimeago(gist.updatedDate)
+
+
+ } + } +} \ No newline at end of file diff --git a/src/main/twirl/gitbucket/gist/revisions.scala.html b/src/main/twirl/gitbucket/gist/revisions.scala.html index a473cb9..433765f 100644 --- a/src/main/twirl/gitbucket/gist/revisions.scala.html +++ b/src/main/twirl/gitbucket/gist/revisions.scala.html @@ -3,73 +3,80 @@ repositoryUrl: gitbucket.gist.util.GistUtils.GistRepositoryURL, editable: Boolean, revisions: List[(gitbucket.core.util.JGitUtil.CommitInfo, List[gitbucket.core.util.JGitUtil.DiffInfo])])(implicit context: gitbucket.core.controller.Context) -@import context._ -@import gitbucket.core.helper.html._ -@import gitbucket.core.view.helpers._ +@import gitbucket.core.view.helpers @import org.eclipse.jgit.diff.DiffEntry.ChangeType @gitbucket.core.html.main(s"Revisions · ${gist.repositoryName}"){ - @gitbucket.gist.html.header(gist, forkedCount, editable) -
- @gitbucket.gist.html.menu("revision", gist, repositoryUrl) -
- @revisions.map { case (revision, diffs) => -
-
- @avatar(revision, 20) @revision.authorName revised this @datetimeago(revision.authorTime) -
- @if(diffs.isEmpty){ - No changes. - } - @revision.id.substring(0, 7) + +
+
+ @gitbucket.gist.html.header(gist, forkedCount, editable) +
+ @gitbucket.gist.html.menu("revision", gist, repositoryUrl) +
+ @revisions.map { case (revision, diffs) => +
+
+ @helpers.avatar(revision, 20) @revision.authorName revised this @gitbucket.core.helper.html.datetimeago(revision.authorTime) +
+ @if(diffs.isEmpty){ + No changes. + } + @revision.id.substring(0, 7) +
+
+
+ @diffs.zipWithIndex.map { case (diff, i) => + + + + + + + + + + + +
+ @diff.changeType match { + case ChangeType.ADD => { + + + @diff.newPath + } + case ChangeType.MODIFY => { + + + @diff.newPath + } + case ChangeType.DELETE => { + + + @diff.oldPath + } + case _ => { + } + } +
+ @if(diff.newContent != None || diff.oldContent != None){ +
+ + + } else { + Not supported + } +
+ } +
-
-
- @diffs.zipWithIndex.map { case (diff, i) => - - - - - - - -
- @diff.changeType match { - case ChangeType.ADD => { - - - @diff.newPath - } - case ChangeType.MODIFY => { - - - @diff.newPath - } - case ChangeType.DELETE => { - - - @diff.oldPath - } - case _ => { - } - } -
- @if(diff.newContent != None || diff.oldContent != None){ -
- - - } else { - Not supported - } -
- } -
+ }
- } +
- - - + + +