mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-10-31 02:25:59 +01:00 
			
		
		
		
	Compare commits
	
		
			85 Commits
		
	
	
		
			disable-gi
			...
			4.35.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 2a3c8e0712 | ||
|  | d3a29b3ecb | ||
|  | 7a50a15748 | ||
|  | 9a1b55b992 | ||
|  | 828b798c0e | ||
|  | 8d8845536d | ||
|  | f20497e769 | ||
|  | 6053d9826e | ||
|  | 85263474a7 | ||
|  | c02a722799 | ||
|  | ce4faceccc | ||
|  | 04c8f8b864 | ||
|  | 1b32e13113 | ||
|  | 401728d47f | ||
|  | 31ace89f43 | ||
|  | 995cb86e90 | ||
|  | e27623ca29 | ||
|  | ea4da561c5 | ||
|  | 2e8f3efafd | ||
|  | f25cf5781c | ||
|  | d97f7c6025 | ||
|  | e91d903650 | ||
|  | 4f93f06de5 | ||
|  | 08ed3c4171 | ||
|  | 5193d82980 | ||
|  | eab82bf1be | ||
|  | 311d758910 | ||
|  | 097a2d32b8 | ||
|  | a34766ccfd | ||
|  | 147eef9ee5 | ||
|  | 49118662b2 | ||
|  | 5989f2e2cb | ||
|  | 127f034bba | ||
|  | e1e00c4b94 | ||
|  | eb64cdd9cd | ||
|  | 1bfa8dffb8 | ||
|  | 33361b8015 | ||
|  | b5ee074075 | ||
|  | cbddc34bfa | ||
|  | 61504ae9e3 | ||
|  | 3049f6010c | ||
|  | d4e01d631f | ||
|  | a46aa2c61c | ||
|  | ad147e8dd5 | ||
|  | 3555519392 | ||
|  | f6a5def638 | ||
|  | 0590cb7048 | ||
|  | b35d0792aa | ||
|  | 0d20bc0173 | ||
|  | 851141c2f4 | ||
|  | 31a104a697 | ||
|  | bfc44cff98 | ||
|  | 0da781c33d | ||
|  | 8dbcbb5485 | ||
|  | 7544f64c65 | ||
|  | 73d05aefad | ||
|  | 4d70b056ad | ||
|  | b81ce41d51 | ||
|  | a143683d7f | ||
|  | 5ba38057dc | ||
|  | 07eb2bc41e | ||
|  | 5e4d041295 | ||
|  | 4d7fc061a4 | ||
|  | 8db98d7b16 | ||
|  | a6063c8aa9 | ||
|  | 2cc1336e82 | ||
|  | 308bda2050 | ||
|  | 36989c38d4 | ||
|  | 29357ae170 | ||
|  | 36643bcdd0 | ||
|  | 72e40a0b12 | ||
|  | 2194ff7625 | ||
|  | b247864bfe | ||
|  | b1c3ae4974 | ||
|  | 81c0e2037f | ||
|  | 6224ec2a7b | ||
|  | 9ff4507fe2 | ||
|  | 67667dbff1 | ||
|  | 1a4961c3e1 | ||
|  | 561220237f | ||
|  | f45b85aa71 | ||
|  | 83b3a7983e | ||
|  | 63d4c5054e | ||
|  | c8f6017be9 | ||
|  | f9fcb54861 | 
| @@ -5,6 +5,10 @@ trim_trailing_whitespace = true | ||||
| insert_final_newline = true | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| end_of_line = lf | ||||
|  | ||||
| [*.md] | ||||
| trim_trailing_whitespace = false | ||||
|  | ||||
| [*.java] | ||||
| trim_trailing_whitespace = true | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "weekly" | ||||
							
								
								
									
										16
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,10 +1,6 @@ | ||||
| name: build | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ master ] | ||||
|   pull_request: | ||||
|     branches: [ master ] | ||||
| on: [push, pull_request] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
| @@ -14,6 +10,16 @@ jobs: | ||||
|         java: [8, 11] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Cache | ||||
|       uses: actions/cache@v2 | ||||
|       env: | ||||
|         cache-name: cache-sbt-libs | ||||
|       with: | ||||
|         path: | | ||||
|           ~/.ivy2/cache | ||||
|           ~/.sbt | ||||
|           ~/.cache/coursier/v1 | ||||
|         key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }} | ||||
|     - name: Set up JDK | ||||
|       uses: actions/setup-java@v1 | ||||
|       with: | ||||
|   | ||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -10,6 +10,7 @@ lib_managed/ | ||||
| src_managed/ | ||||
| project/boot/ | ||||
| project/plugins/project/ | ||||
| .bsp/ | ||||
|  | ||||
| # Scala-IDE specific | ||||
| .scala_dependencies | ||||
| @@ -28,4 +29,8 @@ project/plugins/project/ | ||||
| # Metals specific | ||||
| .metals | ||||
| .bloop | ||||
| project/metals.sbt | ||||
| **/metals.sbt | ||||
|  | ||||
| # Visual Studio Code specific | ||||
| .vscode | ||||
|  | ||||
|   | ||||
							
								
								
									
										39
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,15 +1,38 @@ | ||||
| # Changelog | ||||
| All changes to the project will be documented in this file. | ||||
|  | ||||
| ### 4.33.0 - 31 Dec 2019 | ||||
| ### 4.35.0 - 25 Dec 2020 | ||||
| - Editor and source viewer color theme | ||||
| - Auto completion for issues and pull requests | ||||
| - Upload image from clipboard | ||||
| - Close multiple issues by commit comment | ||||
| - Create pull request from online editor | ||||
| - Milestone overview | ||||
| - Commit status at various places | ||||
| - WebAPI coverage improvements | ||||
|  | ||||
| ## 4.34.0 - 26 Jul 2020 | ||||
| - Enhancement admin settings UI | ||||
|    - File upload settings | ||||
|    - Restrict repository operations | ||||
|    - User-defined CSS | ||||
|    - Limit the repository list in the sidebar | ||||
| - Improve MariaDB support | ||||
| - Improve activity logging | ||||
| - CLI option to persist session on disk in the standalone mode | ||||
| - Web API updates | ||||
|   - Add [list commits API](https://developer.github.com/v3/repos/commits/#list-commits) | ||||
| - Bundled plugins updates | ||||
|   - [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) 4.18.0 -> 4.19.0 | ||||
|   - [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin) 1.8.0 -> 1.9.0 | ||||
|  | ||||
| ## 4.33.0 - 31 Dec 2019 | ||||
| - All CLI options are configurable by environment variables | ||||
| - Folding pull request files | ||||
| - WebHook security options | ||||
| - Add assignee and assignees properties to some Web APIs' response | ||||
|  | ||||
| ### 4.32.0 - 7 Aug 2019 | ||||
|  | ||||
| ## 4.32.0 - 7 Aug 2019 | ||||
| - Bump to Scala 2.13.0 and Scalatra 2.7.0 | ||||
| - Draft pull request | ||||
| - Drop network installation of plugins | ||||
| @@ -17,20 +40,20 @@ All changes to the project will be documented in this file. | ||||
| - Apply default priority to pull requests | ||||
| - Focus title after clicking issue / pull request edit button | ||||
|  | ||||
| ### 4.31.2 - 7 Apr 2019 | ||||
| ## 4.31.2 - 7 Apr 2019 | ||||
| - Bug and security fix | ||||
|  | ||||
| ### 4.31.1 - 17 Mar 2019 | ||||
| ## 4.31.1 - 17 Mar 2019 | ||||
| - Bug fix | ||||
|  | ||||
| ### 4.31.0 - 17 Mar 2019 | ||||
| ## 4.31.0 - 17 Mar 2019 | ||||
| - Docker support in CI plugin | ||||
| - Verify GPG key signed commit | ||||
| - OAuth2 Token (sent as a parameter) authentication support and new APIs in Web API | ||||
| - OGP (Open Graph protocol) support | ||||
| - Username completion with avatars | ||||
|  | ||||
| ### 4.30.1 - 22 Dec 2018 | ||||
| ## 4.30.1 - 22 Dec 2018 | ||||
| - Bug fix for several WebHooks and Web API | ||||
|  | ||||
| ## 4.30.0 - 15 Dec 2018 | ||||
| @@ -117,7 +140,7 @@ All changes to the project will be documented in this file. | ||||
| - Submodule links to web page | ||||
| - Clarify close/reopen button | ||||
|  | ||||
| # 4.20.0 - 23 Dec 2017 | ||||
| ## 4.20.0 - 23 Dec 2017 | ||||
| - Squash and rebase merge strategy for pull requests | ||||
| - Quick pull request creation | ||||
| - Download patch from the diff view | ||||
|   | ||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							| @@ -55,13 +55,17 @@ Support | ||||
| - If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue. | ||||
| - The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. | ||||
|  | ||||
| What's New in 4.33.x | ||||
| What's New in 4.34.x | ||||
| ------------- | ||||
| ### 4.33.0 - 31 Dec 2019 | ||||
| ### 4.35.0 - 25 Dec 2020 | ||||
|  | ||||
| - All CLI options are configurable by environment variables | ||||
| - Folding pull request files | ||||
| - WebHook security options | ||||
| - Add assignee and assignees properties to some Web APIs' response | ||||
| - Editor and source viewer color theme | ||||
| - Auto completion for issues and pull requests | ||||
| - Upload image from clipboard | ||||
| - Close multiple issues by commit comment | ||||
| - Create pull request from online editor | ||||
| - Milestone overview | ||||
| - Commit status at various places | ||||
| - WebAPI coverage improvements | ||||
|  | ||||
| See the [change log](CHANGELOG.md) for all of the updates. | ||||
|   | ||||
							
								
								
									
										29
									
								
								build.sbt
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								build.sbt
									
									
									
									
									
								
							| @@ -3,10 +3,10 @@ import com.typesafe.sbt.pgp.PgpKeys._ | ||||
|  | ||||
| val Organization = "io.github.gitbucket" | ||||
| val Name = "gitbucket" | ||||
| val GitBucketVersion = "4.33.0" | ||||
| val ScalatraVersion = "2.7.0-RC1" | ||||
| val JettyVersion = "9.4.30.v20200611" | ||||
| val JgitVersion = "5.8.0.202006091008-r" | ||||
| val GitBucketVersion = "4.35.0" | ||||
| val ScalatraVersion = "2.7.0" | ||||
| val JettyVersion = "9.4.32.v20200930" | ||||
| val JgitVersion = "5.9.0.202009080501-r" | ||||
|  | ||||
| lazy val root = (project in file(".")) | ||||
|   .enablePlugins(SbtTwirl, ScalatraPlugin) | ||||
| @@ -36,30 +36,28 @@ libraryDependencies ++= Seq( | ||||
|   "org.scalatra"                    %% "scalatra"                    % ScalatraVersion, | ||||
|   "org.scalatra"                    %% "scalatra-json"               % ScalatraVersion, | ||||
|   "org.scalatra"                    %% "scalatra-forms"              % ScalatraVersion, | ||||
|   "org.json4s"                      %% "json4s-jackson"              % "3.6.9", | ||||
|   "commons-io"                      % "commons-io"                   % "2.7", | ||||
|   "org.json4s"                      %% "json4s-jackson"              % "3.6.10", | ||||
|   "commons-io"                      % "commons-io"                   % "2.8.0", | ||||
|   "io.github.gitbucket"             % "solidbase"                    % "1.0.3", | ||||
|   "io.github.gitbucket"             % "markedj"                      % "1.0.16", | ||||
|   "org.apache.commons"              % "commons-compress"             % "1.20", | ||||
|   "org.apache.commons"              % "commons-email"                % "1.5", | ||||
|   "commons-net"                     % "commons-net"                  % "3.6", | ||||
|   "commons-net"                     % "commons-net"                  % "3.7", | ||||
|   "org.apache.httpcomponents"       % "httpclient"                   % "4.5.12", | ||||
|   "org.apache.sshd"                 % "apache-sshd"                  % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"), | ||||
|   "org.apache.tika"                 % "tika-core"                    % "1.24.1", | ||||
|   "com.github.takezoe"              %% "blocking-slick-32"           % "0.0.12", | ||||
|   "com.novell.ldap"                 % "jldap"                        % "2009-10-07", | ||||
|   "com.h2database"                  % "h2"                           % "1.4.199", | ||||
|   "org.mariadb.jdbc"                % "mariadb-java-client"          % "2.6.0", | ||||
|   "org.mariadb.jdbc"                % "mariadb-java-client"          % "2.7.0", | ||||
|   "org.postgresql"                  % "postgresql"                   % "42.2.6", | ||||
|   "ch.qos.logback"                  % "logback-classic"              % "1.2.3", | ||||
|   "com.zaxxer"                      % "HikariCP"                     % "3.4.5", | ||||
|   "com.typesafe"                    % "config"                       % "1.4.0", | ||||
|   "com.typesafe.akka"               %% "akka-actor"                  % "2.5.27", | ||||
|   "fr.brouillard.oss.security.xhub" % "xhub4j-core"                  % "1.1.0", | ||||
|   "com.github.bkromhout"            % "java-diff-utils"              % "2.1.1", | ||||
|   "org.cache2k"                     % "cache2k-all"                  % "1.2.4.Final", | ||||
|   "com.enragedginger"               %% "akka-quartz-scheduler"       % "1.8.1-akka-2.5.x" exclude ("com.mchange", "c3p0") exclude ("com.zaxxer", "HikariCP-java6"), | ||||
|   "net.coobird"                     % "thumbnailator"                % "0.4.11", | ||||
|   "net.coobird"                     % "thumbnailator"                % "0.4.12", | ||||
|   "com.github.zafarkhaja"           % "java-semver"                  % "0.9.0", | ||||
|   "com.nimbusds"                    % "oauth2-oidc-sdk"              % "5.64.4", | ||||
|   "org.eclipse.jetty"               % "jetty-webapp"                 % JettyVersion % "provided", | ||||
| @@ -72,7 +70,8 @@ libraryDependencies ++= Seq( | ||||
|   "org.testcontainers"              % "postgresql"                   % "1.14.3" % "test", | ||||
|   "net.i2p.crypto"                  % "eddsa"                        % "0.3.0", | ||||
|   "is.tagomor.woothee"              % "woothee-java"                 % "1.11.0", | ||||
|   "org.ec4j.core"                   % "ec4j-core"                    % "0.0.3" | ||||
|   "org.ec4j.core"                   % "ec4j-core"                    % "0.0.3", | ||||
|   "org.kohsuke"                     % "github-api"                   % "1.116" % "test" | ||||
| ) | ||||
|  | ||||
| // Compiler settings | ||||
| @@ -122,6 +121,12 @@ libraryDependencies ++= Seq( | ||||
|   "org.eclipse.jetty" % "jetty-util"         % JettyVersion % "executable" | ||||
| ) | ||||
|  | ||||
| // Run package task before test to generate target/webapp for integration test | ||||
| test in Test := { | ||||
|   _root_.sbt.Keys.`package`.value | ||||
|   (test in Test).value | ||||
| } | ||||
|  | ||||
| val executableKey = TaskKey[File]("executable") | ||||
| executableKey := { | ||||
|   import java.util.jar.Attributes.{Name => AttrName} | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| sbt.version=1.3.12 | ||||
| sbt.version=1.4.4 | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| notifications:1.8.0 | ||||
| gist:4.18.0 | ||||
| notifications:1.9.0 | ||||
| gist:4.20.0 | ||||
| emoji:4.6.0 | ||||
| pages:1.8.0 | ||||
| pages:1.9.0 | ||||
|   | ||||
							
								
								
									
										4
									
								
								src/main/resources/update/gitbucket-core_4.34.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/main/resources/update/gitbucket-core_4.34.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <changeSet> | ||||
|   <dropTable tableName="ACTIVITY" /> | ||||
| </changeSet> | ||||
							
								
								
									
										21
									
								
								src/main/resources/update/gitbucket-core_4.35.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/main/resources/update/gitbucket-core_4.35.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <changeSet> | ||||
|   <dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/> | ||||
|   <dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK"/> | ||||
|   <dropPrimaryKey tableName="WEB_HOOK" constraintName="IDX_WEB_HOOK_PK"/> | ||||
|   <addColumn tableName="WEB_HOOK"> | ||||
|     <column name="HOOK_ID" type="int" nullable="false" unique="true"/> | ||||
|   </addColumn> | ||||
|   <addPrimaryKey constraintName="IDX_WEB_HOOK_PK" tableName="WEB_HOOK" columnNames="USER_NAME, REPOSITORY_NAME, URL, HOOK_ID"/> | ||||
|   <addUniqueConstraint constraintName="IDX_WEB_HOOK_1" tableName="WEB_HOOK" columnNames="USER_NAME, REPOSITORY_NAME, URL"/> | ||||
|   <addForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/> | ||||
|   <addForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, URL" referencedTableName="WEB_HOOK" referencedColumnNames="USER_NAME, REPOSITORY_NAME, URL" onDelete="CASCADE" onUpdate="CASCADE"/> | ||||
|   <addAutoIncrement columnName="HOOK_ID" columnDataType="int" tableName="WEB_HOOK"/> | ||||
|  | ||||
|   <createTable tableName="ACCOUNT_PREFERENCE"> | ||||
|     <column name="USER_NAME" type="varchar(100)" nullable="false"/> | ||||
|     <column name="HIGHLIGHTER_THEME" type="varchar(100)" nullable="false" defaultValue="prettify"/> | ||||
|   </createTable> | ||||
|   <addPrimaryKey constraintName="IDX_ACCOUNT_PREFERENCE_PK" tableName="ACCOUNT_PREFERENCE" columnNames="USER_NAME"/> | ||||
|   <addForeignKeyConstraint constraintName="IDX_ACCOUNT_PREFERENCE_FK0" baseTableName="ACCOUNT_PREFERENCE" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/> | ||||
| </changeSet> | ||||
| @@ -1,7 +1,22 @@ | ||||
| package gitbucket.core | ||||
|  | ||||
| import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration} | ||||
| import io.github.gitbucket.solidbase.model.{Version, Module} | ||||
| import java.io.FileOutputStream | ||||
| import java.nio.charset.StandardCharsets | ||||
| import java.sql.Connection | ||||
| import java.util | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.util.Directory.ActivityLog | ||||
| import gitbucket.core.util.JDBCUtil | ||||
| import io.github.gitbucket.solidbase.Solidbase | ||||
| import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration, SqlMigration} | ||||
| import io.github.gitbucket.solidbase.model.{Module, Version} | ||||
| import org.json4s.NoTypeHints | ||||
| import org.json4s.jackson.Serialization | ||||
| import org.json4s.jackson.Serialization.write | ||||
|  | ||||
| import scala.util.Using | ||||
|  | ||||
| object GitBucketCoreModule | ||||
|     extends Module( | ||||
| @@ -65,5 +80,39 @@ object GitBucketCoreModule | ||||
|       new Version("4.31.1"), | ||||
|       new Version("4.31.2"), | ||||
|       new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")), | ||||
|       new Version("4.33.0") | ||||
|       new Version("4.33.0"), | ||||
|       new Version( | ||||
|         "4.34.0", | ||||
|         new Migration() { | ||||
|           override def migrate(moduleId: String, version: String, context: util.Map[String, AnyRef]): Unit = { | ||||
|             implicit val formats = Serialization.formats(NoTypeHints) | ||||
|             import JDBCUtil._ | ||||
|  | ||||
|             val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection] | ||||
|             val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") { | ||||
|               rs => | ||||
|                 Activity( | ||||
|                   activityId = UUID.randomUUID().toString, | ||||
|                   userName = rs.getString("USER_NAME"), | ||||
|                   repositoryName = rs.getString("REPOSITORY_NAME"), | ||||
|                   activityUserName = rs.getString("ACTIVITY_USER_NAME"), | ||||
|                   activityType = rs.getString("ACTIVITY_TYPE"), | ||||
|                   message = rs.getString("MESSAGE"), | ||||
|                   additionalInfo = { | ||||
|                     val additionalInfo = rs.getString("ADDITIONAL_INFO") | ||||
|                     if (rs.wasNull()) None else Some(additionalInfo) | ||||
|                   }, | ||||
|                   activityDate = rs.getTimestamp("ACTIVITY_DATE") | ||||
|                 ) | ||||
|             } | ||||
|             Using.resource(new FileOutputStream(ActivityLog, true)) { out => | ||||
|               list.foreach { activity => | ||||
|                 out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8)) | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         new LiquibaseMigration("update/gitbucket-core_4.34.xml") | ||||
|       ), | ||||
|       new Version("4.35.0", new LiquibaseMigration("update/gitbucket-core_4.35.xml")), | ||||
|     ) | ||||
|   | ||||
| @@ -22,3 +22,12 @@ case class ApiBranchForList( | ||||
|   name: String, | ||||
|   commit: ApiBranchCommit | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/rest/reference/repos#list-branches-for-head-commit | ||||
|  */ | ||||
| case class ApiBranchForHeadCommit( | ||||
|   name: String, | ||||
|   commit: ApiBranchCommit, | ||||
|   `protected`: Boolean | ||||
| ) | ||||
|   | ||||
| @@ -4,7 +4,11 @@ import gitbucket.core.service.ProtectedBranchService | ||||
| import org.json4s._ | ||||
|  | ||||
| /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ | ||||
| case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]) { | ||||
| case class ApiBranchProtection( | ||||
|   url: Option[ApiPath], // for output | ||||
|   enabled: Boolean, | ||||
|   required_status_checks: Option[ApiBranchProtection.Status] | ||||
| ) { | ||||
|   def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone) | ||||
| } | ||||
|  | ||||
| @@ -15,13 +19,36 @@ object ApiBranchProtection { | ||||
|  | ||||
|   def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = | ||||
|     ApiBranchProtection( | ||||
|       url = Some( | ||||
|         ApiPath( | ||||
|           s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection" | ||||
|         ) | ||||
|       ), | ||||
|       enabled = info.enabled, | ||||
|       required_status_checks = Some( | ||||
|         Status(EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), info.contexts) | ||||
|         Status( | ||||
|           Some( | ||||
|             ApiPath( | ||||
|               s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks" | ||||
|             ) | ||||
|           ), | ||||
|           EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), | ||||
|           info.contexts, | ||||
|           Some( | ||||
|             ApiPath( | ||||
|               s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts" | ||||
|             ) | ||||
|           ) | ||||
|   val statusNone = Status(Off, Seq.empty) | ||||
|   case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String]) | ||||
|         ) | ||||
|       ) | ||||
|     ) | ||||
|   val statusNone = Status(None, Off, Seq.empty, None) | ||||
|   case class Status( | ||||
|     url: Option[ApiPath], // for output | ||||
|     enforcement_level: EnforcementLevel, | ||||
|     contexts: Seq[String], | ||||
|     contexts_url: Option[ApiPath] // for output | ||||
|   ) | ||||
|   sealed class EnforcementLevel(val name: String) | ||||
|   case object Off extends EnforcementLevel("off") | ||||
|   case object NonAdmins extends EnforcementLevel("non_admins") | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/main/scala/gitbucket/core/api/ApiMilestone.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/main/scala/gitbucket/core/api/ApiMilestone.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| import gitbucket.core.model.{Milestone, Repository} | ||||
| import gitbucket.core.util.RepositoryName | ||||
| import java.util.Date | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/rest/reference/issues#milestones | ||||
|  */ | ||||
| case class ApiMilestone( | ||||
|   url: ApiPath, | ||||
|   html_url: ApiPath, | ||||
| //  label_url: ApiPath, | ||||
|   id: Int, | ||||
|   number: Int, | ||||
|   state: String, | ||||
|   title: String, | ||||
|   description: String, | ||||
| //  creator: ApiUser,  // MILESTONE table does not have created user column | ||||
|   open_issues: Int, | ||||
|   closed_issues: Int, | ||||
| //  created_at: Option[Date], | ||||
| //  updated_at: Option[Date], | ||||
|   closed_at: Option[Date], | ||||
|   due_on: Option[Date] | ||||
| ) | ||||
|  | ||||
| object ApiMilestone { | ||||
|   def apply( | ||||
|     repository: Repository, | ||||
|     milestone: Milestone, | ||||
|     open_issue_count: Int = 0, | ||||
|     closed_issue_count: Int = 0 | ||||
|   ): ApiMilestone = | ||||
|     ApiMilestone( | ||||
|       url = ApiPath(s"/api/v3/repos/${RepositoryName(repository).fullName}/milestones/${milestone.milestoneId}"), | ||||
|       html_url = ApiPath(s"/${RepositoryName(repository).fullName}/milestone/${milestone.milestoneId}"), | ||||
| //      label_url = ApiPath(s"/api/v3/repos/${RepositoryName(repository).fullName}/milestones/${milestone_number}/labels"), | ||||
|       id = milestone.milestoneId, | ||||
|       number = milestone.milestoneId, // use milestoneId as number | ||||
|       state = if (milestone.closedDate.isDefined) "closed" else "open", | ||||
|       title = milestone.title, | ||||
|       description = milestone.description.getOrElse(""), | ||||
|       open_issues = open_issue_count, | ||||
|       closed_issues = closed_issue_count, | ||||
|       closed_at = milestone.closedDate, | ||||
|       due_on = milestone.dueDate | ||||
|     ) | ||||
| } | ||||
| @@ -12,13 +12,13 @@ case class ApiRepository( | ||||
|   forks: Int, | ||||
|   `private`: Boolean, | ||||
|   default_branch: String, | ||||
|   owner: ApiUser | ||||
|   owner: ApiUser, | ||||
|   has_issues: Boolean | ||||
| ) { | ||||
|   val id = 0 // dummy id | ||||
|   val forks_count = forks | ||||
|   val watchers_count = watchers | ||||
|   val url = ApiPath(s"/api/v3/repos/${full_name}") | ||||
|   val http_url = ApiPath(s"/git/${full_name}.git") | ||||
|   val clone_url = ApiPath(s"/git/${full_name}.git") | ||||
|   val html_url = ApiPath(s"/${full_name}") | ||||
|   val ssh_url = Some(SshPath(s":${full_name}.git")) | ||||
| @@ -39,11 +39,16 @@ object ApiRepository { | ||||
|       forks = forkedCount, | ||||
|       `private` = repository.isPrivate, | ||||
|       default_branch = repository.defaultBranch, | ||||
|       owner = owner | ||||
|       owner = owner, | ||||
|       has_issues = if (repository.options.issuesOption == "DISABLE") false else true | ||||
|     ) | ||||
|  | ||||
|   def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository = | ||||
|     ApiRepository(repositoryInfo.repository, owner, forkedCount = repositoryInfo.forkedCount) | ||||
|     ApiRepository( | ||||
|       repositoryInfo.repository, | ||||
|       owner, | ||||
|       forkedCount = repositoryInfo.forkedCount | ||||
|     ) | ||||
|  | ||||
|   def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository = | ||||
|     this(repositoryInfo, ApiUser(owner)) | ||||
| @@ -57,6 +62,7 @@ object ApiRepository { | ||||
|       forks = 0, | ||||
|       `private` = false, | ||||
|       default_branch = "master", | ||||
|       owner = owner | ||||
|       owner = owner, | ||||
|       has_issues = true | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,6 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| case class ApiRepositoryCollaborator( | ||||
|   permission: String, | ||||
|   user: ApiUser | ||||
| ) | ||||
							
								
								
									
										29
									
								
								src/main/scala/gitbucket/core/api/ApiTag.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/main/scala/gitbucket/core/api/ApiTag.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| import gitbucket.core.util.RepositoryName | ||||
|  | ||||
| case class ApiTagCommit( | ||||
|   sha: String, | ||||
|   url: ApiPath | ||||
| ) | ||||
|  | ||||
| case class ApiTag( | ||||
|   name: String, | ||||
|   commit: ApiTagCommit, | ||||
|   zipball_url: ApiPath, | ||||
|   tarball_url: ApiPath | ||||
| ) | ||||
|  | ||||
| object ApiTag { | ||||
|   def apply( | ||||
|     tagName: String, | ||||
|     repositoryName: RepositoryName, | ||||
|     commitId: String | ||||
|   ): ApiTag = | ||||
|     ApiTag( | ||||
|       name = tagName, | ||||
|       commit = ApiTagCommit(sha = commitId, url = ApiPath(s"/${repositoryName.fullName}/commits/${commitId}")), | ||||
|       zipball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.zip"), | ||||
|       tarball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.tar.gz") | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/main/scala/gitbucket/core/api/ApiWebhook.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/main/scala/gitbucket/core/api/ApiWebhook.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| import gitbucket.core.model.Profile.{RepositoryWebHookEvents, RepositoryWebHooks} | ||||
| import gitbucket.core.model.{RepositoryWebHook, WebHook} | ||||
| import gitbucket.core.util.RepositoryName | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/rest/reference/repos#webhooks | ||||
|  */ | ||||
| case class ApiWebhookConfig( | ||||
|   content_type: String, | ||||
| //  insecure_ssl: String, | ||||
|   url: String | ||||
| ) | ||||
|  | ||||
| case class ApiWebhook( | ||||
|   `type`: String, | ||||
|   id: Int, | ||||
|   name: String, | ||||
|   active: Boolean, | ||||
|   events: List[String], | ||||
|   config: ApiWebhookConfig, | ||||
| //  updated_at: Option[Date], | ||||
| //  created_at: Option[Date], | ||||
|   url: ApiPath, | ||||
| //  test_url: ApiPath, | ||||
| //  ping_url: ApiPath, | ||||
| //  last_response: ... | ||||
| ) | ||||
|  | ||||
| object ApiWebhook { | ||||
|   def apply( | ||||
|     _type: String, | ||||
|     hook: RepositoryWebHook, | ||||
|     hookEvents: Set[WebHook.Event] | ||||
|   ): ApiWebhook = | ||||
|     ApiWebhook( | ||||
|       `type` = _type, | ||||
|       id = hook.hookId, | ||||
|       name = "web", // dummy | ||||
|       active = true, // dummy | ||||
|       events = hookEvents.toList.map(_.name), | ||||
|       config = ApiWebhookConfig(hook.ctype.code, hook.url), | ||||
|       url = ApiPath(s"/api/v3/${hook.userName}/${hook.repositoryName}/hooks/${hook.hookId}") | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/main/scala/gitbucket/core/api/CreateAMilestone.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/main/scala/gitbucket/core/api/CreateAMilestone.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| import java.util.Date | ||||
|  | ||||
| case class CreateAMilestone( | ||||
|   title: String, | ||||
|   state: String = "open", | ||||
|   description: Option[String], | ||||
|   due_on: Option[Date] | ||||
| ) { | ||||
|   def isValid: Boolean = { | ||||
|     title.length <= 100 && title.matches("[a-zA-Z0-9\\-\\+_.]+") | ||||
|   } | ||||
| } | ||||
| @@ -15,3 +15,11 @@ case class CreateAPullRequestAlt( | ||||
|   base: String, | ||||
|   maintainer_can_modify: Option[Boolean] | ||||
| ) | ||||
|  | ||||
| case class UpdateAPullRequest( | ||||
|   title: Option[String], | ||||
|   body: Option[String], | ||||
|   state: Option[String], | ||||
|   base: Option[String], | ||||
|   maintainer_can_modify: Option[Boolean], | ||||
| ) | ||||
|   | ||||
							
								
								
									
										7
									
								
								src/main/scala/gitbucket/core/api/CreateARef.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/main/scala/gitbucket/core/api/CreateARef.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference | ||||
|  * api form | ||||
|  */ | ||||
| case class CreateARef(ref: String, sha: String) | ||||
| @@ -0,0 +1,35 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| case class CreateARepositoryWebhookConfig( | ||||
|   url: String, | ||||
|   content_type: String = "form", | ||||
|   insecure_ssl: String = "0", | ||||
|   secret: Option[String] | ||||
| ) | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook | ||||
|  */ | ||||
| case class CreateARepositoryWebhook( | ||||
|   name: String = "web", | ||||
|   config: CreateARepositoryWebhookConfig, | ||||
|   events: List[String] = List("push"), | ||||
|   active: Boolean = true | ||||
| ) { | ||||
|   def isValid: Boolean = { | ||||
|     config.content_type == "form" || config.content_type == "json" | ||||
|   } | ||||
| } | ||||
|  | ||||
| case class UpdateARepositoryWebhook( | ||||
|   name: String = "web", | ||||
|   config: CreateARepositoryWebhookConfig, | ||||
|   events: List[String] = List("push"), | ||||
|   add_events: List[String] = List(), | ||||
|   remove_events: List[String] = List(), | ||||
|   active: Boolean = true | ||||
| ) { | ||||
|   def isValid: Boolean = { | ||||
|     config.content_type == "form" || config.content_type == "json" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/main/scala/gitbucket/core/api/MergeAPullRequest.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/main/scala/gitbucket/core/api/MergeAPullRequest.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request | ||||
|  */ | ||||
| case class MergeAPullRequest( | ||||
|   commit_title: Option[String], | ||||
|   commit_message: Option[String], | ||||
|   /* TODO: Not Implemented | ||||
|   sha: Option[String],*/ | ||||
|   merge_method: Option[String] | ||||
| ) | ||||
|  | ||||
| case class SuccessToMergePrResponse( | ||||
|   sha: String, | ||||
|   merged: Boolean, | ||||
|   message: String | ||||
| ) | ||||
|  | ||||
| case class FailToMergePrResponse( | ||||
|   documentation_url: String, | ||||
|   message: String | ||||
| ) | ||||
							
								
								
									
										7
									
								
								src/main/scala/gitbucket/core/api/UpdateARef.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/main/scala/gitbucket/core/api/UpdateARef.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| package gitbucket.core.api | ||||
|  | ||||
| /** | ||||
|  * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference | ||||
|  * api form | ||||
|  */ | ||||
| case class UpdateARef(sha: String, force: Boolean) | ||||
| @@ -35,6 +35,7 @@ class AccountController | ||||
|     with WebHookService | ||||
|     with PrioritiesService | ||||
|     with RepositoryCreationService | ||||
|     with RequestCache | ||||
|  | ||||
| trait AccountControllerBase extends AccountManagementControllerBase { | ||||
|   self: AccountService | ||||
| @@ -81,6 +82,8 @@ trait AccountControllerBase extends AccountManagementControllerBase { | ||||
|  | ||||
|   case class PersonalTokenForm(note: String) | ||||
|  | ||||
|   case class SyntaxHighlighterThemeForm(theme: String) | ||||
|  | ||||
|   val newForm = mapping( | ||||
|     "userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), | ||||
|     "password" -> trim(label("Password", text(required, maxlength(20)))), | ||||
| @@ -121,6 +124,10 @@ trait AccountControllerBase extends AccountManagementControllerBase { | ||||
|     "note" -> trim(label("Token", text(required, maxlength(100)))) | ||||
|   )(PersonalTokenForm.apply) | ||||
|  | ||||
|   val syntaxHighlighterThemeForm = mapping( | ||||
|     "highlighterTheme" -> trim(label("Theme", text(required))) | ||||
|   )(SyntaxHighlighterThemeForm.apply) | ||||
|  | ||||
|   case class NewGroupForm( | ||||
|     groupName: String, | ||||
|     description: Option[String], | ||||
| @@ -441,6 +448,29 @@ trait AccountControllerBase extends AccountManagementControllerBase { | ||||
|     redirect(s"/${userName}/_application") | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * Display the user preference settings page | ||||
|    */ | ||||
|   get("/:userName/_preferences")(oneselfOnly { | ||||
|     val userName = params("userName") | ||||
|     val currentTheme = getAccountPreference(userName) match { | ||||
|       case Some(accountHighlighter) => accountHighlighter.highlighterTheme | ||||
|       case _                        => "github-v2" | ||||
|     } | ||||
|     getAccountByUserName(userName).map { x => | ||||
|       html.preferences(x, currentTheme) | ||||
|     } getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * Update the syntax highlighter setting of user | ||||
|    */ | ||||
|   post("/:userName/_preferences/highlighter", syntaxHighlighterThemeForm)(oneselfOnly { form => | ||||
|     val userName = params("userName") | ||||
|     addOrUpdateAccountPreference(userName, form.theme) | ||||
|     redirect(s"/${userName}/_preferences") | ||||
|   }) | ||||
|  | ||||
|   get("/:userName/_hooks")(managersOnly { | ||||
|     val userName = params("userName") | ||||
|     getAccountByUserName(userName).map { account => | ||||
| @@ -521,7 +551,8 @@ trait AccountControllerBase extends AccountManagementControllerBase { | ||||
|     val url = params("url") | ||||
|     val token = Some(params("token")) | ||||
|     val ctype = WebHookContentType.valueOf(params("ctype")) | ||||
|     val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token) | ||||
|     val dummyWebHookInfo = | ||||
|       RepositoryWebHook(userName = userName, repositoryName = "dummy", url = url, ctype = ctype, token = token) | ||||
|     val dummyPayload = { | ||||
|       val ownerAccount = getAccountByUserName(userName).get | ||||
|       WebHookPushPayload.createDummyPayload(ownerAccount) | ||||
|   | ||||
| @@ -13,6 +13,7 @@ class ApiController | ||||
|     with ApiIssueCommentControllerBase | ||||
|     with ApiIssueControllerBase | ||||
|     with ApiIssueLabelControllerBase | ||||
|     with ApiIssueMilestoneControllerBase | ||||
|     with ApiOrganizationControllerBase | ||||
|     with ApiPullRequestControllerBase | ||||
|     with ApiReleaseControllerBase | ||||
| @@ -22,6 +23,7 @@ class ApiController | ||||
|     with ApiRepositoryContentsControllerBase | ||||
|     with ApiRepositoryControllerBase | ||||
|     with ApiRepositoryStatusControllerBase | ||||
|     with ApiRepositoryWebhookControllerBase | ||||
|     with ApiUserControllerBase | ||||
|     with RepositoryService | ||||
|     with AccountService | ||||
| @@ -52,6 +54,7 @@ class ApiController | ||||
|     with ReferrerAuthenticator | ||||
|     with ReadableUsersAuthenticator | ||||
|     with WritableUsersAuthenticator | ||||
|     with RequestCache | ||||
|  | ||||
| trait ApiControllerBase extends ControllerBase { | ||||
|  | ||||
|   | ||||
| @@ -21,10 +21,17 @@ class DashboardController | ||||
|     with WebHookPullRequestService | ||||
|     with WebHookPullRequestReviewCommentService | ||||
|     with MilestonesService | ||||
|     with CommitStatusService | ||||
|     with UsersAuthenticator | ||||
|     with RequestCache | ||||
|  | ||||
| trait DashboardControllerBase extends ControllerBase { | ||||
|   self: IssuesService with PullRequestService with RepositoryService with AccountService with UsersAuthenticator => | ||||
|   self: IssuesService | ||||
|     with PullRequestService | ||||
|     with RepositoryService | ||||
|     with AccountService | ||||
|     with CommitStatusService | ||||
|     with UsersAuthenticator => | ||||
|  | ||||
|   get("/dashboard/repos")(usersOnly { | ||||
|     val repos = getVisibleRepositories( | ||||
| @@ -85,12 +92,13 @@ trait DashboardControllerBase extends ControllerBase { | ||||
|     val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) | ||||
|     val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name) | ||||
|     val page = IssueSearchCondition.page(request) | ||||
|     val issues = searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, userRepos: _*) | ||||
|  | ||||
|     html.issues( | ||||
|       searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), | ||||
|       issues.map(issue => (issue, None)), | ||||
|       page, | ||||
|       countIssue(condition.copy(state = "open"), false, userRepos: _*), | ||||
|       countIssue(condition.copy(state = "closed"), false, userRepos: _*), | ||||
|       countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, userRepos: _*), | ||||
|       countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, userRepos: _*), | ||||
|       filter match { | ||||
|         case "assigned"  => condition.copy(assigned = Some(Some(userName))) | ||||
|         case "mentioned" => condition.copy(mentioned = Some(userName)) | ||||
| @@ -115,12 +123,24 @@ trait DashboardControllerBase extends ControllerBase { | ||||
|     val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName) | ||||
|     val allRepos = getAllRepositories(userName) | ||||
|     val page = IssueSearchCondition.page(request) | ||||
|     val issues = searchIssue( | ||||
|       condition, | ||||
|       IssueSearchOption.PullRequests, | ||||
|       (page - 1) * PullRequestLimit, | ||||
|       PullRequestLimit, | ||||
|       allRepos: _* | ||||
|     ) | ||||
|     val status = issues.map { issue => | ||||
|       issue.commitId.flatMap { commitId => | ||||
|         getCommitStatusWithSummary(issue.issue.userName, issue.issue.repositoryName, commitId) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     html.pulls( | ||||
|       searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), | ||||
|       issues.zip(status), | ||||
|       page, | ||||
|       countIssue(condition.copy(state = "open"), true, allRepos: _*), | ||||
|       countIssue(condition.copy(state = "closed"), true, allRepos: _*), | ||||
|       countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, allRepos: _*), | ||||
|       countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, allRepos: _*), | ||||
|       filter match { | ||||
|         case "assigned"  => condition.copy(assigned = Some(Some(userName))) | ||||
|         case "mentioned" => condition.copy(mentioned = Some(userName)) | ||||
|   | ||||
| @@ -29,6 +29,7 @@ class IndexController | ||||
|     with AccessTokenService | ||||
|     with AccountFederationService | ||||
|     with OpenIDConnectService | ||||
|     with RequestCache | ||||
|  | ||||
| trait IndexControllerBase extends ControllerBase { | ||||
|   self: RepositoryService | ||||
| @@ -78,7 +79,7 @@ trait IndexControllerBase extends ControllerBase { | ||||
|       } | ||||
|       .getOrElse { | ||||
|         gitbucket.core.html.index( | ||||
|           getRecentActivities(), | ||||
|           getRecentPublicActivities(), | ||||
|           getVisibleRepositories(None, withoutPhysicalInfo = true), | ||||
|           showBannerToCreatePersonalAccessToken = false | ||||
|         ) | ||||
| @@ -161,7 +162,7 @@ trait IndexControllerBase extends ControllerBase { | ||||
|  | ||||
|   get("/activities.atom") { | ||||
|     contentType = "application/atom+xml; type=feed" | ||||
|     xml.feed(getRecentActivities()) | ||||
|     xml.feed(getRecentPublicActivities()) | ||||
|   } | ||||
|  | ||||
|   post("/sidebar-collapse") { | ||||
| @@ -212,8 +213,10 @@ trait IndexControllerBase extends ControllerBase { | ||||
|             } | ||||
|             .map { t => | ||||
|               Map( | ||||
|                 "label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(t.userName)}</b> ${StringUtil | ||||
|                   .escapeHtml(t.fullName)}", | ||||
|                 "label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml( | ||||
|                   StringUtil.cutTail(t.userName, 25, "...") | ||||
|                 )}</b> ${StringUtil | ||||
|                   .escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))}", | ||||
|                 "value" -> t.userName | ||||
|               ) | ||||
|             } | ||||
|   | ||||
| @@ -30,6 +30,7 @@ class IssuesController | ||||
|     with WebHookPullRequestReviewCommentService | ||||
|     with CommitsService | ||||
|     with PrioritiesService | ||||
|     with RequestCache | ||||
|  | ||||
| trait IssuesControllerBase extends ControllerBase { | ||||
|   self: IssuesService | ||||
| @@ -111,6 +112,7 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|                 getLabels(owner, name), | ||||
|                 isIssueEditable(repository), | ||||
|                 isIssueManageable(repository), | ||||
|                 isIssueCommentManageable(repository), | ||||
|                 repository | ||||
|               ) | ||||
|             } | ||||
| @@ -237,8 +239,8 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|     defining(repository.owner, repository.name) { | ||||
|       case (owner, name) => | ||||
|         getComment(owner, name, params("id")).map { comment => | ||||
|           if (isEditableContent(owner, name, comment.commentedUserName)) { | ||||
|             Ok(deleteComment(comment.issueId, comment.commentId)) | ||||
|           if (isDeletableComment(owner, name, comment.commentedUserName)) { | ||||
|             Ok(deleteComment(repository.owner, repository.name, comment.issueId, comment.commentId)) | ||||
|           } else Unauthorized() | ||||
|         } getOrElse NotFound() | ||||
|     } | ||||
| @@ -368,6 +370,9 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|             } | ||||
|           case _ => BadRequest() | ||||
|         } | ||||
|         if (params("uri").nonEmpty) { | ||||
|           redirect(params("uri")) | ||||
|         } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
| @@ -376,6 +381,9 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|       executeBatch(repository) { issueId => | ||||
|         getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { | ||||
|           registerIssueLabel(repository.owner, repository.name, issueId, labelId, true) | ||||
|           if (params("uri").nonEmpty) { | ||||
|             redirect(params("uri")) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } getOrElse NotFound() | ||||
| @@ -386,6 +394,9 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|       executeBatch(repository) { | ||||
|         updateAssignedUserName(repository.owner, repository.name, _, value, true) | ||||
|       } | ||||
|       if (params("uri").nonEmpty) { | ||||
|         redirect(params("uri")) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
| @@ -416,6 +427,29 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * JSON API for issue and PR completion. | ||||
|    */ | ||||
|   ajaxGet("/:owner/:repository/_issue/proposals")(writableUsersOnly { repository => | ||||
|     contentType = formats("json") | ||||
|     org.json4s.jackson.Serialization.write( | ||||
|       Map( | ||||
|         "options" -> ( | ||||
|           getOpenIssues(repository.owner, repository.name) | ||||
|             .map { t => | ||||
|               Map( | ||||
|                 "label" -> s"""${if (t.isPullRequest) "<i class='octicon octicon-git-pull-request'></i>" | ||||
|                 else "<i class='octicon octicon-issue-opened'></i>"}<b> #${StringUtil | ||||
|                   .escapeHtml(t.issueId.toString)} ${StringUtil | ||||
|                   .escapeHtml(StringUtil.cutTail(t.title, 50, "..."))}</b>""", | ||||
|                 "value" -> t.issueId.toString | ||||
|               ) | ||||
|             } | ||||
|         ) | ||||
|       ) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") | ||||
|   val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) | ||||
|   val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) | ||||
| @@ -425,6 +459,7 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|     params("from") match { | ||||
|       case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") | ||||
|       case "pulls"  => redirect(s"/${repository.owner}/${repository.name}/pulls") | ||||
|       case _        => | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -432,20 +467,22 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|     defining(repository.owner, repository.name) { | ||||
|       case (owner, repoName) => | ||||
|         val page = IssueSearchCondition.page(request) | ||||
|  | ||||
|         // retrieve search condition | ||||
|         val condition = IssueSearchCondition(request) | ||||
|         // search issues | ||||
|         val issues = | ||||
|           searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, owner -> repoName) | ||||
|  | ||||
|         html.list( | ||||
|           "issues", | ||||
|           searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), | ||||
|           issues.map(issue => (issue, None)), | ||||
|           page, | ||||
|           getAssignableUserNames(owner, repoName), | ||||
|           getMilestones(owner, repoName), | ||||
|           getPriorities(owner, repoName), | ||||
|           getLabels(owner, repoName), | ||||
|           countIssue(condition.copy(state = "open"), false, owner -> repoName), | ||||
|           countIssue(condition.copy(state = "closed"), false, owner -> repoName), | ||||
|           countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, owner -> repoName), | ||||
|           countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, owner -> repoName), | ||||
|           condition, | ||||
|           repository, | ||||
|           isIssueEditable(repository), | ||||
| @@ -462,4 +499,13 @@ trait IssuesControllerBase extends ControllerBase { | ||||
|   ): Boolean = { | ||||
|     hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Tests whether an issue comment is deletable by a logged-in user. | ||||
|    */ | ||||
|   private def isDeletableComment(owner: String, repository: String, author: String)( | ||||
|     implicit context: Context | ||||
|   ): Boolean = { | ||||
|     hasOwnerRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,18 @@ | ||||
| package gitbucket.core.controller | ||||
|  | ||||
| import gitbucket.core.issues.milestones.html | ||||
| import gitbucket.core.service.{AccountService, MilestonesService, RepositoryService} | ||||
| import gitbucket.core.service.IssuesService.{IssueLimit, IssueSearchCondition} | ||||
| import gitbucket.core.service.{ | ||||
|   AccountService, | ||||
|   CommitStatusService, | ||||
|   IssueSearchOption, | ||||
|   MilestonesService, | ||||
|   RepositoryService | ||||
| } | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} | ||||
| import gitbucket.core.util.SyntaxSugars._ | ||||
| import gitbucket.core.view.helpers.{getAssignableUserNames, getLabels, getPriorities, searchIssue} | ||||
| import org.scalatra.forms._ | ||||
| import org.scalatra.i18n.Messages | ||||
|  | ||||
| @@ -13,11 +21,16 @@ class MilestonesController | ||||
|     with MilestonesService | ||||
|     with RepositoryService | ||||
|     with AccountService | ||||
|     with CommitStatusService | ||||
|     with ReferrerAuthenticator | ||||
|     with WritableUsersAuthenticator | ||||
|  | ||||
| trait MilestonesControllerBase extends ControllerBase { | ||||
|   self: MilestonesService with RepositoryService with ReferrerAuthenticator with WritableUsersAuthenticator => | ||||
|   self: MilestonesService | ||||
|     with RepositoryService | ||||
|     with CommitStatusService | ||||
|     with ReferrerAuthenticator | ||||
|     with WritableUsersAuthenticator => | ||||
|  | ||||
|   case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date]) | ||||
|  | ||||
| @@ -36,6 +49,41 @@ trait MilestonesControllerBase extends ControllerBase { | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/milestone/:id")(referrersOnly { repository => | ||||
|     val milestone = getMilestone(repository.owner, repository.name, params("id").toInt) | ||||
|     val page = IssueSearchCondition.page(request) | ||||
|     val condition = IssueSearchCondition( | ||||
|       request, | ||||
|       milestone.get.title | ||||
|     ) | ||||
|     val issues = searchIssue( | ||||
|       condition, | ||||
|       IssueSearchOption.Both, | ||||
|       (page - 1) * IssueLimit, | ||||
|       IssueLimit, | ||||
|       repository.owner -> repository.name | ||||
|     ) | ||||
|     val status = issues.map { issue => | ||||
|       issue.commitId.flatMap { commitId => | ||||
|         getCommitStatusWithSummary(issue.issue.userName, issue.issue.repositoryName, commitId) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     html.milestone( | ||||
|       condition.state, | ||||
|       issues.zip(status), | ||||
|       page, | ||||
|       getAssignableUserNames(repository.owner, repository.name), | ||||
|       getPriorities(repository.owner, repository.name), | ||||
|       getLabels(repository.owner, repository.name), | ||||
|       condition, | ||||
|       getMilestonesWithIssueCount(repository.owner, repository.name) | ||||
|         .filter(p => p._1.milestoneId == milestone.get.milestoneId), | ||||
|       repository, | ||||
|       hasDeveloperRole(repository.owner, repository.name, context.loginAccount) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   get("/:owner/:repository/issues/milestones/new")(writableUsersOnly { | ||||
|     html.edit(None, _) | ||||
|   }) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| package gitbucket.core.controller | ||||
|  | ||||
| import gitbucket.core.model.activity.DeleteBranchInfo | ||||
| import gitbucket.core.pulls.html | ||||
| import gitbucket.core.service.CommitStatusService | ||||
| import gitbucket.core.service.MergeService | ||||
| @@ -36,6 +37,7 @@ class PullRequestsController | ||||
|     with MergeService | ||||
|     with ProtectedBranchService | ||||
|     with PrioritiesService | ||||
|     with RequestCache | ||||
|  | ||||
| trait PullRequestsControllerBase extends ControllerBase { | ||||
|   self: RepositoryService | ||||
| @@ -217,7 +219,7 @@ trait PullRequestsControllerBase extends ControllerBase { | ||||
|             val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) | ||||
|             val mergeStatus = PullRequestService.MergeStatus( | ||||
|               conflictMessage = conflictMessage, | ||||
|               commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo), | ||||
|               commitStatuses = getCommitStatuses(owner, name, pullreq.commitIdTo), | ||||
|               branchProtection = branchProtection, | ||||
|               branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom), | ||||
|               needStatusCheck = context.loginAccount | ||||
| @@ -271,7 +273,8 @@ trait PullRequestsControllerBase extends ControllerBase { | ||||
|           val userName = context.loginAccount.get.userName | ||||
|           Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|             git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call() | ||||
|             recordDeleteBranchActivity(repository.owner, repository.name, userName, pullreq.requestBranch) | ||||
|             val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch) | ||||
|             recordActivity(deleteBranchInfo) | ||||
|           } | ||||
|           createComment( | ||||
|             baseRepository.owner, | ||||
| @@ -634,20 +637,33 @@ trait PullRequestsControllerBase extends ControllerBase { | ||||
|     defining(repository.owner, repository.name) { | ||||
|       case (owner, repoName) => | ||||
|         val page = IssueSearchCondition.page(request) | ||||
|  | ||||
|         // retrieve search condition | ||||
|         val condition = IssueSearchCondition(request) | ||||
|         // search issues | ||||
|         val issues = searchIssue( | ||||
|           condition, | ||||
|           IssueSearchOption.PullRequests, | ||||
|           (page - 1) * PullRequestLimit, | ||||
|           PullRequestLimit, | ||||
|           owner -> repoName | ||||
|         ) | ||||
|         // commit status | ||||
|         val status = issues.map { issue => | ||||
|           issue.commitId.flatMap { commitId => | ||||
|             getCommitStatusWithSummary(owner, repoName, commitId) | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         gitbucket.core.issues.html.list( | ||||
|           "pulls", | ||||
|           searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), | ||||
|           issues.zip(status), | ||||
|           page, | ||||
|           getAssignableUserNames(owner, repoName), | ||||
|           getMilestones(owner, repoName), | ||||
|           getPriorities(owner, repoName), | ||||
|           getLabels(owner, repoName), | ||||
|           countIssue(condition.copy(state = "open"), true, owner -> repoName), | ||||
|           countIssue(condition.copy(state = "closed"), true, owner -> repoName), | ||||
|           countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, owner -> repoName), | ||||
|           countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, owner -> repoName), | ||||
|           condition, | ||||
|           repository, | ||||
|           isEditable(repository), | ||||
|   | ||||
| @@ -2,7 +2,15 @@ package gitbucket.core.controller | ||||
|  | ||||
| import java.io.File | ||||
|  | ||||
| import gitbucket.core.service.{AccountService, ActivityService, PaginationHelper, ReleaseService, RepositoryService} | ||||
| import gitbucket.core.model.activity.ReleaseInfo | ||||
| import gitbucket.core.service.{ | ||||
|   AccountService, | ||||
|   ActivityService, | ||||
|   PaginationHelper, | ||||
|   ReleaseService, | ||||
|   RepositoryService, | ||||
|   RequestCache | ||||
| } | ||||
| import gitbucket.core.util._ | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| @@ -22,6 +30,7 @@ class ReleaseController | ||||
|     with ReadableUsersAuthenticator | ||||
|     with ReferrerAuthenticator | ||||
|     with WritableUsersAuthenticator | ||||
|     with RequestCache | ||||
|  | ||||
| trait ReleaseControllerBase extends ControllerBase { | ||||
|   self: RepositoryService | ||||
| @@ -119,7 +128,8 @@ trait ReleaseControllerBase extends ControllerBase { | ||||
|         createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount) | ||||
|     } | ||||
|  | ||||
|     recordReleaseActivity(repository.owner, repository.name, loginAccount.userName, form.name, tagName) | ||||
|     val releaseInfo = ReleaseInfo(repository.owner, repository.name, loginAccount.userName, form.name, tagName) | ||||
|     recordActivity(releaseInfo) | ||||
|  | ||||
|     redirect(s"/${repository.owner}/${repository.name}/releases/${tagName}") | ||||
|   }) | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import gitbucket.core.util.SyntaxSugars._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.model.WebHookContentType | ||||
| import gitbucket.core.model.activity.RenameRepositoryInfo | ||||
| import org.scalatra.forms._ | ||||
| import org.scalatra.i18n.Messages | ||||
| import org.eclipse.jgit.api.Git | ||||
| @@ -30,8 +31,10 @@ class RepositorySettingsController | ||||
|     with ProtectedBranchService | ||||
|     with CommitStatusService | ||||
|     with DeployKeyService | ||||
|     with ActivityService | ||||
|     with OwnerAuthenticator | ||||
|     with UsersAuthenticator | ||||
|     with RequestCache | ||||
|  | ||||
| trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|   self: RepositoryService | ||||
| @@ -40,6 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|     with ProtectedBranchService | ||||
|     with CommitStatusService | ||||
|     with DeployKeyService | ||||
|     with ActivityService | ||||
|     with OwnerAuthenticator | ||||
|     with UsersAuthenticator => | ||||
|  | ||||
| @@ -97,9 +101,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|       "events" -> webhookEvents, | ||||
|       "ctype" -> label("ctype", text()), | ||||
|       "token" -> optional(trim(label("token", text(maxlength(100))))) | ||||
|     )( | ||||
|       (url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token) | ||||
|     ) | ||||
|     )((url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)) | ||||
|  | ||||
|   // for rename repository | ||||
|   case class RenameRepositoryForm(repositoryName: String) | ||||
| @@ -176,14 +178,15 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   /** Branch protection for branch */ | ||||
|   get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository => | ||||
|   get("/:owner/:repository/settings/branches/*")(ownerOnly { repository => | ||||
|     import gitbucket.core.api._ | ||||
|     val branch = params("branch") | ||||
|     val branch = params("splat") | ||||
|  | ||||
|     if (!repository.branchList.contains(branch)) { | ||||
|       redirect(s"/${repository.owner}/${repository.name}/settings/branches") | ||||
|     } else { | ||||
|       val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) | ||||
|       val lastWeeks = getRecentStatuesContexts( | ||||
|       val lastWeeks = getRecentStatusContexts( | ||||
|         repository.owner, | ||||
|         repository.name, | ||||
|         Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC)) | ||||
| @@ -225,7 +228,13 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|    * Display the web hook edit page. | ||||
|    */ | ||||
|   get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => | ||||
|     val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None) | ||||
|     val webhook = RepositoryWebHook( | ||||
|       userName = repository.owner, | ||||
|       repositoryName = repository.name, | ||||
|       url = "", | ||||
|       ctype = WebHookContentType.FORM, | ||||
|       token = None | ||||
|     ) | ||||
|     html.edithook(webhook, Set(WebHook.Push), repository, true) | ||||
|   }) | ||||
|  | ||||
| @@ -251,7 +260,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|    * Send the test request to registered web hook URLs. | ||||
|    */ | ||||
|   ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => | ||||
|     def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => | ||||
|     def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = | ||||
|       h.map { h => | ||||
|         Array(h.getName, h.getValue) | ||||
|       } | ||||
|  | ||||
| @@ -267,7 +277,13 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|         val url = params("url") | ||||
|         val token = Some(params("token")) | ||||
|         val ctype = WebHookContentType.valueOf(params("ctype")) | ||||
|         val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token) | ||||
|         val dummyWebHookInfo = RepositoryWebHook( | ||||
|           userName = repository.owner, | ||||
|           repositoryName = repository.name, | ||||
|           url = url, | ||||
|           ctype = ctype, | ||||
|           token = token | ||||
|         ) | ||||
|         val dummyPayload = { | ||||
|           val ownerAccount = getAccountByUserName(repository.owner).get | ||||
|           val commits = | ||||
| @@ -371,7 +387,16 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|   post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) => | ||||
|     if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) { | ||||
|       if (repository.name != form.repositoryName) { | ||||
|         // Update database and move git repository | ||||
|         renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) | ||||
|         // Record activity log | ||||
|         val renameInfo = RenameRepositoryInfo( | ||||
|           repository.owner, | ||||
|           form.repositoryName, | ||||
|           context.loginAccount.get.userName, | ||||
|           repository.name | ||||
|         ) | ||||
|         recordActivity(renameInfo) | ||||
|       } | ||||
|       redirect(s"/${repository.owner}/${form.repositoryName}") | ||||
|     } else Forbidden() | ||||
| @@ -384,7 +409,16 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|     if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) { | ||||
|       // Change repository owner | ||||
|       if (repository.owner != form.newOwner) { | ||||
|         // Update database and move git repository | ||||
|         renameRepository(repository.owner, repository.name, form.newOwner, repository.name) | ||||
|         // Record activity log | ||||
|         val renameInfo = RenameRepositoryInfo( | ||||
|           form.newOwner, | ||||
|           repository.name, | ||||
|           context.loginAccount.get.userName, | ||||
|           repository.owner | ||||
|         ) | ||||
|         recordActivity(renameInfo) | ||||
|       } | ||||
|       redirect(s"/${form.newOwner}/${repository.name}") | ||||
|     } else Forbidden() | ||||
| @@ -435,7 +469,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|   /** | ||||
|    * Provides duplication check for web hook url. | ||||
|    */ | ||||
|   private def webHook(needExists: Boolean): Constraint = new Constraint() { | ||||
|   private def webHook(needExists: Boolean): Constraint = | ||||
|     new Constraint() { | ||||
|       override def validate(name: String, value: String, messages: Messages): Option[String] = | ||||
|         if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) { | ||||
|           Some(if (needExists) { | ||||
| @@ -448,7 +483,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   private def webhookEvents = new ValueType[Set[WebHook.Event]] { | ||||
|   private def webhookEvents = | ||||
|     new ValueType[Set[WebHook.Event]] { | ||||
|       def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = { | ||||
|         WebHook.Event.values.flatMap { t => | ||||
|           params.get(name + "." + t.name).map(_ => t) | ||||
| @@ -480,7 +516,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|   /** | ||||
|    * Duplicate check for the rename repository name. | ||||
|    */ | ||||
|   private def renameRepositoryName: Constraint = new Constraint() { | ||||
|   private def renameRepositoryName: Constraint = | ||||
|     new Constraint() { | ||||
|       override def validate( | ||||
|         name: String, | ||||
|         value: String, | ||||
| @@ -498,9 +535,9 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|     } | ||||
|  | ||||
|   /** | ||||
|    * | ||||
|    */ | ||||
|   private def featureOption: Constraint = new Constraint() { | ||||
|   private def featureOption: Constraint = | ||||
|     new Constraint() { | ||||
|       override def validate( | ||||
|         name: String, | ||||
|         value: String, | ||||
| @@ -513,7 +550,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|   /** | ||||
|    * Provides Constraint to validate the repository transfer user. | ||||
|    */ | ||||
|   private def transferUser: Constraint = new Constraint() { | ||||
|   private def transferUser: Constraint = | ||||
|     new Constraint() { | ||||
|       override def validate(name: String, value: String, messages: Messages): Option[String] = | ||||
|         getAccountByUserName(value) match { | ||||
|           case None => Some("User does not exist.") | ||||
| @@ -530,11 +568,16 @@ trait RepositorySettingsControllerBase extends ControllerBase { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   private def mergeOptions = new ValueType[Seq[String]] { | ||||
|   private def mergeOptions = | ||||
|     new ValueType[Seq[String]] { | ||||
|       override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = { | ||||
|         params.getOrElse("mergeOptions", Nil) | ||||
|       } | ||||
|     override def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = { | ||||
|       override def validate( | ||||
|         name: String, | ||||
|         params: Map[String, Seq[String]], | ||||
|         messages: Messages | ||||
|       ): Seq[(String, String)] = { | ||||
|         val mergeOptions = params.getOrElse("mergeOptions", Nil) | ||||
|         if (mergeOptions.isEmpty) { | ||||
|           Seq("mergeOptions" -> "At least one option must be enabled.") | ||||
|   | ||||
| @@ -4,9 +4,9 @@ import java.io.{File, FileInputStream, FileOutputStream} | ||||
|  | ||||
| import scala.util.Using | ||||
| import javax.servlet.http.{HttpServletRequest, HttpServletResponse} | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.repo.html | ||||
| import gitbucket.core.helper | ||||
| import gitbucket.core.model.activity.DeleteBranchInfo | ||||
| import gitbucket.core.service._ | ||||
| import gitbucket.core.service.RepositoryCommitFileService.CommitFile | ||||
| import gitbucket.core.util._ | ||||
| @@ -14,7 +14,8 @@ import gitbucket.core.util.StringUtil._ | ||||
| import gitbucket.core.util.SyntaxSugars._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.model.{Account, CommitState, CommitStatus} | ||||
| import gitbucket.core.model.Account | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.util.JGitUtil.CommitInfo | ||||
| import gitbucket.core.view | ||||
| import gitbucket.core.view.helpers | ||||
| @@ -60,6 +61,7 @@ class RepositoryViewerController | ||||
|     with WebHookPullRequestService | ||||
|     with WebHookPullRequestReviewCommentService | ||||
|     with ProtectedBranchService | ||||
|     with RequestCache | ||||
|  | ||||
| /** | ||||
|  * The repository viewer. | ||||
| @@ -88,7 +90,9 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     branch: String, | ||||
|     path: String, | ||||
|     uploadFiles: String, | ||||
|     message: Option[String] | ||||
|     message: Option[String], | ||||
|     commit: String, | ||||
|     newBranch: Boolean | ||||
|   ) | ||||
|  | ||||
|   case class EditorForm( | ||||
| @@ -100,7 +104,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     lineSeparator: String, | ||||
|     newFileName: String, | ||||
|     oldFileName: Option[String], | ||||
|     commit: String | ||||
|     commit: String, | ||||
|     newBranch: Boolean | ||||
|   ) | ||||
|  | ||||
|   case class DeleteForm( | ||||
| @@ -108,7 +113,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     path: String, | ||||
|     message: Option[String], | ||||
|     fileName: String, | ||||
|     commit: String | ||||
|     commit: String, | ||||
|     newBranch: Boolean | ||||
|   ) | ||||
|  | ||||
|   case class CommentForm( | ||||
| @@ -131,6 +137,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     "path" -> trim(label("Path", text())), | ||||
|     "uploadFiles" -> trim(label("Upload files", text(required))), | ||||
|     "message" -> trim(label("Message", optional(text()))), | ||||
|     "commit" -> trim(label("Commit", text(required, conflict))), | ||||
|     "newBranch" -> trim(label("New Branch", boolean())) | ||||
|   )(UploadForm.apply) | ||||
|  | ||||
|   val editorForm = mapping( | ||||
| @@ -142,7 +150,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     "lineSeparator" -> trim(label("Line Separator", text(required))), | ||||
|     "newFileName" -> trim(label("Filename", text(required))), | ||||
|     "oldFileName" -> trim(label("Old filename", optional(text()))), | ||||
|     "commit" -> trim(label("Commit", text(required, conflict))) | ||||
|     "commit" -> trim(label("Commit", text(required, conflict))), | ||||
|     "newBranch" -> trim(label("New Branch", boolean())) | ||||
|   )(EditorForm.apply) | ||||
|  | ||||
|   val deleteForm = mapping( | ||||
| @@ -150,7 +159,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     "path" -> trim(label("Path", text())), | ||||
|     "message" -> trim(label("Message", optional(text()))), | ||||
|     "fileName" -> trim(label("Filename", text(required))), | ||||
|     "commit" -> trim(label("Commit", text(required, conflict))) | ||||
|     "commit" -> trim(label("Commit", text(required, conflict))), | ||||
|     "newBranch" -> trim(label("New Branch", boolean())) | ||||
|   )(DeleteForm.apply) | ||||
|  | ||||
|   val commentForm = mapping( | ||||
| @@ -252,23 +262,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     val (branchName, path) = repository.splitPath(multiParams("splat").head) | ||||
|     val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) | ||||
|  | ||||
|     def getStatuses(sha: String): List[CommitStatus] = { | ||||
|       getCommitStatues(repository.owner, repository.name, sha) | ||||
|     } | ||||
|  | ||||
|     def getSummary(statuses: List[CommitStatus]): (CommitState, String) = { | ||||
|       val stateMap = statuses.groupBy(_.state) | ||||
|       val state = CommitState.combine(stateMap.keySet) | ||||
|       val summary = stateMap.map { case (keyState, states) => s"${states.size} ${keyState.name}" }.mkString(", ") | ||||
|       state -> summary | ||||
|     } | ||||
|  | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { | ||||
|       git => | ||||
|         def getTags(sha: String): List[String] = { | ||||
|           JGitUtil.getTagsOnCommit(git, sha) | ||||
|         } | ||||
|  | ||||
|         JGitUtil.getCommitLog(git, branchName, page, 30, path) match { | ||||
|           case Right((logs, hasNext)) => | ||||
|             html.commits( | ||||
| @@ -277,34 +272,33 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|               repository, | ||||
|               logs | ||||
|                 .map { | ||||
|                   c => | ||||
|                   commit => | ||||
|                     ( | ||||
|                       CommitInfo( | ||||
|                       id = c.id, | ||||
|                       shortMessage = c.shortMessage, | ||||
|                       fullMessage = c.fullMessage, | ||||
|                       parents = c.parents, | ||||
|                       authorTime = c.authorTime, | ||||
|                       authorName = c.authorName, | ||||
|                       authorEmailAddress = c.authorEmailAddress, | ||||
|                       commitTime = c.commitTime, | ||||
|                       committerName = c.committerName, | ||||
|                       committerEmailAddress = c.committerEmailAddress, | ||||
|                       commitSign = c.commitSign, | ||||
|                       verified = c.commitSign | ||||
|                         .flatMap { s => | ||||
|                           GpgUtil.verifySign(s) | ||||
|                         } | ||||
|                         id = commit.id, | ||||
|                         shortMessage = commit.shortMessage, | ||||
|                         fullMessage = commit.fullMessage, | ||||
|                         parents = commit.parents, | ||||
|                         authorTime = commit.authorTime, | ||||
|                         authorName = commit.authorName, | ||||
|                         authorEmailAddress = commit.authorEmailAddress, | ||||
|                         commitTime = commit.commitTime, | ||||
|                         committerName = commit.committerName, | ||||
|                         committerEmailAddress = commit.committerEmailAddress, | ||||
|                         commitSign = commit.commitSign, | ||||
|                         verified = commit.commitSign.flatMap(GpgUtil.verifySign) | ||||
|                       ), | ||||
|                       JGitUtil.getTagsOnCommit(git, commit.id), | ||||
|                       getCommitStatusWithSummary(repository.owner, repository.name, commit.id) | ||||
|                     ) | ||||
|                 } | ||||
|                 .splitWith { (commit1, commit2) => | ||||
|                 .splitWith { | ||||
|                   case ((commit1, _, _), (commit2, _, _)) => | ||||
|                     view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) | ||||
|                 }, | ||||
|               page, | ||||
|               hasNext, | ||||
|               hasDeveloperRole(repository.owner, repository.name, context.loginAccount), | ||||
|               getStatuses, | ||||
|               getSummary, | ||||
|               getTags | ||||
|               hasDeveloperRole(repository.owner, repository.name, context.loginAccount) | ||||
|             ) | ||||
|           case Left(_) => NotFound() | ||||
|         } | ||||
| @@ -335,7 +329,16 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     val (branch, path) = repository.splitPath(multiParams("splat").head) | ||||
|     val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||
|       .needStatusCheck(context.loginAccount.get.userName) | ||||
|     html.upload(branch, repository, if (path.length == 0) Nil else path.split("/").toList, protectedBranch) | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|       val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) | ||||
|       html.upload( | ||||
|         branch, | ||||
|         repository, | ||||
|         if (path.length == 0) Nil else path.split("/").toList, | ||||
|         protectedBranch, | ||||
|         revCommit.name | ||||
|       ) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) => | ||||
| @@ -348,9 +351,25 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|       file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}") | ||||
|     } | ||||
|  | ||||
|     if (form.newBranch) { | ||||
|       val newBranchName = createNewBranchForPullRequest(repository, form.branch) | ||||
|       val objectId = _commit(newBranchName) | ||||
|       val issueId = | ||||
|         createIssueAndPullRequest(repository, form.branch, newBranchName, form.commit, objectId.name, form.message) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") | ||||
|     } else { | ||||
|       _commit(form.branch) | ||||
|       if (form.path.length == 0) { | ||||
|         redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}") | ||||
|       } else { | ||||
|         redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}") | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     def _commit(branchName: String): ObjectId = { | ||||
|       commitFiles( | ||||
|         repository = repository, | ||||
|       branch = form.branch, | ||||
|         branch = branchName, | ||||
|         path = form.path, | ||||
|         files = files.toIndexedSeq, | ||||
|         message = form.message.getOrElse("Add files via upload"), | ||||
| @@ -373,11 +392,6 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|             builder.finish() | ||||
|           } | ||||
|       } | ||||
|  | ||||
|     if (form.path.length == 0) { | ||||
|       redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}") | ||||
|     } else { | ||||
|       redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}") | ||||
|     } | ||||
|   }) | ||||
|  | ||||
| @@ -432,9 +446,24 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) => | ||||
|     if (form.newBranch) { | ||||
|       val newBranchName = createNewBranchForPullRequest(repository, form.branch) | ||||
|       val objectId = _commit(newBranchName) | ||||
|       val issueId = | ||||
|         createIssueAndPullRequest(repository, form.branch, newBranchName, form.commit, objectId.name, form.message) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") | ||||
|     } else { | ||||
|       _commit(form.branch) | ||||
|       redirect( | ||||
|         s"/${repository.owner}/${repository.name}/blob/${form.branch}/${if (form.path.length == 0) urlEncode(form.newFileName) | ||||
|         else s"${form.path}/${urlEncode(form.newFileName)}"}" | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     def _commit(branchName: String): ObjectId = { | ||||
|       commitFile( | ||||
|         repository = repository, | ||||
|       branch = form.branch, | ||||
|         branch = branchName, | ||||
|         path = form.path, | ||||
|         newFileName = Some(form.newFileName), | ||||
|         oldFileName = None, | ||||
| @@ -445,17 +474,28 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|         loginAccount = context.loginAccount.get, | ||||
|         settings = context.settings | ||||
|       ) | ||||
|  | ||||
|     redirect( | ||||
|       s"/${repository.owner}/${repository.name}/blob/${form.branch}/${if (form.path.length == 0) urlEncode(form.newFileName) | ||||
|       else s"${form.path}/${urlEncode(form.newFileName)}"}" | ||||
|     ) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) => | ||||
|     if (form.newBranch) { | ||||
|       val newBranchName = createNewBranchForPullRequest(repository, form.branch) | ||||
|       val objectId = _commit(newBranchName) | ||||
|       val issueId = | ||||
|         createIssueAndPullRequest(repository, form.branch, newBranchName, form.commit, objectId.name, form.message) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") | ||||
|     } else { | ||||
|       _commit(form.branch) | ||||
|       redirect( | ||||
|         s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName) | ||||
|         else s"${form.path}/${urlEncode(form.newFileName)}"}" | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     def _commit(branchName: String): ObjectId = { | ||||
|       commitFile( | ||||
|         repository = repository, | ||||
|       branch = form.branch, | ||||
|         branch = branchName, | ||||
|         path = form.path, | ||||
|         newFileName = Some(form.newFileName), | ||||
|         oldFileName = form.oldFileName, | ||||
| @@ -470,17 +510,28 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|         loginAccount = context.loginAccount.get, | ||||
|         settings = context.settings | ||||
|       ) | ||||
|  | ||||
|     redirect( | ||||
|       s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName) | ||||
|       else s"${form.path}/${urlEncode(form.newFileName)}"}" | ||||
|     ) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) => | ||||
|     if (form.newBranch) { | ||||
|       val newBranchName = createNewBranchForPullRequest(repository, form.branch) | ||||
|       val objectId = _commit(newBranchName) | ||||
|       val issueId = | ||||
|         createIssueAndPullRequest(repository, form.branch, newBranchName, form.commit, objectId.name, form.message) | ||||
|       redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}") | ||||
|     } else { | ||||
|       _commit(form.branch) | ||||
|       redirect( | ||||
|         s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) "" | ||||
|         else "/" + form.path}" | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     def _commit(branchName: String): ObjectId = { | ||||
|       commitFile( | ||||
|         repository = repository, | ||||
|       branch = form.branch, | ||||
|         branch = branchName, | ||||
|         path = form.path, | ||||
|         newFileName = None, | ||||
|         oldFileName = Some(form.fileName), | ||||
| @@ -491,12 +542,61 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|         loginAccount = context.loginAccount.get, | ||||
|         settings = context.settings | ||||
|       ) | ||||
|  | ||||
|     redirect( | ||||
|       s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) "" else "/" + form.path}" | ||||
|     ) | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   private def getNewBranchName(repository: RepositoryInfo): String = { | ||||
|     var i = 1 | ||||
|     val branchNamePrefix = cutTail(context.loginAccount.get.userName.replaceAll("[^a-zA-Z0-9-_]", "-"), 25) | ||||
|     while (repository.branchList.exists(p => p.contains(s"$branchNamePrefix-patch-$i"))) { | ||||
|       i += 1 | ||||
|     } | ||||
|     s"$branchNamePrefix-patch-$i" | ||||
|   } | ||||
|  | ||||
|   private def createNewBranchForPullRequest(repository: RepositoryInfo, baseBranchName: String): String = { | ||||
|     val newBranchName = getNewBranchName(repository) | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|       JGitUtil.createBranch(git, baseBranchName, newBranchName) | ||||
|     } | ||||
|     newBranchName | ||||
|   } | ||||
|  | ||||
|   private def createIssueAndPullRequest( | ||||
|     repository: RepositoryInfo, | ||||
|     baseBranch: String, | ||||
|     requestBranch: String, | ||||
|     commitIdFrom: String, | ||||
|     commitIdTo: String, | ||||
|     commitMessage: Option[String] | ||||
|   ): Int = { | ||||
|     val issueId = insertIssue( | ||||
|       owner = repository.owner, | ||||
|       repository = repository.name, | ||||
|       loginUser = context.loginAccount.get.userName, | ||||
|       title = requestBranch, | ||||
|       content = commitMessage, | ||||
|       assignedUserName = None, | ||||
|       milestoneId = None, | ||||
|       priorityId = None, | ||||
|       isPullRequest = true | ||||
|     ) | ||||
|     createPullRequest( | ||||
|       originRepository = repository, | ||||
|       issueId = issueId, | ||||
|       originBranch = baseBranch, | ||||
|       requestUserName = repository.owner, | ||||
|       requestRepositoryName = repository.name, | ||||
|       requestBranch = requestBranch, | ||||
|       commitIdFrom = commitIdFrom, | ||||
|       commitIdTo = commitIdTo, | ||||
|       isDraft = false, | ||||
|       loginAccount = context.loginAccount.get, | ||||
|       settings = context.settings | ||||
|     ) | ||||
|     issueId | ||||
|   } | ||||
|  | ||||
|   get("/:owner/:repository/raw/*")(referrersOnly { repository => | ||||
|     val (id, path) = repository.splitPath(multiParams("splat").head) | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
| @@ -514,6 +614,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|   val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository => | ||||
|     val (id, path) = repository.splitPath(multiParams("splat").head) | ||||
|     val raw = params.get("raw").getOrElse("false").toBoolean | ||||
|     val highlighterTheme = getSyntaxHighlighterTheme() | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { | ||||
|       git => | ||||
|         val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) | ||||
| @@ -533,13 +634,25 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|                 hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount), | ||||
|                 isBlame = request.paths(2) == "blame", | ||||
|                 isLfsFile = isLfsFile(git, objectId), | ||||
|                 tabSize = info.tabSize | ||||
|                 tabSize = info.tabSize, | ||||
|                 highlighterTheme = highlighterTheme | ||||
|               ) | ||||
|             } | ||||
|         } getOrElse NotFound() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   private def getSyntaxHighlighterTheme()(implicit context: Context): String = { | ||||
|     context.loginAccount match { | ||||
|       case Some(account) => | ||||
|         getAccountPreference(account.userName) match { | ||||
|           case Some(x) => x.highlighterTheme | ||||
|           case _       => "github-v2" | ||||
|         } | ||||
|       case _ => "github-v2" | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private def isLfsFile(git: Git, objectId: ObjectId): Boolean = { | ||||
|     JGitUtil.getObjectLoaderFromId(git, objectId)(JGitUtil.isLfsPointer).getOrElse(false) | ||||
|   } | ||||
| @@ -601,6 +714,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|                 new JGitUtil.CommitInfo(revCommit), | ||||
|                 JGitUtil.getBranchesOfCommit(git, revCommit.getName), | ||||
|                 JGitUtil.getTagsOfCommit(git, revCommit.getName), | ||||
|                 getCommitStatusWithSummary(repository.owner, repository.name, revCommit.getName), | ||||
|                 getCommitComments(repository.owner, repository.name, id, true), | ||||
|                 repository, | ||||
|                 diffs, | ||||
| @@ -759,19 +873,20 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|             defaultBranch = repository.repository.defaultBranch, | ||||
|             origin = repository.repository.originUserName.isEmpty | ||||
|           ) | ||||
|           .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime)) | ||||
|           .sortBy(branch => (branch.mergeInfo.isEmpty, branch.commitTime)) | ||||
|           .map( | ||||
|             br => | ||||
|             branch => | ||||
|               ( | ||||
|                 br, | ||||
|                 branch, | ||||
|                 getPullRequestByRequestCommit( | ||||
|                   repository.owner, | ||||
|                   repository.name, | ||||
|                   repository.repository.defaultBranch, | ||||
|                   br.name, | ||||
|                   br.commitId | ||||
|                   branch.name, | ||||
|                   branch.commitId | ||||
|                 ), | ||||
|                 protectedBranches.contains(br.name) | ||||
|                 protectedBranches.contains(branch.name), | ||||
|                 getCommitStatusWithSummary(repository.owner, repository.name, branch.commitId) | ||||
|             ) | ||||
|           ) | ||||
|           .reverse | ||||
| @@ -832,7 +947,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     if (repository.repository.defaultBranch != branchName) { | ||||
|       Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|         git.branchDelete().setForce(true).setBranchNames(branchName).call() | ||||
|         recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName) | ||||
|         val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, branchName) | ||||
|         recordActivity(deleteBranchInfo) | ||||
|       } | ||||
|     } | ||||
|     redirect(s"/${repository.owner}/${repository.name}/branches") | ||||
| @@ -905,10 +1021,6 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|     lazy val isValid: Boolean = fileIds.nonEmpty | ||||
|   } | ||||
|  | ||||
|   private val readmeFiles = PluginRegistry().renderableExtensions.map { extension => | ||||
|     s"readme.${extension}" | ||||
|   } ++ Seq("readme.txt", "readme") | ||||
|  | ||||
|   /** | ||||
|    * Provides HTML of the file list. | ||||
|    * | ||||
| @@ -930,12 +1042,19 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|                 if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) | ||||
|               val commitCount = JGitUtil.getCommitCount(git, lastModifiedCommit.getName) | ||||
|               // get files | ||||
|               val files = JGitUtil.getFileList(git, revision, path, context.settings.baseUrl, commitCount) | ||||
|               val files = JGitUtil.getFileList( | ||||
|                 git, | ||||
|                 revision, | ||||
|                 path, | ||||
|                 context.settings.baseUrl, | ||||
|                 commitCount, | ||||
|                 context.settings.repositoryViewer.maxFiles | ||||
|               ) | ||||
|               val parentPath = if (path == ".") Nil else path.split("/").toList | ||||
|               // process README.md or README.markdown | ||||
|               val readme = files | ||||
|               // process README | ||||
|               val readme = files // files should be sorted alphabetically. | ||||
|                 .find { file => | ||||
|                   !file.isDirectory && readmeFiles.contains(file.name.toLowerCase) | ||||
|                   !file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase) | ||||
|                 } | ||||
|                 .map { file => | ||||
|                   val path = (file.name :: parentPath.reverse).reverse | ||||
| @@ -951,6 +1070,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { | ||||
|                 repository, | ||||
|                 if (path == ".") Nil else path.split("/").toList, // current path | ||||
|                 new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit | ||||
|                 getCommitStatusWithSummary(repository.owner, repository.name, lastModifiedCommit.getName), | ||||
|                 commitCount, | ||||
|                 files, | ||||
|                 readme, | ||||
|   | ||||
| @@ -48,7 +48,6 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase { | ||||
|     )(RepositoryOperation.apply), | ||||
|     "gravatar" -> trim(label("Gravatar", boolean())), | ||||
|     "notification" -> trim(label("Notification", boolean())), | ||||
|     "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), | ||||
|     "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), | ||||
|     "ssh" -> mapping( | ||||
|       "enabled" -> trim(label("SSH access", boolean())), | ||||
| @@ -109,7 +108,10 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase { | ||||
|       "timeout" -> trim(label("Timeout", long(required))), | ||||
|       "largeMaxFileSize" -> trim(label("Max file size for large file", long(required))), | ||||
|       "largeTimeout" -> trim(label("Timeout for large file", long(required))) | ||||
|     )(Upload.apply) | ||||
|     )(Upload.apply), | ||||
|     "repositoryViewer" -> mapping( | ||||
|       "maxFiles" -> trim(label("Max files", number(required))) | ||||
|     )(RepositoryViewerSettings.apply) | ||||
|   )(SystemSettings.apply).verifying { settings => | ||||
|     Vector( | ||||
|       if (settings.ssh.enabled && settings.baseUrl.isEmpty) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package gitbucket.core.controller | ||||
|  | ||||
| import gitbucket.core.model.WebHook | ||||
| import gitbucket.core.model.activity.{CreateWikiPageInfo, DeleteWikiInfo, EditWikiPageInfo} | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.service.WebHookService.WebHookGollumPayload | ||||
| import gitbucket.core.wiki.html | ||||
| @@ -13,6 +14,7 @@ import gitbucket.core.util.Directory._ | ||||
| import org.scalatra.forms._ | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.scalatra.i18n.Messages | ||||
|  | ||||
| import scala.util.Using | ||||
|  | ||||
| class WikiController | ||||
| @@ -24,6 +26,7 @@ class WikiController | ||||
|     with WebHookService | ||||
|     with ReadableUsersAuthenticator | ||||
|     with ReferrerAuthenticator | ||||
|     with RequestCache | ||||
|  | ||||
| trait WikiControllerBase extends ControllerBase { | ||||
|   self: WikiService | ||||
| @@ -184,13 +187,9 @@ trait WikiControllerBase extends ControllerBase { | ||||
|           ).foreach { | ||||
|             commitId => | ||||
|               updateLastActivityDate(repository.owner, repository.name) | ||||
|               recordEditWikiPageActivity( | ||||
|                 repository.owner, | ||||
|                 repository.name, | ||||
|                 loginAccount.userName, | ||||
|                 form.pageName, | ||||
|                 commitId | ||||
|               ) | ||||
|               val wikiEditInfo = | ||||
|                 EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) | ||||
|               recordActivity(wikiEditInfo) | ||||
|               callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) { | ||||
|                 getAccountByUserName(repository.owner).map { repositoryUser => | ||||
|                   WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount) | ||||
| @@ -228,7 +227,9 @@ trait WikiControllerBase extends ControllerBase { | ||||
|           ).foreach { | ||||
|             commitId => | ||||
|               updateLastActivityDate(repository.owner, repository.name) | ||||
|               recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) | ||||
|               val createWikiPageInfo = | ||||
|                 CreateWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName) | ||||
|               recordActivity(createWikiPageInfo) | ||||
|               callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) { | ||||
|                 getAccountByUserName(repository.owner).map { repositoryUser => | ||||
|                   WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount) | ||||
| @@ -250,14 +251,13 @@ trait WikiControllerBase extends ControllerBase { | ||||
|       val pageName = StringUtil.urlDecode(params("page")) | ||||
|  | ||||
|       defining(context.loginAccount.get) { loginAccount => | ||||
|         deleteWikiPage( | ||||
|         val deleteWikiInfo = DeleteWikiInfo( | ||||
|           repository.owner, | ||||
|           repository.name, | ||||
|           pageName, | ||||
|           loginAccount.fullName, | ||||
|           loginAccount.mailAddress, | ||||
|           s"Destroyed ${pageName}" | ||||
|           loginAccount.userName, | ||||
|           pageName | ||||
|         ) | ||||
|         recordActivity(deleteWikiInfo) | ||||
|         updateLastActivityDate(repository.owner, repository.name) | ||||
|  | ||||
|         redirect(s"/${repository.owner}/${repository.name}/wiki") | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| package gitbucket.core.controller.api | ||||
| import gitbucket.core.api.{ApiObject, ApiRef, JsonFormat} | ||||
| import gitbucket.core.api.{ApiObject, ApiRef, CreateARef, JsonFormat, UpdateARef} | ||||
| import gitbucket.core.controller.ControllerBase | ||||
| import gitbucket.core.util.Directory.getRepositoryDir | ||||
| import gitbucket.core.util.ReferrerAuthenticator | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.lib.ObjectId | ||||
| import org.eclipse.jgit.lib.RefUpdate.Result | ||||
| import org.scalatra.{BadRequest, NoContent, UnprocessableEntity} | ||||
| import org.slf4j.LoggerFactory | ||||
|  | ||||
| import scala.jdk.CollectionConverters._ | ||||
| @@ -17,7 +20,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase { | ||||
|  | ||||
|   /* | ||||
|    * i. Get a reference | ||||
|    * https://developer.github.com/v3/git/refs/#get-a-reference | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository => | ||||
|     getRef() | ||||
| @@ -55,21 +58,79 @@ trait ApiGitReferenceControllerBase extends ControllerBase { | ||||
|  | ||||
|   /* | ||||
|    * ii. Get all references | ||||
|    * https://developer.github.com/v3/git/refs/#get-all-references | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#list-matching-references | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * iii. Create a reference | ||||
|    * https://developer.github.com/v3/git/refs/#create-a-reference | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference | ||||
|    */ | ||||
|   post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { _ => | ||||
|     extractFromJsonBody[CreateARef].map { | ||||
|       data => | ||||
|         Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => | ||||
|           val ref = git.getRepository.findRef(data.ref) | ||||
|           if (ref == null) { | ||||
|             val update = git.getRepository.updateRef(data.ref) | ||||
|             update.setNewObjectId(ObjectId.fromString(data.sha)) | ||||
|             val result = update.update() | ||||
|             result match { | ||||
|               case Result.NEW => JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName))) | ||||
|               case _          => UnprocessableEntity(result.name()) | ||||
|             } | ||||
|           } else { | ||||
|             UnprocessableEntity("Ref already exists.") | ||||
|           } | ||||
|         } | ||||
|     } getOrElse BadRequest() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iv. Update a reference | ||||
|    * https://developer.github.com/v3/git/refs/#update-a-reference | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference | ||||
|    */ | ||||
|   patch("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ => | ||||
|     val refName = multiParams("splat").mkString("/") | ||||
|     extractFromJsonBody[UpdateARef].map { | ||||
|       data => | ||||
|         Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => | ||||
|           val ref = git.getRepository.findRef(refName) | ||||
|           if (ref == null) { | ||||
|             UnprocessableEntity("Ref does not exist.") | ||||
|           } else { | ||||
|             val update = git.getRepository.updateRef(ref.getName) | ||||
|             update.setNewObjectId(ObjectId.fromString(data.sha)) | ||||
|             update.setForceUpdate(data.force) | ||||
|             val result = update.update() | ||||
|             result match { | ||||
|               case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE => | ||||
|                 JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName))) | ||||
|               case _ => UnprocessableEntity(result.name()) | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|     } getOrElse BadRequest() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * v. Delete a reference | ||||
|  * https://developer.github.com/v3/git/refs/#delete-a-reference | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference | ||||
|    */ | ||||
|   delete("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ => | ||||
|     val refName = multiParams("splat").mkString("/") | ||||
|     Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => | ||||
|       val ref = git.getRepository.findRef(refName) | ||||
|       if (ref == null) { | ||||
|         UnprocessableEntity("Ref does not exist.") | ||||
|       } else { | ||||
|         val update = git.getRepository.updateRef(ref.getName) | ||||
|         update.setForceUpdate(true) | ||||
|         val result = update.delete() | ||||
|         result match { | ||||
|           case Result.FORCED => NoContent() | ||||
|           case _             => UnprocessableEntity(result.name()) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import gitbucket.core.controller.{Context, ControllerBase} | ||||
| import gitbucket.core.service._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName} | ||||
| import org.scalatra.{ActionResult, NoContent} | ||||
|  | ||||
| trait ApiIssueCommentControllerBase extends ControllerBase { | ||||
|   self: AccountService | ||||
| @@ -14,8 +15,8 @@ trait ApiIssueCommentControllerBase extends ControllerBase { | ||||
|     with ReadableUsersAuthenticator | ||||
|     with ReferrerAuthenticator => | ||||
|   /* | ||||
|    * i. List comments on an issue | ||||
|    * https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue | ||||
|    * i. List issue comments for a repository | ||||
|    * https://docs.github.com/en/rest/reference/issues#list-issue-comments-for-a-repository | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository => | ||||
|     (for { | ||||
| @@ -30,18 +31,90 @@ trait ApiIssueCommentControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * ii. List comments in a repository | ||||
|    * https://developer.github.com/v3/issues/comments/#list-comments-in-a-repository | ||||
|    * ii. Get an issue comment | ||||
|    * https://docs.github.com/en/rest/reference/issues#get-an-issue-comment | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/issues/comments/:id")(referrersOnly { repository => | ||||
|     val commentId = params("id").toInt | ||||
|     getCommentForApi(repository.owner, repository.name, commentId) match { | ||||
|       case Some((issueComment, user, issue)) => | ||||
|         JsonFormat( | ||||
|           ApiComment(issueComment, RepositoryName(repository), issue.issueId, ApiUser(user), issue.isPullRequest) | ||||
|         ) | ||||
|       case _ => NotFound() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iii. Update an issue comment | ||||
|    * https://docs.github.com/en/rest/reference/issues#update-an-issue-comment | ||||
|    */ | ||||
|   patch("/api/v3/repos/:owner/:repository/issues/comments/:id")(readableUsersOnly { repository => | ||||
|     val commentId = params("id") | ||||
|     val result = for { | ||||
|       issueComment <- getComment(repository.owner, repository.name, commentId) | ||||
|       issue <- getIssue(repository.owner, repository.name, issueComment.issueId.toString) | ||||
|     } yield { | ||||
|       if (isEditable(repository.owner, repository.name, issueComment.commentedUserName)) { | ||||
|         val body = extractFromJsonBody[CreateAComment].map(_.body) | ||||
|         updateCommentByApi(repository, issue, issueComment.commentId.toString, body) | ||||
|         getComment(repository.owner, repository.name, commentId) match { | ||||
|           case Some(issueComment) => | ||||
|             JsonFormat( | ||||
|               ApiComment( | ||||
|                 issueComment, | ||||
|                 RepositoryName(repository), | ||||
|                 issue.issueId, | ||||
|                 ApiUser(context.loginAccount.get), | ||||
|                 issue.isPullRequest | ||||
|               ) | ||||
|             ) | ||||
|           case _ => | ||||
|         } | ||||
|       } else { | ||||
|         Unauthorized() | ||||
|       } | ||||
|     } | ||||
|     result match { | ||||
|       case Some(response) => response | ||||
|       case None           => NotFound() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iv. Delete a comment | ||||
|    * https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment | ||||
|    */ | ||||
|   delete("/api/v3/repos/:owner/:repo/issues/comments/:id")(readableUsersOnly { repository => | ||||
|     val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] = | ||||
|       for { | ||||
|         commentId <- params("id").toIntOpt | ||||
|         comment <- getComment(repository.owner, repository.name, commentId.toString) | ||||
|         issue <- getIssue(repository.owner, repository.name, comment.issueId.toString) | ||||
|       } yield { | ||||
|         if (isEditable(repository.owner, repository.name, comment.commentedUserName)) { | ||||
|           val maybeDeletedComment = deleteCommentByApi(repository, comment, issue) | ||||
|           Right(maybeDeletedComment.map(_.commentId)) | ||||
|         } else { | ||||
|           Left(Unauthorized()) | ||||
|         } | ||||
|       } | ||||
|     maybeDeleteResponse | ||||
|       .map { | ||||
|         case Right(maybeDeletedCommentId) => maybeDeletedCommentId.getOrElse(NotFound()) | ||||
|         case Left(err)                    => err | ||||
|       } | ||||
|       .getOrElse(NotFound()) | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * v. List issue comments | ||||
|    * https://docs.github.com/en/rest/reference/issues#list-issue-comments | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * iii. Get a single comment | ||||
|    * https://developer.github.com/v3/issues/comments/#get-a-single-comment | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * iv. Create a comment | ||||
|    * https://developer.github.com/v3/issues/comments/#create-a-comment | ||||
|    * vi. Create an issue comment | ||||
|    * https://docs.github.com/en/rest/reference/issues#create-an-issue-comment | ||||
|    */ | ||||
|   post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository => | ||||
|     (for { | ||||
| @@ -64,16 +137,6 @@ trait ApiIssueCommentControllerBase extends ControllerBase { | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * v. Edit a comment | ||||
|    * https://developer.github.com/v3/issues/comments/#edit-a-comment | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * vi. Delete a comment | ||||
|    * https://developer.github.com/v3/issues/comments/#delete-a-comment | ||||
|    */ | ||||
|  | ||||
|   private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = | ||||
|     hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,118 @@ | ||||
| package gitbucket.core.controller.api | ||||
| import gitbucket.core.api._ | ||||
| import gitbucket.core.controller.ControllerBase | ||||
| import gitbucket.core.service.MilestonesService | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import org.scalatra.NoContent | ||||
|  | ||||
| trait ApiIssueMilestoneControllerBase extends ControllerBase { | ||||
|   self: MilestonesService with WritableUsersAuthenticator with ReferrerAuthenticator => | ||||
|  | ||||
|   /* | ||||
|    * i. List milestones | ||||
|    * https://docs.github.com/en/rest/reference/issues#list-milestones | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/milestones")(referrersOnly { repository => | ||||
|     val state = params.getOrElse("state", "all") | ||||
|     // TODO "sort", "direction" params should be implemented. | ||||
|     val apiMilestones = (for (milestoneWithIssue <- getMilestonesWithIssueCount(repository.owner, repository.name) | ||||
|                                 .sortBy(p => p._1.milestoneId)) | ||||
|       yield { | ||||
|         ApiMilestone( | ||||
|           repository.repository, | ||||
|           milestoneWithIssue._1, | ||||
|           milestoneWithIssue._2, | ||||
|           milestoneWithIssue._3 | ||||
|         ) | ||||
|       }).reverse | ||||
|     state match { | ||||
|       case "all" => JsonFormat(apiMilestones) | ||||
|       case "open" | "closed" => | ||||
|         JsonFormat( | ||||
|           apiMilestones.filter(p => p.state == state) | ||||
|         ) | ||||
|       case _ => NotFound() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * ii. Create a milestone | ||||
|    * https://docs.github.com/en/rest/reference/issues#create-a-milestone | ||||
|    */ | ||||
|   post("/api/v3/repos/:owner/:repository/milestones")(writableUsersOnly { repository => | ||||
|     (for { | ||||
|       data <- extractFromJsonBody[CreateAMilestone] if data.isValid | ||||
|       milestoneId = createMilestone( | ||||
|         repository.owner, | ||||
|         repository.name, | ||||
|         data.title, | ||||
|         data.description, | ||||
|         data.due_on | ||||
|       ) | ||||
|       apiMilestone <- getApiMilestone(repository, milestoneId) | ||||
|     } yield { | ||||
|       JsonFormat(apiMilestone) | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iii. Get a milestone | ||||
|    * https://docs.github.com/en/rest/reference/issues#get-a-milestone | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/milestones/:number")(referrersOnly { repository => | ||||
|     val milestoneId = params("number").toInt // use milestoneId as number | ||||
|     (for (apiMilestone <- getApiMilestone(repository, milestoneId)) yield { | ||||
|       JsonFormat(apiMilestone) | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iv.Update a milestone | ||||
|    * https://docs.github.com/en/rest/reference/issues#update-a-milestone | ||||
|    */ | ||||
|   patch("/api/v3/repos/:owner/:repository/milestones/:number")(writableUsersOnly { repository => | ||||
|     val milestoneId = params("number").toInt | ||||
|     (for { | ||||
|       data <- extractFromJsonBody[CreateAMilestone] if data.isValid | ||||
|       milestone <- getMilestone(repository.owner, repository.name, milestoneId) | ||||
|       _ = (data.state, milestone.closedDate) match { | ||||
|         case ("open", Some(_)) => | ||||
|           openMilestone(milestone) | ||||
|         case ("closed", None) => | ||||
|           closeMilestone(milestone) | ||||
|         case _ => | ||||
|       } | ||||
|       milestone <- getMilestone(repository.owner, repository.name, milestoneId) | ||||
|       _ = updateMilestone(milestone.copy(title = data.title, description = data.description, dueDate = data.due_on)) | ||||
|       apiMilestone <- getApiMilestone(repository, milestoneId) | ||||
|     } yield { | ||||
|       JsonFormat(apiMilestone) | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * v. Delete a milestone | ||||
|    * https://docs.github.com/en/rest/reference/issues#delete-a-milestone | ||||
|    */ | ||||
|   delete("/api/v3/repos/:owner/:repository/milestones/:number")(writableUsersOnly { repository => | ||||
|     val milestoneId = params("number").toInt // use milestoneId as number | ||||
|     deleteMilestone(repository.owner, repository.name, milestoneId) | ||||
|     NoContent() | ||||
|   }) | ||||
|  | ||||
|   private def getApiMilestone(repository: RepositoryInfo, milestoneId: Int): Option[ApiMilestone] = { | ||||
|     getMilestonesWithIssueCount(repository.owner, repository.name) | ||||
|       .find(p => p._1.milestoneId == milestoneId) | ||||
|       .map( | ||||
|         milestoneWithIssue => | ||||
|           ApiMilestone( | ||||
|             repository.repository, | ||||
|             milestoneWithIssue._1, | ||||
|             milestoneWithIssue._2, | ||||
|             milestoneWithIssue._3 | ||||
|         ) | ||||
|       ) | ||||
|   } | ||||
| } | ||||
| @@ -10,7 +10,7 @@ import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.JGitUtil.CommitInfo | ||||
| import gitbucket.core.util._ | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.scalatra.NoContent | ||||
| import org.scalatra.{Conflict, MethodNotAllowed, NoContent, Ok} | ||||
| import scala.util.Using | ||||
|  | ||||
| import scala.jdk.CollectionConverters._ | ||||
| @@ -161,8 +161,28 @@ trait ApiPullRequestControllerBase extends ControllerBase { | ||||
|  | ||||
|   /* | ||||
|    * v. Update a pull request | ||||
|    * https://developer.github.com/v3/pulls/#update-a-pull-request | ||||
|    * https://docs.github.com/en/rest/reference/pulls#update-a-pull-request | ||||
|    */ | ||||
|   patch("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository => | ||||
|     (for { | ||||
|       issueId <- params("id").toIntOpt | ||||
|       account <- context.loginAccount | ||||
|       settings = context.settings | ||||
|       data <- extractFromJsonBody[UpdateAPullRequest] | ||||
|     } yield { | ||||
|       updatePullRequestsByApi( | ||||
|         repository, | ||||
|         issueId, | ||||
|         account, | ||||
|         settings, | ||||
|         data.title, | ||||
|         data.body, | ||||
|         data.state, | ||||
|         data.base | ||||
|       ) | ||||
|       JsonFormat(getApiPullRequest(repository, issueId)) | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * vi. List commits on a pull request | ||||
| @@ -217,8 +237,72 @@ trait ApiPullRequestControllerBase extends ControllerBase { | ||||
|  | ||||
|   /* | ||||
|    * ix. Merge a pull request (Merge Button) | ||||
|    * https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button | ||||
|    * https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request | ||||
|    */ | ||||
|   put("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository => | ||||
|     (for { | ||||
|       //TODO: crash when body is empty | ||||
|       //TODO: Implement sha parameter | ||||
|       data <- extractFromJsonBody[MergeAPullRequest] | ||||
|       issueId <- params("id").toIntOpt | ||||
|       (issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId) | ||||
|     } yield { | ||||
|       if (checkConflict(repository.owner, repository.name, pullReq.branch, issueId).isDefined) { | ||||
|         Conflict( | ||||
|           JsonFormat( | ||||
|             FailToMergePrResponse( | ||||
|               message = "Head branch was modified. Review and try the merge again.", | ||||
|               documentation_url = "https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request", | ||||
|             ) | ||||
|           ) | ||||
|         ) | ||||
|       } else { | ||||
|         if (issue.closed) { | ||||
|           MethodNotAllowed( | ||||
|             JsonFormat( | ||||
|               FailToMergePrResponse( | ||||
|                 message = "Pull Request is not mergeable, Closed", | ||||
|                 documentation_url = "https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request", | ||||
|               ) | ||||
|             ) | ||||
|           ) | ||||
|         } else { | ||||
|           val strategy = | ||||
|             if (data.merge_method.getOrElse("merge-commit") == "merge") "merge-commit" | ||||
|             else data.merge_method.getOrElse("merge-commit") | ||||
|           mergePullRequest( | ||||
|             repository, | ||||
|             issueId, | ||||
|             context.loginAccount.get, | ||||
|             data.commit_message.getOrElse(""), //TODO: Implement commit_title | ||||
|             strategy, | ||||
|             pullReq.isDraft, | ||||
|             context.settings | ||||
|           ) match { | ||||
|             case Right(objectId) => | ||||
|               Ok( | ||||
|                 JsonFormat( | ||||
|                   SuccessToMergePrResponse( | ||||
|                     sha = objectId.toString, | ||||
|                     merged = true, | ||||
|                     message = "Pull Request successfully merged" | ||||
|                   ) | ||||
|                 ) | ||||
|               ) | ||||
|             case Left(message) => | ||||
|               MethodNotAllowed( | ||||
|                 JsonFormat( | ||||
|                   FailToMergePrResponse( | ||||
|                     message = "Pull Request is not mergeable", | ||||
|                     documentation_url = "https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request", | ||||
|                   ) | ||||
|                 ) | ||||
|               ) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * x. Labels, assignees, and milestones | ||||
|   | ||||
| @@ -7,6 +7,8 @@ import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.JGitUtil.getBranches | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.scalatra.NoContent | ||||
|  | ||||
| import scala.util.Using | ||||
|  | ||||
| trait ApiRepositoryBranchControllerBase extends ControllerBase { | ||||
| @@ -22,7 +24,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase { | ||||
|  | ||||
|   /** | ||||
|    * i. List branches | ||||
|    * https://developer.github.com/v3/repos/branches/#list-branches | ||||
|    * https://docs.github.com/en/rest/reference/repos#list-branches | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/branches")(referrersOnly { repository => | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
| @@ -41,8 +43,8 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * ii. Get branch | ||||
|    * https://developer.github.com/v3/repos/branches/#get-branch | ||||
|    * ii. Get a branch | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-a-branch | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/branches/*")(referrersOnly { repository => | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { | ||||
| @@ -65,147 +67,206 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase { | ||||
|  | ||||
|   /* | ||||
|    * iii. Get branch protection | ||||
|    * https://developer.github.com/v3/repos/branches/#get-branch-protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-branch-protection | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/branches/:branch/protection")(referrersOnly { repository => | ||||
|     val branch = params("branch") | ||||
|     if (repository.branchList.contains(branch)) { | ||||
|       val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||
|       JsonFormat( | ||||
|         ApiBranchProtection(protection) | ||||
|       ) | ||||
|     } else { NotFound() } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iv. Update branch protection | ||||
|    * https://developer.github.com/v3/repos/branches/#update-branch-protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#update-branch-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * v. Remove branch protection | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-branch-protection | ||||
|    * v. Delete branch protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#delete-branch-protection | ||||
|    */ | ||||
|   delete("/api/v3/repos/:owner/:repository/branches/:branch/protection")(writableUsersOnly { repository => | ||||
|     val branch = params("branch") | ||||
|     if (repository.branchList.contains(branch)) { | ||||
|       val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||
|       if (protection.enabled) { | ||||
|         disableBranchProtection(repository.owner, repository.name, branch) | ||||
|         NoContent() | ||||
|       } else NotFound() | ||||
|     } else NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * vi. Get admin branch protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-admin-branch-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * vi. Get required status checks of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#get-required-status-checks-of-protected-branch | ||||
|    * vii. Set admin branch protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#set-admin-branch-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * vii. Update required status checks of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#update-required-status-checks-of-protected-branch | ||||
|    * viii. Delete admin branch protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#delete-admin-branch-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * viii. Remove required status checks of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-status-checks-of-protected-branch | ||||
|    * ix. Get pull request review protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-pull-request-review-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * ix. List required status checks contexts of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#list-required-status-checks-contexts-of-protected-branch | ||||
|    * x. Update pull request review protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#update-pull-request-review-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * x. Replace required status checks contexts of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#replace-required-status-checks-contexts-of-protected-branch | ||||
|    * xi. Delete pull request review protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#delete-pull-request-review-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xi. Add required status checks contexts of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-status-checks-contexts-of-protected-branch | ||||
|    * xii. Get commit signature protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-commit-signature-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xii. Remove required status checks contexts of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-status-checks-contexts-of-protected-branch | ||||
|    * xiii. Create commit signature protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#create-commit-signature-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xiii. Get pull request review enforcement of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#get-pull-request-review-enforcement-of-protected-branch | ||||
|    * xiv. Delete commit signature protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#delete-commit-signature-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xiv. Update pull request review enforcement of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#update-pull-request-review-enforcement-of-protected-branch | ||||
|    * xv. Get status checks protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-status-checks-protection | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/branches/:branch/protection/required_status_checks")(referrersOnly { | ||||
|     repository => | ||||
|       val branch = params("branch") | ||||
|       if (repository.branchList.contains(branch)) { | ||||
|         val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||
|         JsonFormat( | ||||
|           ApiBranchProtection(protection).required_status_checks | ||||
|         ) | ||||
|       } else { NotFound() } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * xvi. Update status check protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#update-status-check-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xv. Remove pull request review enforcement of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-pull-request-review-enforcement-of-protected-branch | ||||
|    * xvii. Remove status check protection | ||||
|    * https://docs.github.com/en/rest/reference/repos#remove-status-check-protection | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xvi. Get required signatures of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#get-required-signatures-of-protected-branch | ||||
|    * xviii. Get all status check contexts | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-all-status-check-contexts | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/branches/:branch/protection/required_status_checks/contexts")(referrersOnly { | ||||
|     repository => | ||||
|       val branch = params("branch") | ||||
|       if (repository.branchList.contains(branch)) { | ||||
|         val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||
|         if (protection.enabled) { | ||||
|           protection.contexts.toList | ||||
|         } else NotFound() | ||||
|       } else NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * xix. Add status check contexts | ||||
|    * https://docs.github.com/en/rest/reference/repos#add-status-check-contexts | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xvii. Add required signatures of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#add-required-signatures-of-protected-branch | ||||
|    * xx. Set status check contexts | ||||
|    * https://docs.github.com/en/rest/reference/repos#set-status-check-contexts | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xviii. Remove required signatures of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-signatures-of-protected-branch | ||||
|    * xxi. Remove status check contexts | ||||
|    * https://docs.github.com/en/rest/reference/repos#remove-status-check-contexts | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xix. Get admin enforcement of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#get-admin-enforcement-of-protected-branch | ||||
|    * xxii. Get access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xx. Add admin enforcement of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#add-admin-enforcement-of-protected-branch | ||||
|    * xxiii. Delete access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#delete-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxi. Remove admin enforcement of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-admin-enforcement-of-protected-branch | ||||
|    * xxiv. Get apps with access to the protected branch | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-apps-with-access-to-the-protected-branch | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxii. Get restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#get-restrictions-of-protected-branch | ||||
|    * xxv. Add app access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#add-app-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxiii. Remove restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-restrictions-of-protected-branch | ||||
|    * xxvi. Set app access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#set-app-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxiv. List team restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#list-team-restrictions-of-protected-branch | ||||
|    * xxvii. Remove app access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#remove-app-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxv. Replace team restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#replace-team-restrictions-of-protected-branch | ||||
|    * xxviii. Get teams with access to the protected branch | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-teams-with-access-to-the-protected-branch | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxvi. Add team restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#add-team-restrictions-of-protected-branch | ||||
|    * xxix. Add team access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#add-team-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxvii. Remove team restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-team-restrictions-of-protected-branch | ||||
|    * xxx. Set team access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#set-team-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxviii. List user restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#list-user-restrictions-of-protected-branch | ||||
|    * xxxi. Remove team access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#remove-team-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxix. Replace user restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#replace-user-restrictions-of-protected-branch | ||||
|    * xxxii. Get users with access to the protected branch | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-users-with-access-to-the-protected-branch | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxx. Add user restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#add-user-restrictions-of-protected-branch | ||||
|    * xxxiii. Add user access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#add-user-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxxi. Remove user restrictions of protected branch | ||||
|    * https://developer.github.com/v3/repos/branches/#remove-user-restrictions-of-protected-branch | ||||
|    * xxxiv. Set user access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#set-user-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xxxv. Remove user access restrictions | ||||
|    * https://docs.github.com/en/rest/reference/repos#remove-user-access-restrictions | ||||
|    */ | ||||
|  | ||||
|   /** | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| package gitbucket.core.controller.api | ||||
| import gitbucket.core.api.{AddACollaborator, ApiUser, JsonFormat} | ||||
| import gitbucket.core.api.{AddACollaborator, ApiRepositoryCollaborator, ApiUser, JsonFormat} | ||||
| import gitbucket.core.controller.ControllerBase | ||||
| import gitbucket.core.service.{AccountService, RepositoryService} | ||||
| import gitbucket.core.util.Implicits._ | ||||
| @@ -10,8 +10,8 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { | ||||
|   self: RepositoryService with AccountService with ReferrerAuthenticator with OwnerAuthenticator => | ||||
|  | ||||
|   /* | ||||
|    * i. List collaborators | ||||
|    * https://developer.github.com/v3/repos/collaborators/#list-collaborators | ||||
|    * i. List repository collaborators | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#list-repository-collaborators | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/collaborators")(referrersOnly { repository => | ||||
|     // TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members. | ||||
| @@ -19,19 +19,40 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { | ||||
|       getCollaboratorUserNames(params("owner"), params("repository")).map(u => ApiUser(getAccountByUserName(u).get)) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * ii. Check if a user is a collaborator | ||||
|    * https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#check-if-a-user-is-a-repository-collaborator | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/collaborators/:userName")(referrersOnly { repository => | ||||
|     (for (account <- getAccountByUserName(params("userName"))) yield { | ||||
|       if (getCollaboratorUserNames(repository.owner, repository.name).contains(account.userName)) { | ||||
|         NoContent() | ||||
|       } else { | ||||
|         NotFound() | ||||
|       } | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iii. Review a user's permission level | ||||
|    * https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level | ||||
|    * iii. Get repository permissions for a user | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-repository-permissions-for-a-user | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/collaborators/:userName/permission")(referrersOnly { repository => | ||||
|     (for { | ||||
|       account <- getAccountByUserName(params("userName")) | ||||
|       collaborator <- getCollaborators(repository.owner, repository.name) | ||||
|         .find(p => p._1.collaboratorName == account.userName) | ||||
|     } yield { | ||||
|       JsonFormat( | ||||
|         ApiRepositoryCollaborator(collaborator._1.role, ApiUser(account)) | ||||
|       ) | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iv. Add user as a collaborator | ||||
|    * https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator | ||||
|    * iv. Add a repository collaborator | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#add-a-repository-collaborator | ||||
|    * requested #1586 | ||||
|    */ | ||||
|   put("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => | ||||
| @@ -44,8 +65,8 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * v. Remove user as a collaborator | ||||
|    * https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator | ||||
|    * v. Remove a repository collaborator | ||||
|    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#remove-a-repository-collaborator | ||||
|    * requested #1586 | ||||
|    */ | ||||
|   delete("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => | ||||
|   | ||||
| @@ -1,19 +1,20 @@ | ||||
| package gitbucket.core.controller.api | ||||
| import gitbucket.core.api.{ApiCommits, JsonFormat} | ||||
| import gitbucket.core.api.{ApiBranchCommit, ApiBranchForHeadCommit, ApiCommits, JsonFormat} | ||||
| import gitbucket.core.controller.ControllerBase | ||||
| import gitbucket.core.model.Account | ||||
| import gitbucket.core.service.{AccountService, CommitsService} | ||||
| import gitbucket.core.service.{AccountService, CommitsService, ProtectedBranchService} | ||||
| import gitbucket.core.util.Directory.getRepositoryDir | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.JGitUtil.CommitInfo | ||||
| import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommit} | ||||
| import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName} | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.revwalk.RevWalk | ||||
|  | ||||
| import scala.jdk.CollectionConverters._ | ||||
| import scala.util.Using | ||||
|  | ||||
| trait ApiRepositoryCommitControllerBase extends ControllerBase { | ||||
|   self: AccountService with CommitsService with ReferrerAuthenticator => | ||||
|   self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator => | ||||
|   /* | ||||
|    * i. List commits on a repository | ||||
|    * https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository | ||||
| @@ -22,7 +23,7 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase { | ||||
|     val owner = repository.owner | ||||
|     val name = repository.name | ||||
|     // TODO: The following parameters need to be implemented. [:path, :author, :since, :until] | ||||
|     val sha = if (request.body.nonEmpty) (parse(request.body) \ "sha").extract[String] else "refs/heads/master"; | ||||
|     val sha = params.getOrElse("sha", "refs/heads/master") | ||||
|     Using.resource(Git.open(getRepositoryDir(owner, name))) { | ||||
|       git => | ||||
|         val repo = git.getRepository | ||||
| @@ -110,4 +111,22 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase { | ||||
|    * v. Commit signature verification | ||||
|    * https://developer.github.com/v3/repos/commits/#commit-signature-verification | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * vi. List branches for HEAD commit | ||||
|    * https://docs.github.com/en/rest/reference/repos#list-branches-for-head-commit | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/commits/:sha/branches-where-head")(referrersOnly { repository => | ||||
|     val sha = params("sha") | ||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|       val apiBranchForCommits = for { | ||||
|         branch <- getBranchesOfCommit(git, sha) | ||||
|         br <- getBranches(git, branch, repository.repository.originUserName.isEmpty).find(_.name == branch) | ||||
|       } yield { | ||||
|         val protection = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||
|         ApiBranchForHeadCommit(branch, ApiBranchCommit(br.commitId), protection.enabled) | ||||
|       } | ||||
|       JsonFormat(apiBranchForCommits) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package gitbucket.core.controller.api | ||||
| import gitbucket.core.api.{ApiContents, ApiError, CreateAFile, JsonFormat} | ||||
| import gitbucket.core.controller.ControllerBase | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService} | ||||
| import gitbucket.core.util.Directory.getRepositoryDir | ||||
| import gitbucket.core.util.JGitUtil.{FileInfo, getContentFromId, getFileList} | ||||
| @@ -8,15 +9,30 @@ import gitbucket.core.util._ | ||||
| import gitbucket.core.view.helpers.{isRenderable, renderMarkup} | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import org.eclipse.jgit.api.Git | ||||
|  | ||||
| import scala.util.Using | ||||
|  | ||||
| trait ApiRepositoryContentsControllerBase extends ControllerBase { | ||||
|   self: ReferrerAuthenticator with WritableUsersAuthenticator with RepositoryCommitFileService => | ||||
|  | ||||
|   /* | ||||
|    * i. Get the README | ||||
|    * https://developer.github.com/v3/repos/contents/#get-the-readme | ||||
|   /** | ||||
|    * i. Get a repository README | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-a-repository-readme | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/readme")(referrersOnly { repository => | ||||
|     Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { | ||||
|       git => | ||||
|         val refStr = params.getOrElse("ref", repository.repository.defaultBranch) | ||||
|         val files = getFileList(git, refStr, ".", maxFiles = context.settings.repositoryViewer.maxFiles) | ||||
|         files // files should be sorted alphabetically. | ||||
|           .find { file => | ||||
|             !file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase) | ||||
|           } match { | ||||
|           case Some(x) => getContents(repository = repository, path = x.name, refStr = refStr, ignoreCase = true) | ||||
|           case _       => NotFound() | ||||
|         } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /** | ||||
|    * ii. Get contents | ||||
| @@ -34,21 +50,32 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase { | ||||
|     getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch)) | ||||
|   }) | ||||
|  | ||||
|   private def getContents(repository: RepositoryService.RepositoryInfo, path: String, refStr: String) = { | ||||
|     def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = { | ||||
|   private def getContents( | ||||
|     repository: RepositoryService.RepositoryInfo, | ||||
|     path: String, | ||||
|     refStr: String, | ||||
|     ignoreCase: Boolean = false | ||||
|   ) = { | ||||
|     def getFileInfo(git: Git, revision: String, pathStr: String, ignoreCase: Boolean): Option[FileInfo] = { | ||||
|       val (dirName, fileName) = pathStr.lastIndexOf('/') match { | ||||
|         case -1 => | ||||
|           (".", pathStr) | ||||
|         case n => | ||||
|           (pathStr.take(n), pathStr.drop(n + 1)) | ||||
|       } | ||||
|       getFileList(git, revision, dirName).find(f => f.name.equals(fileName)) | ||||
|       if (ignoreCase) { | ||||
|         getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles) | ||||
|           .find(_.name.toLowerCase.equals(fileName.toLowerCase)) | ||||
|       } else { | ||||
|         getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles) | ||||
|           .find(_.name.equals(fileName)) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git => | ||||
|       val fileList = getFileList(git, refStr, path) | ||||
|       val fileList = getFileList(git, refStr, path, maxFiles = context.settings.repositoryViewer.maxFiles) | ||||
|       if (fileList.isEmpty) { // file or NotFound | ||||
|         getFileInfo(git, refStr, path) | ||||
|         getFileInfo(git, refStr, path, ignoreCase) | ||||
|           .flatMap { f => | ||||
|             val largeFile = params.get("large_file").exists(s => s.equals("true")) | ||||
|             val content = getContentFromId(git, f.id, largeFile) | ||||
|   | ||||
| @@ -54,11 +54,15 @@ trait ApiRepositoryControllerBase extends ControllerBase { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|   /** | ||||
|    * iv. List all public repositories | ||||
|    * https://developer.github.com/v3/repos/#list-all-public-repositories | ||||
|    * Not implemented | ||||
|    * https://developer.github.com/v3/repos/#list-public-repositories | ||||
|    */ | ||||
|   get("/api/v3/repositories") { | ||||
|     JsonFormat(getPublicRepositories().map { r => | ||||
|       ApiRepository(r, getAccountByUserName(r.owner).get) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * v. Create | ||||
| @@ -174,9 +178,14 @@ trait ApiRepositoryControllerBase extends ControllerBase { | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|    * xiii. List tags | ||||
|    * https://developer.github.com/v3/repos/#list-tags | ||||
|    * xiii. List repository tags | ||||
|    * https://docs.github.com/en/rest/reference/repos#list-repository-tags | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository => | ||||
|     JsonFormat( | ||||
|       repository.tags.map(tagInfo => ApiTag(tagInfo.name, RepositoryName(repository), tagInfo.id)) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * xiv. Delete a repository | ||||
|   | ||||
| @@ -47,7 +47,7 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase { | ||||
|       ref <- params.get("ref") | ||||
|       sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) | ||||
|     } yield { | ||||
|       JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map { | ||||
|       JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map { | ||||
|         case (status, creator) => | ||||
|           ApiCommitStatus(status, ApiUser(creator)) | ||||
|       }) | ||||
| @@ -73,7 +73,7 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase { | ||||
|       owner <- getAccountByUserName(repository.owner) | ||||
|       sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) | ||||
|     } yield { | ||||
|       val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) | ||||
|       val statuses = getCommitStatusesWithCreator(repository.owner, repository.name, sha) | ||||
|       JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|   | ||||
| @@ -0,0 +1,120 @@ | ||||
| package gitbucket.core.controller.api | ||||
| import gitbucket.core.api._ | ||||
| import gitbucket.core.controller.ControllerBase | ||||
| import gitbucket.core.model.{WebHook, WebHookContentType} | ||||
| import gitbucket.core.service.{RepositoryService, WebHookService} | ||||
| import gitbucket.core.util._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import org.scalatra.NoContent | ||||
|  | ||||
| trait ApiRepositoryWebhookControllerBase extends ControllerBase { | ||||
|   self: RepositoryService with WebHookService with ReferrerAuthenticator with WritableUsersAuthenticator => | ||||
|  | ||||
|   /* | ||||
|    * i. List repository webhooks | ||||
|    * https://docs.github.com/en/rest/reference/repos#list-repository-webhooks | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/hooks")(referrersOnly { repository => | ||||
|     val apiWebhooks = for { | ||||
|       (hook, events) <- getWebHooks(repository.owner, repository.name) | ||||
|     } yield { | ||||
|       ApiWebhook("Repository", hook, events) | ||||
|     } | ||||
|     JsonFormat(apiWebhooks) | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * ii. Create a repository webhook | ||||
|    * https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook | ||||
|    */ | ||||
|   post("/api/v3/repos/:owner/:repository/hooks")(writableUsersOnly { repository => | ||||
|     (for { | ||||
|       data <- extractFromJsonBody[CreateARepositoryWebhook] if data.isValid | ||||
|       ctype = if (data.config.content_type == "form") WebHookContentType.FORM else WebHookContentType.JSON | ||||
|       events = data.events.map(p => WebHook.Event.valueOf(p)).toSet | ||||
|     } yield { | ||||
|       addWebHook( | ||||
|         repository.owner, | ||||
|         repository.name, | ||||
|         data.config.url, | ||||
|         events, | ||||
|         ctype, | ||||
|         data.config.secret | ||||
|       ) | ||||
|       getWebHook(repository.owner, repository.name, data.config.url) match { | ||||
|         case Some(createdHook) => JsonFormat(ApiWebhook("Repository", createdHook._1, createdHook._2)) | ||||
|         case _                 => | ||||
|       } | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iii. Get a repository webhook | ||||
|    * https://docs.github.com/en/rest/reference/repos#get-a-repository-webhook | ||||
|    */ | ||||
|   get("/api/v3/repos/:owner/:repository/hooks/:id")(referrersOnly { repository => | ||||
|     val hookId = params("id").toInt | ||||
|     getWebHookById(hookId) match { | ||||
|       case Some(hook) => JsonFormat(ApiWebhook("Repository", hook._1, hook._2)) | ||||
|       case _          => NotFound() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * iv. Update a repository webhook | ||||
|    * https://docs.github.com/en/rest/reference/repos#update-a-repository-webhook | ||||
|    */ | ||||
|   patch("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository => | ||||
|     val hookId = params("id").toInt | ||||
|     (for { | ||||
|       data <- extractFromJsonBody[UpdateARepositoryWebhook] if data.isValid | ||||
|       ctype = data.config.content_type match { | ||||
|         case "json" => WebHookContentType.JSON | ||||
|         case _      => WebHookContentType.FORM | ||||
|       } | ||||
|     } yield { | ||||
|       val events = (data.events ++ data.add_events) | ||||
|         .filterNot(p => data.remove_events.contains(p)) | ||||
|         .map(p => WebHook.Event.valueOf(p)) | ||||
|         .toSet | ||||
|       updateWebHookByApi( | ||||
|         hookId, | ||||
|         repository.owner, | ||||
|         repository.name, | ||||
|         data.config.url, | ||||
|         events, | ||||
|         ctype, | ||||
|         data.config.secret | ||||
|       ) | ||||
|       getWebHookById(hookId) match { | ||||
|         case Some(updatedHook) => JsonFormat(ApiWebhook("Repository", updatedHook._1, updatedHook._2)) | ||||
|         case _                 => | ||||
|       } | ||||
|     }) getOrElse NotFound() | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * v. Delete a repository webhook | ||||
|    * https://docs.github.com/en/rest/reference/repos#delete-a-repository-webhook | ||||
|    */ | ||||
|   delete("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository => | ||||
|     val hookId = params("id").toInt | ||||
|     getWebHookById(hookId) match { | ||||
|       case Some(_) => | ||||
|         deleteWebHookById(params("id").toInt) | ||||
|         NoContent() | ||||
|       case _ => NotFound() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * vi. Ping a repository webhook | ||||
|    * https://docs.github.com/en/rest/reference/repos#ping-a-repository-webhook | ||||
|    */ | ||||
|  | ||||
|   /* | ||||
|  * vi. Test the push repository webhook | ||||
|  * https://docs.github.com/en/rest/reference/repos#test-the-push-repository-webhook | ||||
|  */ | ||||
|  | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/main/scala/gitbucket/core/model/AccountPreference.scala
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/main/scala/gitbucket/core/model/AccountPreference.scala
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| package gitbucket.core.model | ||||
|  | ||||
| trait AccountPreferenceComponent { self: Profile => | ||||
|   import profile.api._ | ||||
|  | ||||
|   lazy val AccountPreferences = TableQuery[AccountPreferences] | ||||
|  | ||||
|   class AccountPreferences(tag: Tag) extends Table[AccountPreference](tag, "ACCOUNT_PREFERENCE") { | ||||
|     val userName = column[String]("USER_NAME", O PrimaryKey) | ||||
|     val highlighterTheme = column[String]("HIGHLIGHTER_THEME") | ||||
|     def * = | ||||
|       (userName, highlighterTheme) <> (AccountPreference.tupled, AccountPreference.unapply) | ||||
|  | ||||
|     def byPrimaryKey(userName: String): Rep[Boolean] = this.userName === userName.bind | ||||
|   } | ||||
| } | ||||
|  | ||||
| case class AccountPreference( | ||||
|   userName: String, | ||||
|   highlighterTheme: String = "prettify" | ||||
| ) | ||||
| @@ -1,5 +1,9 @@ | ||||
| package gitbucket.core.model | ||||
|  | ||||
| /** | ||||
|  * ActivityComponent has been deprecated, but keep it for binary compatibility. | ||||
|  */ | ||||
| @deprecated("ActivityComponent has been deprecated, but keep it for binary compatibility.", "4.34.0") | ||||
| trait ActivityComponent extends TemplateComponent { self: Profile => | ||||
|   import profile.api._ | ||||
|   import self._ | ||||
| @@ -7,14 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile => | ||||
|   lazy val Activities = TableQuery[Activities] | ||||
|  | ||||
|   class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { | ||||
|     val activityId = column[Int]("ACTIVITY_ID", O AutoInc) | ||||
|     val activityUserName = column[String]("ACTIVITY_USER_NAME") | ||||
|     val activityType = column[String]("ACTIVITY_TYPE") | ||||
|     val message = column[String]("MESSAGE") | ||||
|     val additionalInfo = column[String]("ADDITIONAL_INFO") | ||||
|     val activityDate = column[java.util.Date]("ACTIVITY_DATE") | ||||
|     def * = | ||||
|       (userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply) | ||||
|     def * = ??? | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -26,5 +23,5 @@ case class Activity( | ||||
|   message: String, | ||||
|   additionalInfo: Option[String], | ||||
|   activityDate: java.util.Date, | ||||
|   activityId: Int = 0 | ||||
|   activityId: String | ||||
| ) | ||||
|   | ||||
| @@ -45,7 +45,7 @@ trait CoreProfile | ||||
|     with Profile | ||||
|     with AccessTokenComponent | ||||
|     with AccountComponent | ||||
|     with ActivityComponent | ||||
|     with ActivityComponent // ActivityComponent has been deprecated, but keep it for binary compatibility | ||||
|     with CollaboratorComponent | ||||
|     with CommitCommentComponent | ||||
|     with CommitStatusComponent | ||||
| @@ -70,5 +70,6 @@ trait CoreProfile | ||||
|     with ReleaseTagComponent | ||||
|     with ReleaseAssetComponent | ||||
|     with AccountExtraMailAddressComponent | ||||
|     with AccountPreferenceComponent | ||||
|  | ||||
| object Profile extends CoreProfile | ||||
|   | ||||
| @@ -9,20 +9,25 @@ trait RepositoryWebHookComponent extends TemplateComponent { self: Profile => | ||||
|   lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks] | ||||
|  | ||||
|   class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate { | ||||
|     val hookId = column[Int]("HOOK_ID", O AutoInc) | ||||
|     val url = column[String]("URL") | ||||
|     val token = column[Option[String]]("TOKEN") | ||||
|     val ctype = column[WebHookContentType]("CTYPE") | ||||
|     def * = | ||||
|       (userName, repositoryName, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply) | ||||
|       (userName, repositoryName, hookId, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String, url: String) = | ||||
|     def byRepositoryUrl(owner: String, repository: String, url: String) = | ||||
|       byRepository(owner, repository) && (this.url === url.bind) | ||||
|  | ||||
|     def byId(id: Int) = | ||||
|       (this.hookId === id.bind) | ||||
|   } | ||||
| } | ||||
|  | ||||
| case class RepositoryWebHook( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   hookId: Int = 0, | ||||
|   url: String, | ||||
|   ctype: WebHookContentType, | ||||
|   token: Option[String] | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
|  | ||||
| trait BaseActivityInfo { | ||||
|  | ||||
|   def toActivity: Activity | ||||
|  | ||||
|   protected def trimInfoString(str: String, maxLen: Int): String = | ||||
|     if (str.length > maxLen) s"${str.substring(0, maxLen)}..." | ||||
|     else str | ||||
| } | ||||
| @@ -0,0 +1,74 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
| import gitbucket.core.util.JGitUtil.CommitInfo | ||||
|  | ||||
| final case class PushInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   branchName: String, | ||||
|   commits: List[CommitInfo] | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "push", | ||||
|       s"[user:$activityUserName] pushed to [branch:$userName/$repositoryName#$branchName] at [repo:$userName/$repositoryName]", | ||||
|       Some(buildCommitSummary(commits)), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
|  | ||||
|   private[this] def buildCommitSummary(commits: List[CommitInfo]): String = | ||||
|     commits | ||||
|       .take(5) | ||||
|       .map(commit => s"${commit.id}:${commit.shortMessage}") | ||||
|       .mkString("\n") | ||||
| } | ||||
|  | ||||
| final case class CreateBranchInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   branchName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_branch", | ||||
|       s"[user:$activityUserName] created branch [branch:$userName/$repositoryName#$branchName] at [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class DeleteBranchInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   branchName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "delete_branch", | ||||
|       s"[user:$activityUserName] deleted branch $branchName at [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,69 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class IssueCommentInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   comment: String, | ||||
|   issueId: Int | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "comment_issue", | ||||
|       s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(trimInfoString(comment, 200)), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class PullRequestCommentInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   comment: String, | ||||
|   issueId: Int | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "comment_issue", | ||||
|       s"[user:$activityUserName] commented on pull request [pullreq:$userName/$repositoryName#$issueId]", | ||||
|       Some(trimInfoString(comment, 200)), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class CommitCommentInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   comment: String, | ||||
|   commitId: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "comment_commit", | ||||
|       s"[user:$activityUserName] commented on commit [commit:$userName/$repositoryName@$commitId]", | ||||
|       Some(trimInfoString(comment, 200)), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class ForkInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   forkedUserName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "fork", | ||||
|       s"[user:$activityUserName] forked [repo:$userName/$repositoryName] to [repo:$forkedUserName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,132 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class CreateIssueInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   title: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "open_issue", | ||||
|       s"[user:$activityUserName] opened issue [issue:$userName/$repositoryName#$issueId]", | ||||
|       Some(title), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class CloseIssueInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   title: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "close_issue", | ||||
|       s"[user:$activityUserName] closed issue [issue:$userName/$repositoryName#$issueId]", | ||||
|       Some(title), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class ReopenIssueInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   title: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "reopen_issue", | ||||
|       s"[user:$activityUserName] reopened issue [issue:$userName/$repositoryName#$issueId]", | ||||
|       Some(title), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class OpenPullRequestInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   title: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "open_pullreq", | ||||
|       s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class ClosePullRequestInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   title: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "close_issue", | ||||
|       s"[user:$activityUserName] closed pull request [pullreq:$userName/$repositoryName#$issueId]", | ||||
|       Some(title), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class ReopenPullRequestInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   title: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "reopen_issue", | ||||
|       s"[user:$activityUserName] reopened pull request [issue:$userName/$repositoryName#$issueId]", | ||||
|       Some(title), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class MergeInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   issueId: Int, | ||||
|   message: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "merge_pullreq", | ||||
|       s"[user:$activityUserName] merged pull request [pullreq:$userName/$repositoryName#$issueId]", | ||||
|       Some(message), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class ReleaseInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   releaseName: String, | ||||
|   tagName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "release", | ||||
|       s"[user:$activityUserName] released [release:$userName/$repositoryName/$tagName:$releaseName] at [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,84 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class CreateRepositoryInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_repository", | ||||
|       s"[user:$activityUserName] created [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class DeleteRepositoryInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "delete_repository", | ||||
|       s"[user:$activityUserName] deleted [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class TransferRepositoryInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   oldUserName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "transfer_repository", | ||||
|       s"[user:$activityUserName] transferred [repo:$oldUserName/$repositoryName] to [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class RenameRepositoryInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   oldRepositoryName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "rename_repository", | ||||
|       s"[user:$activityUserName] renamed [repo:$userName/$oldRepositoryName] at [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,46 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class CreateTagInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   tagName: String, | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_tag", | ||||
|       s"[user:$activityUserName] created tag [tag:$userName/$repositoryName#$tagName] at [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class DeleteTagInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   tagName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "delete_tag", | ||||
|       s"[user:$activityUserName] deleted tag $tagName at [repo:$userName/$repositoryName]", | ||||
|       None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,67 @@ | ||||
| package gitbucket.core.model.activity | ||||
|  | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.model.Profile.currentDate | ||||
|  | ||||
| final case class CreateWikiPageInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   pageName: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_wiki", | ||||
|       s"[user:$activityUserName] created the [repo:$userName/$repositoryName] wiki", | ||||
|       Some(pageName), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class EditWikiPageInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   pageName: String, | ||||
|   commitId: String | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "edit_wiki", | ||||
|       s"[user:$activityUserName] edited the [repo:$userName/$repositoryName] wiki", | ||||
|       Some(s"$pageName:$commitId"), | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
|  | ||||
| final case class DeleteWikiInfo( | ||||
|   userName: String, | ||||
|   repositoryName: String, | ||||
|   activityUserName: String, | ||||
|   pageName: String, | ||||
| ) extends BaseActivityInfo { | ||||
|  | ||||
|   override def toActivity: Activity = | ||||
|     Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "delete_wiki", | ||||
|       s"[user:$activityUserName] deleted the page [$pageName] in the [repo:$userName/$repositoryName] wiki", | ||||
|       additionalInfo = None, | ||||
|       currentDate, | ||||
|       UUID.randomUUID().toString | ||||
|     ) | ||||
| } | ||||
| @@ -13,6 +13,14 @@ trait IssueHook { | ||||
|     implicit session: Session, | ||||
|     context: Context | ||||
|   ): Unit = () | ||||
|   def deletedComment(commentId: Int, issue: Issue, repository: RepositoryInfo)( | ||||
|     implicit session: Session, | ||||
|     context: Context | ||||
|   ): Unit = () | ||||
|   def updatedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)( | ||||
|     implicit session: Session, | ||||
|     context: Context | ||||
|   ): Unit = () | ||||
|   def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () | ||||
|   def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () | ||||
|   def assigned( | ||||
|   | ||||
| @@ -57,6 +57,7 @@ class PluginRegistry { | ||||
|   private val textDecorators = new ConcurrentLinkedQueue[TextDecorator] | ||||
|   private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider] | ||||
|   suggestionProviders.add(new UserNameSuggestionProvider()) | ||||
|   suggestionProviders.add(new IssueSuggestionProvider()) | ||||
|   private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]() | ||||
|  | ||||
|   def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo) | ||||
|   | ||||
| @@ -6,11 +6,25 @@ import profile.api._ | ||||
|  | ||||
| trait ReceiveHook { | ||||
|  | ||||
|   def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)( | ||||
|   def preReceive( | ||||
|     owner: String, | ||||
|     repository: String, | ||||
|     receivePack: ReceivePack, | ||||
|     command: ReceiveCommand, | ||||
|     pusher: String, | ||||
|     mergePullRequest: Boolean | ||||
|   )( | ||||
|     implicit session: Session | ||||
|   ): Option[String] = None | ||||
|  | ||||
|   def postReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)( | ||||
|   def postReceive( | ||||
|     owner: String, | ||||
|     repository: String, | ||||
|     receivePack: ReceivePack, | ||||
|     command: ReceiveCommand, | ||||
|     pusher: String, | ||||
|     mergePullRequest: Boolean | ||||
|   )( | ||||
|     implicit session: Session | ||||
|   ): Unit = () | ||||
|  | ||||
|   | ||||
| @@ -91,7 +91,7 @@ trait SuggestionProvider { | ||||
|    * If this suggestion provider needs some additional process to assemble the proposal list (e.g. It need to use Ajax | ||||
|    * to get a proposal list from the server), then override this method and return any JavaScript code. | ||||
|    */ | ||||
|   def additionalScript(implicit context: Context): String = "" | ||||
|   def additionalScript(repository: RepositoryInfo)(implicit context: Context): String = "" | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -99,6 +99,14 @@ class UserNameSuggestionProvider extends SuggestionProvider { | ||||
|   override val id: String = "user" | ||||
|   override val prefix: String = "@" | ||||
|   override val context: Seq[String] = Seq("issues") | ||||
|   override def additionalScript(implicit context: Context): String = | ||||
|   override def additionalScript(repository: RepositoryInfo)(implicit context: Context): String = | ||||
|     s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });""" | ||||
| } | ||||
|  | ||||
| class IssueSuggestionProvider extends SuggestionProvider { | ||||
|   override val id: String = "issue" | ||||
|   override val prefix: String = "#" | ||||
|   override val context: Seq[String] = Seq("issues") | ||||
|   override def additionalScript(repository: RepositoryInfo)(implicit context: Context): String = | ||||
|     s"""$$.get('${context.path}/${repository.owner}/${repository.name}/_issue/proposals', function (data) { issue = data.options; });""" | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package gitbucket.core.service | ||||
|  | ||||
| import org.slf4j.LoggerFactory | ||||
| import gitbucket.core.model.{Account, AccountExtraMailAddress, GroupMember} | ||||
| import gitbucket.core.model.{Account, AccountExtraMailAddress, AccountPreference, GroupMember} | ||||
| import gitbucket.core.model.Profile._ | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.Profile.dateColumnType | ||||
| @@ -17,7 +17,9 @@ trait AccountService { | ||||
|   def authenticate(settings: SystemSettings, userName: String, password: String)( | ||||
|     implicit s: Session | ||||
|   ): Option[Account] = { | ||||
|     val account = if (settings.ldapAuthentication) { | ||||
|     val account = if (password.isEmpty) { | ||||
|       None | ||||
|     } else if (settings.ldapAuthentication) { | ||||
|       ldapAuthentication(settings, userName, password) | ||||
|     } else { | ||||
|       defaultAuthentication(userName, password) | ||||
| @@ -308,6 +310,33 @@ trait AccountService { | ||||
|       Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list.distinct | ||||
|   } | ||||
|  | ||||
|   /* | ||||
|    * For account preference | ||||
|    */ | ||||
|   def getAccountPreference(userName: String)( | ||||
|     implicit s: Session | ||||
|   ): Option[AccountPreference] = { | ||||
|     AccountPreferences filter (_.byPrimaryKey(userName)) firstOption | ||||
|   } | ||||
|  | ||||
|   def addAccountPreference(userName: String, highlighterTheme: String)(implicit s: Session): Unit = { | ||||
|     AccountPreferences insert AccountPreference(userName = userName, highlighterTheme = highlighterTheme) | ||||
|   } | ||||
|  | ||||
|   def updateAccountPreference(userName: String, highlighterTheme: String)(implicit s: Session): Unit = { | ||||
|     AccountPreferences | ||||
|       .filter(_.byPrimaryKey(userName)) | ||||
|       .map(t => t.highlighterTheme) | ||||
|       .update(highlighterTheme) | ||||
|   } | ||||
|  | ||||
|   def addOrUpdateAccountPreference(userName: String, highlighterTheme: String)(implicit s: Session): Unit = { | ||||
|     getAccountPreference(userName) match { | ||||
|       case Some(_) => updateAccountPreference(userName, highlighterTheme) | ||||
|       case _       => addAccountPreference(userName, highlighterTheme) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| object AccountService extends AccountService | ||||
|   | ||||
| @@ -3,389 +3,101 @@ package gitbucket.core.service | ||||
| import gitbucket.core.model.Activity | ||||
| import gitbucket.core.util.JGitUtil | ||||
| import gitbucket.core.model.Profile._ | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.util.Directory._ | ||||
| import org.json4s._ | ||||
| import org.json4s.jackson.Serialization | ||||
| import org.json4s.jackson.Serialization.{read, write} | ||||
|  | ||||
| import scala.util.Using | ||||
| import java.io.FileOutputStream | ||||
| import java.nio.charset.StandardCharsets | ||||
| import java.util.UUID | ||||
|  | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.model.activity.BaseActivityInfo | ||||
| import org.apache.commons.io.input.ReversedLinesFileReader | ||||
|  | ||||
| import scala.collection.mutable.ListBuffer | ||||
|  | ||||
| trait ActivityService { | ||||
|   self: RequestCache => | ||||
|  | ||||
|   def deleteOldActivities(limit: Int)(implicit s: Session): Int = { | ||||
|     Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id => | ||||
|       Activities.filter(_.activityId <= id.bind).delete | ||||
|     } getOrElse 0 | ||||
|   private implicit val formats = Serialization.formats(NoTypeHints) | ||||
|  | ||||
|   private def writeLog(activity: Activity): Unit = { | ||||
|     Using.resource(new FileOutputStream(ActivityLog, true)) { out => | ||||
|       out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] = | ||||
|     Activities | ||||
|       .join(Repositories) | ||||
|       .on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) | ||||
|       .filter { | ||||
|         case (t1, t2) => | ||||
|           if (isPublic) { | ||||
|             (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) | ||||
|   def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = { | ||||
|     if (!ActivityLog.exists()) { | ||||
|       List.empty | ||||
|     } else { | ||||
|             (t1.activityUserName === activityUserName.bind) | ||||
|       val list = new ListBuffer[Activity] | ||||
|       Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader => | ||||
|         var json: String = null | ||||
|         while (list.length < 50 && { json = reader.readLine(); json } != null) { | ||||
|           val activity = read[Activity](json) | ||||
|           if (activity.activityUserName == activityUserName) { | ||||
|             if (isPublic == false) { | ||||
|               list += activity | ||||
|             } else { | ||||
|               if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName) | ||||
|                     .map(_.isPrivate) | ||||
|                     .getOrElse(true)) { | ||||
|                 list += activity | ||||
|               } | ||||
|             } | ||||
|       .sortBy { case (t1, t2) => t1.activityId desc } | ||||
|       .map { case (t1, t2) => t1 } | ||||
|       .take(30) | ||||
|       .list | ||||
|  | ||||
|   def getRecentActivities()(implicit s: Session): List[Activity] = | ||||
|     Activities | ||||
|       .join(Repositories) | ||||
|       .on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) | ||||
|       .filter { case (t1, t2) => t2.isPrivate === false.bind } | ||||
|       .sortBy { case (t1, t2) => t1.activityId desc } | ||||
|       .map { case (t1, t2) => t1 } | ||||
|       .take(30) | ||||
|       .list | ||||
|  | ||||
|   def getRecentActivitiesByOwners(owners: Set[String])(implicit s: Session): List[Activity] = | ||||
|     Activities | ||||
|       .join(Repositories) | ||||
|       .on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) | ||||
|       .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) | ||||
|       .list | ||||
|  | ||||
|   def recordCreateRepositoryActivity(userName: String, repositoryName: String, activityUserName: String)( | ||||
|     implicit s: Session | ||||
|   ): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_repository", | ||||
|       s"[user:${activityUserName}] created [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordCreateIssueActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     title: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "open_issue", | ||||
|       s"[user:${activityUserName}] opened issue [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordCloseIssueActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     title: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "close_issue", | ||||
|       s"[user:${activityUserName}] closed issue [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordClosePullRequestActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     title: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "close_issue", | ||||
|       s"[user:${activityUserName}] closed pull request [pullreq:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordReopenIssueActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     title: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "reopen_issue", | ||||
|       s"[user:${activityUserName}] reopened issue [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordReopenPullRequestActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     title: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "reopen_issue", | ||||
|       s"[user:${activityUserName}] reopened pull request [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordCommentIssueActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     comment: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "comment_issue", | ||||
|       s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(cut(comment, 200)), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordCommentPullRequestActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     comment: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "comment_issue", | ||||
|       s"[user:${activityUserName}] commented on pull request [pullreq:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(cut(comment, 200)), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordCommentCommitActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     commitId: String, | ||||
|     comment: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "comment_commit", | ||||
|       s"[user:${activityUserName}] commented on commit [commit:${userName}/${repositoryName}@${commitId}]", | ||||
|       Some(cut(comment, 200)), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordCreateWikiPageActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     pageName: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_wiki", | ||||
|       s"[user:${activityUserName}] created the [repo:${userName}/${repositoryName}] wiki", | ||||
|       Some(pageName), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordEditWikiPageActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     pageName: String, | ||||
|     commitId: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "edit_wiki", | ||||
|       s"[user:${activityUserName}] edited the [repo:${userName}/${repositoryName}] wiki", | ||||
|       Some(pageName + ":" + commitId), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordPushActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     branchName: String, | ||||
|     commits: List[JGitUtil.CommitInfo] | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "push", | ||||
|       s"[user:${activityUserName}] pushed to [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", | ||||
|       Some( | ||||
|         commits | ||||
|           .take(5) | ||||
|           .map { commit => | ||||
|             commit.id + ":" + commit.shortMessage | ||||
|           } | ||||
|           .mkString("\n") | ||||
|       ), | ||||
|       currentDate | ||||
|     ) | ||||
|         } | ||||
|       } | ||||
|       list.toList | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def recordCreateTagActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     tagName: String, | ||||
|     commits: List[JGitUtil.CommitInfo] | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_tag", | ||||
|       s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|   def getRecentPublicActivities()(implicit context: Context): List[Activity] = { | ||||
|     if (!ActivityLog.exists()) { | ||||
|       List.empty | ||||
|     } else { | ||||
|       val list = new ListBuffer[Activity] | ||||
|       Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader => | ||||
|         var json: String = null | ||||
|         while (list.length < 50 && { json = reader.readLine(); json } != null) { | ||||
|           val activity = read[Activity](json) | ||||
|           if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName) | ||||
|                 .map(_.isPrivate) | ||||
|                 .getOrElse(true)) { | ||||
|             list += activity | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       list.toList | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def recordDeleteTagActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     tagName: String, | ||||
|     commits: List[JGitUtil.CommitInfo] | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "delete_tag", | ||||
|       s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|   def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = { | ||||
|     if (!ActivityLog.exists()) { | ||||
|       List.empty | ||||
|     } else { | ||||
|       val list = new ListBuffer[Activity] | ||||
|       Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader => | ||||
|         var json: String = null | ||||
|         while (list.length < 50 && { json = reader.readLine(); json } != null) { | ||||
|           val activity = read[Activity](json) | ||||
|           if (owners.contains(activity.userName)) { | ||||
|             list += activity | ||||
|           } else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName) | ||||
|                        .map(_.isPrivate) | ||||
|                        .getOrElse(true)) { | ||||
|             list += activity | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       list.toList | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def recordCreateBranchActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     branchName: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "create_branch", | ||||
|       s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordDeleteBranchActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     branchName: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "delete_branch", | ||||
|       s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)( | ||||
|     implicit s: Session | ||||
|   ): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "fork", | ||||
|       s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordPullRequestActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     title: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "open_pullreq", | ||||
|       s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(title), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordMergeActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     issueId: Int, | ||||
|     message: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "merge_pullreq", | ||||
|       s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]", | ||||
|       Some(message), | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   def recordReleaseActivity( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     activityUserName: String, | ||||
|     releaseName: String, | ||||
|     tagName: String | ||||
|   )(implicit s: Session): Unit = | ||||
|     Activities insert Activity( | ||||
|       userName, | ||||
|       repositoryName, | ||||
|       activityUserName, | ||||
|       "release", | ||||
|       s"[user:${activityUserName}] released [release:${userName}/${repositoryName}/${tagName}:${releaseName}] at [repo:${userName}/${repositoryName}]", | ||||
|       None, | ||||
|       currentDate | ||||
|     ) | ||||
|  | ||||
|   private def cut(value: String, length: Int): String = | ||||
|     if (value.length > length) value.substring(0, length) + "..." else value | ||||
|   def recordActivity[T <: { def toActivity: Activity }](info: T): Unit = | ||||
|     writeLog(info.toActivity) | ||||
| } | ||||
|   | ||||
| @@ -47,6 +47,18 @@ trait CommitStatusService { | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|   def getCommitStatusWithSummary(userName: String, repositoryName: String, sha: String)( | ||||
|     implicit s: Session | ||||
|   ): Option[(CommitState, List[CommitStatus])] = { | ||||
|     val statuses = getCommitStatuses(userName, repositoryName, sha) | ||||
|     if (statuses.isEmpty) { | ||||
|       None | ||||
|     } else { | ||||
|       val summary = CommitState.combine(statuses.groupBy(_.state).keySet) | ||||
|       Some((summary, statuses)) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session): Option[CommitStatus] = | ||||
|     CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption | ||||
|  | ||||
| @@ -55,10 +67,12 @@ trait CommitStatusService { | ||||
|   ): Option[CommitStatus] = | ||||
|     CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind).firstOption | ||||
|  | ||||
|   def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session): List[CommitStatus] = | ||||
|     byCommitStatues(userName, repositoryName, sha).list | ||||
|   def getCommitStatuses(userName: String, repositoryName: String, sha: String)( | ||||
|     implicit s: Session | ||||
|   ): List[CommitStatus] = | ||||
|     byCommitStatus(userName, repositoryName, sha).list | ||||
|  | ||||
|   def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)( | ||||
|   def getRecentStatusContexts(userName: String, repositoryName: String, time: java.util.Date)( | ||||
|     implicit s: Session | ||||
|   ): List[String] = | ||||
|     CommitStatuses | ||||
| @@ -68,15 +82,15 @@ trait CommitStatusService { | ||||
|       .map(_._1) | ||||
|       .list | ||||
|  | ||||
|   def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)( | ||||
|   def getCommitStatusesWithCreator(userName: String, repositoryName: String, sha: String)( | ||||
|     implicit s: Session | ||||
|   ): List[(CommitStatus, Account)] = | ||||
|     byCommitStatues(userName, repositoryName, sha) | ||||
|     byCommitStatus(userName, repositoryName, sha) | ||||
|       .join(Accounts) | ||||
|       .filter { case (t, a) => t.creator === a.userName } | ||||
|       .list | ||||
|  | ||||
|   protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) = | ||||
|   protected def byCommitStatus(userName: String, repositoryName: String, sha: String)(implicit s: Session) = | ||||
|     CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha)).sortBy(_.updatedDate desc) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import gitbucket.core.model.{Account, CommitComment} | ||||
| import gitbucket.core.model.Profile._ | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.Profile.dateColumnType | ||||
| import gitbucket.core.model.activity.{CommitCommentInfo, PullRequestCommentInfo} | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.util.Directory._ | ||||
| @@ -80,13 +81,9 @@ trait CommitsService { | ||||
|       case Some(issueId) => | ||||
|         getPullRequest(repository.owner, repository.name, issueId).foreach { | ||||
|           case (issue, pullRequest) => | ||||
|             recordCommentPullRequestActivity( | ||||
|               repository.owner, | ||||
|               repository.name, | ||||
|               loginAccount.userName, | ||||
|               issueId, | ||||
|               content | ||||
|             ) | ||||
|             val pullRequestCommentInfo = | ||||
|               PullRequestCommentInfo(repository.owner, repository.name, loginAccount.userName, content, issueId) | ||||
|             recordActivity(pullRequestCommentInfo) | ||||
|             PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, content, issue, repository)) | ||||
|             callPullRequestReviewCommentWebHook( | ||||
|               "create", | ||||
| @@ -99,13 +96,9 @@ trait CommitsService { | ||||
|             ) | ||||
|         } | ||||
|       case None => | ||||
|         recordCommentCommitActivity( | ||||
|           repository.owner, | ||||
|           repository.name, | ||||
|           loginAccount.userName, | ||||
|           commitId, | ||||
|           content | ||||
|         ) | ||||
|         val commitCommentInfo = | ||||
|           CommitCommentInfo(repository.owner, repository.name, loginAccount.userName, content, commitId) | ||||
|         recordActivity(commitCommentInfo) | ||||
|     } | ||||
|  | ||||
|     commentId | ||||
|   | ||||
| @@ -1,9 +1,18 @@ | ||||
| package gitbucket.core.service | ||||
|  | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.model.Issue | ||||
| import gitbucket.core.model.{Issue, IssueComment} | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.model.activity.{ | ||||
|   CloseIssueInfo, | ||||
|   ClosePullRequestInfo, | ||||
|   IssueCommentInfo, | ||||
|   PullRequestCommentInfo, | ||||
|   ReopenIssueInfo, | ||||
|   ReopenPullRequestInfo | ||||
| } | ||||
| import gitbucket.core.plugin.{IssueHook, PluginRegistry} | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.util.SyntaxSugars._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
|  | ||||
| @@ -29,25 +38,31 @@ trait HandleCommentService { | ||||
|         case (owner, name) => | ||||
|           val userName = loginAccount.userName | ||||
|  | ||||
|           val (action, actionActivity) = actionOpt | ||||
|             .collect { | ||||
|               case "close" if (!issue.closed) => | ||||
|                 true -> | ||||
|                   (Some("close") -> Some( | ||||
|                     if (issue.isPullRequest) recordClosePullRequestActivity _ | ||||
|                     else recordCloseIssueActivity _ | ||||
|                   )) | ||||
|               case "reopen" if (issue.closed) => | ||||
|                 false -> | ||||
|                   (Some("reopen") -> Some( | ||||
|                     if (issue.isPullRequest) recordReopenPullRequestActivity _ | ||||
|                     else recordReopenIssueActivity _ | ||||
|                   )) | ||||
|           actionOpt.collect { | ||||
|             case "close" if !issue.closed => | ||||
|               updateClosed(owner, name, issue.issueId, true) | ||||
|             case "reopen" if issue.closed => | ||||
|               updateClosed(owner, name, issue.issueId, false) | ||||
|           } | ||||
|             .map { | ||||
|               case (closed, t) => | ||||
|                 updateClosed(owner, name, issue.issueId, closed) | ||||
|                 t | ||||
|  | ||||
|           val (action, _) = actionOpt | ||||
|             .collect { | ||||
|               case "close" if !issue.closed => | ||||
|                 val info = if (issue.isPullRequest) { | ||||
|                   ClosePullRequestInfo(owner, name, userName, issue.issueId, issue.title) | ||||
|                 } else { | ||||
|                   CloseIssueInfo(owner, name, userName, issue.issueId, issue.title) | ||||
|                 } | ||||
|                 recordActivity(info) | ||||
|                 Some("close") -> info | ||||
|               case "reopen" if issue.closed => | ||||
|                 val info = if (issue.isPullRequest) { | ||||
|                   ReopenPullRequestInfo(owner, name, userName, issue.issueId, issue.title) | ||||
|                 } else { | ||||
|                   ReopenIssueInfo(owner, name, userName, issue.issueId, issue.title) | ||||
|                 } | ||||
|                 recordActivity(info) | ||||
|                 Some("reopen") -> info | ||||
|             } | ||||
|             .getOrElse(None -> None) | ||||
|  | ||||
| @@ -68,8 +83,12 @@ trait HandleCommentService { | ||||
|               ) | ||||
|  | ||||
|               // record comment activity | ||||
|               if (issue.isPullRequest) recordCommentPullRequestActivity(owner, name, userName, issue.issueId, content) | ||||
|               else recordCommentIssueActivity(owner, name, userName, issue.issueId, content) | ||||
|               val commentInfo = if (issue.isPullRequest) { | ||||
|                 PullRequestCommentInfo(owner, name, userName, content, issue.issueId) | ||||
|               } else { | ||||
|                 IssueCommentInfo(owner, name, userName, content, issue.issueId) | ||||
|               } | ||||
|               recordActivity(commentInfo) | ||||
|  | ||||
|               // extract references and create refer comment | ||||
|               createReferComment(owner, name, issue, content, loginAccount) | ||||
| @@ -77,10 +96,6 @@ trait HandleCommentService { | ||||
|               id | ||||
|           } | ||||
|  | ||||
|           actionActivity.foreach { f => | ||||
|             f(owner, name, userName, issue.issueId, issue.title) | ||||
|           } | ||||
|  | ||||
|           // call web hooks | ||||
|           action match { | ||||
|             case None => | ||||
| @@ -121,4 +136,59 @@ trait HandleCommentService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def deleteCommentByApi(repoInfo: RepositoryInfo, comment: IssueComment, issue: Issue)( | ||||
|     implicit context: Context, | ||||
|     s: Session | ||||
|   ): Option[IssueComment] = context.loginAccount.flatMap { _ => | ||||
|     comment.action match { | ||||
|       case "comment" => | ||||
|         val deleteResult = deleteComment(repoInfo.owner, repoInfo.name, comment.issueId, comment.commentId) | ||||
|         val registry = PluginRegistry() | ||||
|         val hooks: Seq[IssueHook] = if (issue.isPullRequest) registry.getPullRequestHooks else registry.getIssueHooks | ||||
|         hooks.foreach(_.deletedComment(comment.commentId, issue, repoInfo)) | ||||
|         deleteResult match { | ||||
|           case n if n > 0 => Some(comment) | ||||
|           case _          => None | ||||
|         } | ||||
|       case _ => None | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def updateCommentByApi( | ||||
|     repository: RepositoryService.RepositoryInfo, | ||||
|     issue: Issue, | ||||
|     commentId: String, | ||||
|     content: Option[String] | ||||
|   )(implicit context: Context, s: Session): Option[(Issue, Int)] = { | ||||
|     context.loginAccount.flatMap { loginAccount => | ||||
|       defining(repository.owner, repository.name) { | ||||
|         case (owner, name) => | ||||
|           val userName = loginAccount.userName | ||||
|           content match { | ||||
|             case Some(content) => | ||||
|               // Update comment | ||||
|               val _commentId = Some(updateComment(issue.issueId, commentId.toInt, content)) | ||||
|               // Record comment activity | ||||
|               val commentInfo = if (issue.isPullRequest) { | ||||
|                 PullRequestCommentInfo(owner, name, userName, content, issue.issueId) | ||||
|               } else { | ||||
|                 IssueCommentInfo(owner, name, userName, content, issue.issueId) | ||||
|               } | ||||
|               recordActivity(commentInfo) | ||||
|               // extract references and create refer comment | ||||
|               createReferComment(owner, name, issue, content, loginAccount) | ||||
|               // call web hooks | ||||
|               commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount, context.settings)) | ||||
|               // call hooks | ||||
|               if (issue.isPullRequest) | ||||
|                 PluginRegistry().getPullRequestHooks | ||||
|                   .foreach(_.updatedComment(commentId.toInt, content, issue, repository)) | ||||
|               else | ||||
|                 PluginRegistry().getIssueHooks.foreach(_.updatedComment(commentId.toInt, content, issue, repository)) | ||||
|               _commentId.map(issue -> _) | ||||
|             case _ => None | ||||
|           } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package gitbucket.core.service | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.model.{Account, Issue} | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.activity.CreateIssueInfo | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.util.Implicits._ | ||||
| @@ -51,7 +52,8 @@ trait IssueCreationService { | ||||
|     } | ||||
|  | ||||
|     // record activity | ||||
|     recordCreateIssueActivity(owner, name, userName, issueId, title) | ||||
|     val createIssueInfo = CreateIssueInfo(owner, name, userName, issueId, title) | ||||
|     recordActivity(createIssueInfo) | ||||
|  | ||||
|     // extract references and create refer comment | ||||
|     createReferComment(owner, name, issue, title + " " + body.getOrElse(""), loginAccount) | ||||
| @@ -72,6 +74,13 @@ trait IssueCreationService { | ||||
|     hasDeveloperRole(repository.owner, repository.name, context.loginAccount) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Tests whether an logged-in user can manage issues comment. | ||||
|    */ | ||||
|   protected def isIssueCommentManageable(repository: RepositoryInfo)(implicit context: Context, s: Session): Boolean = { | ||||
|     hasOwnerRole(repository.owner, repository.name, context.loginAccount) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Tests whether an logged-in user can post issues. | ||||
|    */ | ||||
|   | ||||
| @@ -31,6 +31,9 @@ trait IssuesService { | ||||
|       Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption | ||||
|     else None | ||||
|  | ||||
|   def getOpenIssues(owner: String, repository: String)(implicit s: Session): List[Issue] = | ||||
|     Issues filter (_.byRepository(owner, repository)) filterNot (_.closed) sortBy (_.issueId desc) list | ||||
|  | ||||
|   def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = | ||||
|     IssueComments filter (_.byIssue(owner, repository, issueId)) sortBy (_.commentId asc) list | ||||
|  | ||||
| @@ -68,6 +71,20 @@ trait IssuesService { | ||||
|     else None | ||||
|   } | ||||
|  | ||||
|   def getCommentForApi(owner: String, repository: String, commentId: Int)( | ||||
|     implicit s: Session | ||||
|   ): Option[(IssueComment, Account, Issue)] = | ||||
|     IssueComments | ||||
|       .filter(_.byRepository(owner, repository)) | ||||
|       .filter(_.commentId === commentId) | ||||
|       .filter(_.action inSetBind Set("comment", "close_comment", "reopen_comment")) | ||||
|       .join(Accounts) | ||||
|       .on { case t1 ~ t2 => t1.commentedUserName === t2.userName } | ||||
|       .join(Issues) | ||||
|       .on { case t1 ~ t2 ~ t3 => t3.byIssue(t1.userName, t1.repositoryName, t1.issueId) } | ||||
|       .map { case t1 ~ t2 ~ t3 => (t1, t2, t3) } | ||||
|       .firstOption | ||||
|  | ||||
|   def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session): List[Label] = { | ||||
|     IssueLabels | ||||
|       .join(Labels) | ||||
| @@ -90,14 +107,14 @@ trait IssuesService { | ||||
|    * Returns the count of the search result against  issues. | ||||
|    * | ||||
|    * @param condition the search condition | ||||
|    * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. | ||||
|    * @param searchOption 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 | ||||
|    */ | ||||
|   def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, repos: (String, String)*)( | ||||
|   def countIssue(condition: IssueSearchCondition, searchOption: IssueSearchOption, repos: (String, String)*)( | ||||
|     implicit s: Session | ||||
|   ): Int = { | ||||
|     Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first | ||||
|     Query(searchIssueQuery(repos, condition, searchOption).length).first | ||||
|   } | ||||
|  | ||||
|   /** | ||||
| @@ -115,7 +132,7 @@ trait IssuesService { | ||||
|     filterUser: Map[String, String] | ||||
|   )(implicit s: Session): Map[String, Int] = { | ||||
|  | ||||
|     searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false) | ||||
|     searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues) | ||||
|       .join(IssueLabels) | ||||
|       .on { | ||||
|         case t1 ~ t2 => | ||||
| @@ -153,7 +170,7 @@ trait IssuesService { | ||||
|     filterUser: Map[String, String] | ||||
|   )(implicit s: Session): Map[String, Int] = { | ||||
|  | ||||
|     searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false) | ||||
|     searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues) | ||||
|       .join(Priorities) | ||||
|       .on { | ||||
|         case t1 ~ t2 => | ||||
| @@ -171,42 +188,11 @@ trait IssuesService { | ||||
|       .toMap | ||||
|   } | ||||
|  | ||||
|   def getCommitStatues(userName: String, repositoryName: String, issueId: Int)( | ||||
|     implicit s: Session | ||||
|   ): Option[CommitStatusInfo] = { | ||||
|     val status = PullRequests | ||||
|       .filter { pr => | ||||
|         pr.userName === userName.bind && pr.repositoryName === repositoryName.bind && pr.issueId === issueId.bind | ||||
|       } | ||||
|       .join(CommitStatuses) | ||||
|       .on { | ||||
|         case pr ~ cs => | ||||
|           pr.userName === cs.userName && pr.repositoryName === cs.repositoryName && pr.commitIdTo === cs.commitId | ||||
|       } | ||||
|       .list | ||||
|  | ||||
|     if (status.nonEmpty) { | ||||
|       val (_, cs) = status.head | ||||
|       Some( | ||||
|         CommitStatusInfo( | ||||
|           count = status.length, | ||||
|           successCount = status.count(_._2.state == CommitState.SUCCESS), | ||||
|           context = (if (status.length == 1) Some(cs.context) else None), | ||||
|           state = (if (status.length == 1) Some(cs.state) else None), | ||||
|           targetUrl = (if (status.length == 1) cs.targetUrl else None), | ||||
|           description = (if (status.length == 1) cs.description else None) | ||||
|         ) | ||||
|       ) | ||||
|     } else { | ||||
|       None | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns the search result against issues. | ||||
|    * | ||||
|    * @param condition the search condition | ||||
|    * @param pullRequest if true then returns only pull requests, false then returns only issues. | ||||
|    * @param searchOption 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 | ||||
| @@ -214,13 +200,13 @@ trait IssuesService { | ||||
|    */ | ||||
|   def searchIssue( | ||||
|     condition: IssueSearchCondition, | ||||
|     pullRequest: Boolean, | ||||
|     searchOption: IssueSearchOption, | ||||
|     offset: Int, | ||||
|     limit: Int, | ||||
|     repos: (String, String)* | ||||
|   )(implicit s: Session): List[IssueInfo] = { | ||||
|     // get issues and comment count and labels | ||||
|     val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos) | ||||
|     val result = searchIssueQueryBase(condition, searchOption, offset, limit, repos) | ||||
|       .joinLeft(IssueLabels) | ||||
|       .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } | ||||
|       .joinLeft(Labels) | ||||
| @@ -229,9 +215,11 @@ trait IssuesService { | ||||
|       .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } | ||||
|       .joinLeft(Priorities) | ||||
|       .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) } | ||||
|       .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc } | ||||
|       .joinLeft(PullRequests) | ||||
|       .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) } | ||||
|       .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc } | ||||
|       .map { | ||||
|         case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => | ||||
|         case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => | ||||
|           ( | ||||
|             t1, | ||||
|             t2.commentCount, | ||||
| @@ -239,7 +227,8 @@ trait IssuesService { | ||||
|             t4.map(_.labelName), | ||||
|             t4.map(_.color), | ||||
|             t5.map(_.title), | ||||
|             t6.map(_.priorityName) | ||||
|             t6.map(_.priorityName), | ||||
|             t7.map(_.commitIdTo) | ||||
|           ) | ||||
|       } | ||||
|       .list | ||||
| @@ -249,7 +238,7 @@ trait IssuesService { | ||||
|  | ||||
|     result.map { issues => | ||||
|       issues.head match { | ||||
|         case (issue, commentCount, _, _, _, milestone, priority) => | ||||
|         case (issue, commentCount, _, _, _, milestone, priority, commitId) => | ||||
|           IssueInfo( | ||||
|             issue, | ||||
|             issues.flatMap { t => | ||||
| @@ -258,7 +247,7 @@ trait IssuesService { | ||||
|             milestone, | ||||
|             priority, | ||||
|             commentCount, | ||||
|             getCommitStatues(issue.userName, issue.repositoryName, issue.issueId) | ||||
|             commitId | ||||
|           ) | ||||
|       } | ||||
|     } toList | ||||
| @@ -271,7 +260,7 @@ trait IssuesService { | ||||
|     implicit s: Session | ||||
|   ): List[(Issue, Account, Option[Account])] = { | ||||
|     // get issues and comment count and labels | ||||
|     searchIssueQueryBase(condition, false, offset, limit, repos) | ||||
|     searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos) | ||||
|       .join(Accounts) | ||||
|       .on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName } | ||||
|       .joinLeft(Accounts) | ||||
| @@ -288,7 +277,7 @@ trait IssuesService { | ||||
|     implicit s: Session | ||||
|   ): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = { | ||||
|     // get issues and comment count and labels | ||||
|     searchIssueQueryBase(condition, true, offset, limit, repos) | ||||
|     searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos) | ||||
|       .join(PullRequests) | ||||
|       .on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } | ||||
|       .join(Repositories) | ||||
| @@ -306,12 +295,12 @@ trait IssuesService { | ||||
|  | ||||
|   private def searchIssueQueryBase( | ||||
|     condition: IssueSearchCondition, | ||||
|     pullRequest: Boolean, | ||||
|     searchOption: IssueSearchOption, | ||||
|     offset: Int, | ||||
|     limit: Int, | ||||
|     repos: Seq[(String, String)] | ||||
|   )(implicit s: Session) = | ||||
|     searchIssueQuery(repos, condition, pullRequest) | ||||
|     searchIssueQuery(repos, condition, searchOption) | ||||
|       .join(IssueOutline) | ||||
|       .on { (t1, t2) => | ||||
|         t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) | ||||
| @@ -349,7 +338,11 @@ trait IssuesService { | ||||
|   /** | ||||
|    * Assembles query for conditional issue searching. | ||||
|    */ | ||||
|   private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)( | ||||
|   private def searchIssueQuery( | ||||
|     repos: Seq[(String, String)], | ||||
|     condition: IssueSearchCondition, | ||||
|     searchOption: IssueSearchOption | ||||
|   )( | ||||
|     implicit s: Session | ||||
|   ) = | ||||
|     Issues filter { t1 => | ||||
| @@ -363,7 +356,11 @@ trait IssuesService { | ||||
|       (t1.priorityId.? isEmpty, condition.priority == Some(None)) && | ||||
|       (t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) && | ||||
|       (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && | ||||
|       (t1.pullRequest === pullRequest.bind) && | ||||
|       (searchOption match { | ||||
|         case IssueSearchOption.Issues       => t1.pullRequest === false | ||||
|         case IssueSearchOption.PullRequests => t1.pullRequest === true | ||||
|         case IssueSearchOption.Both         => t1.pullRequest === false || t1.pullRequest === true | ||||
|       }) && | ||||
|       // Milestone filter | ||||
|       (Milestones filter { t2 => | ||||
|         (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && | ||||
| @@ -635,7 +632,10 @@ trait IssuesService { | ||||
|     IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate) | ||||
|   } | ||||
|  | ||||
|   def deleteComment(issueId: Int, commentId: Int)(implicit s: Session): Int = { | ||||
|   def deleteComment(owner: String, repository: String, issueId: Int, commentId: Int)( | ||||
|     implicit context: Context, | ||||
|     s: Session | ||||
|   ): Int = { | ||||
|     Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate) | ||||
|     IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match { | ||||
|       case Some(c) if c.action == "reopen_comment" => | ||||
| @@ -644,6 +644,16 @@ trait IssuesService { | ||||
|         IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close") | ||||
|       case Some(_) => | ||||
|         IssueComments.filter(_.byPrimaryKey(commentId)).delete | ||||
|         IssueComments insert IssueComment( | ||||
|           userName = owner, | ||||
|           repositoryName = repository, | ||||
|           issueId = issueId, | ||||
|           action = "delete_comment", | ||||
|           commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"), | ||||
|           content = s"", | ||||
|           registeredDate = currentDate, | ||||
|           updatedDate = currentDate | ||||
|         ) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -910,27 +920,46 @@ object IssuesService { | ||||
|         param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) | ||||
|       ) | ||||
|  | ||||
|     def apply(request: HttpServletRequest, milestone: String): IssueSearchCondition = | ||||
|       IssueSearchCondition( | ||||
|         param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), | ||||
|         Some(Some(milestone)), | ||||
|         param(request, "priority").map { | ||||
|           case "none" => None | ||||
|           case x      => Some(x) | ||||
|         }, | ||||
|         param(request, "author"), | ||||
|         param(request, "assigned").map { | ||||
|           case "none" => None | ||||
|           case x      => Some(x) | ||||
|         }, | ||||
|         param(request, "mentioned"), | ||||
|         param(request, "state", Seq("open", "closed")).getOrElse("open"), | ||||
|         param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"), | ||||
|         param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), | ||||
|         param(request, "visibility"), | ||||
|         param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) | ||||
|       ) | ||||
|  | ||||
|     def page(request: HttpServletRequest) = { | ||||
|       PaginationHelper.page(param(request, "page")) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   case class CommitStatusInfo( | ||||
|     count: Int, | ||||
|     successCount: Int, | ||||
|     context: Option[String], | ||||
|     state: Option[CommitState], | ||||
|     targetUrl: Option[String], | ||||
|     description: Option[String] | ||||
|   ) | ||||
|  | ||||
|   case class IssueInfo( | ||||
|     issue: Issue, | ||||
|     labels: List[Label], | ||||
|     milestone: Option[String], | ||||
|     priority: Option[String], | ||||
|     commentCount: Int, | ||||
|     status: Option[CommitStatusInfo] | ||||
|     commitId: Option[String] | ||||
|   ) | ||||
|  | ||||
| } | ||||
|  | ||||
| sealed trait IssueSearchOption | ||||
|  | ||||
| object IssueSearchOption { | ||||
|   case object Issues extends IssueSearchOption | ||||
|   case object PullRequests extends IssueSearchOption | ||||
|   case object Both extends IssueSearchOption | ||||
| } | ||||
|   | ||||
| @@ -2,16 +2,18 @@ package gitbucket.core.service | ||||
|  | ||||
| import gitbucket.core.api.JsonFormat | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.model.{Account, PullRequest, WebHook} | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.model.{Account, Issue, PullRequest, WebHook} | ||||
| import gitbucket.core.plugin.{PluginRegistry, ReceiveHook} | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.util.{JGitUtil, LockUtil} | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo} | ||||
| import gitbucket.core.service.SystemSettingsService.SystemSettings | ||||
| import gitbucket.core.util.JGitUtil.CommitInfo | ||||
| import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger} | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.transport.RefSpec | ||||
| import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack, RefSpec} | ||||
| import org.eclipse.jgit.errors.NoMergeBaseException | ||||
| import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository} | ||||
| import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} | ||||
| @@ -35,7 +37,7 @@ trait MergeService { | ||||
|    */ | ||||
|   def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = { | ||||
|     Using.resource(Git.open(getRepositoryDir(userName, repositoryName))) { git => | ||||
|       new MergeCacheInfo(git, branch, issueId).checkConflict() | ||||
|       new MergeCacheInfo(git, userName, repositoryName, branch, issueId, Nil).checkConflict() | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -52,41 +54,47 @@ trait MergeService { | ||||
|     issueId: Int | ||||
|   ): Option[Option[String]] = { | ||||
|     Using.resource(Git.open(getRepositoryDir(userName, repositoryName))) { git => | ||||
|       new MergeCacheInfo(git, branch, issueId).checkConflictCache() | ||||
|       new MergeCacheInfo(git, userName, repositoryName, branch, issueId, Nil).checkConflictCache() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** merge the pull request with a merge commit */ | ||||
|   def mergePullRequest( | ||||
|   def mergeWithMergeCommit( | ||||
|     git: Git, | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     message: String, | ||||
|     committer: PersonIdent | ||||
|   ): ObjectId = { | ||||
|     new MergeCacheInfo(git, branch, issueId).merge(message, committer) | ||||
|   )(implicit s: Session): ObjectId = { | ||||
|     new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).merge(message, committer) | ||||
|   } | ||||
|  | ||||
|   /** rebase to the head of the pull request branch */ | ||||
|   def rebasePullRequest( | ||||
|   def mergeWithRebase( | ||||
|     git: Git, | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     commits: Seq[RevCommit], | ||||
|     committer: PersonIdent | ||||
|   ): ObjectId = { | ||||
|     new MergeCacheInfo(git, branch, issueId).rebase(committer, commits) | ||||
|   )(implicit s: Session): ObjectId = { | ||||
|     new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).rebase(committer, commits) | ||||
|   } | ||||
|  | ||||
|   /** squash commits in the pull request and append it */ | ||||
|   def squashPullRequest( | ||||
|   def mergeWithSquash( | ||||
|     git: Git, | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     message: String, | ||||
|     committer: PersonIdent | ||||
|   ): ObjectId = { | ||||
|     new MergeCacheInfo(git, branch, issueId).squash(message, committer) | ||||
|   )(implicit s: Session): ObjectId = { | ||||
|     new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).squash(message, committer) | ||||
|   } | ||||
|  | ||||
|   /** fetch remote branch to my repository refs/pull/{issueId}/head */ | ||||
| @@ -168,7 +176,7 @@ trait MergeService { | ||||
|     remoteBranch: String, | ||||
|     loginAccount: Account, | ||||
|     message: String, | ||||
|     pullreq: Option[PullRequest], | ||||
|     pullRequest: Option[PullRequest], | ||||
|     settings: SystemSettings | ||||
|   )(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = { | ||||
|     val localUserName = localRepository.owner | ||||
| @@ -200,13 +208,14 @@ trait MergeService { | ||||
|           } | ||||
|  | ||||
|           // record activity | ||||
|           recordPushActivity( | ||||
|           val pushInfo = PushInfo( | ||||
|             localUserName, | ||||
|             localRepositoryName, | ||||
|             loginAccount.userName, | ||||
|             localBranch, | ||||
|             commits | ||||
|           ) | ||||
|           recordActivity(pushInfo) | ||||
|  | ||||
|           // close issue by commit message | ||||
|           if (localBranch == localRepository.repository.defaultBranch) { | ||||
| @@ -215,6 +224,14 @@ trait MergeService { | ||||
|                 .foreach { issueId => | ||||
|                   getIssue(localRepository.owner, localRepository.name, issueId.toString).foreach { issue => | ||||
|                     callIssuesWebHook("closed", localRepository, issue, loginAccount, settings) | ||||
|                     val closeIssueInfo = CloseIssueInfo( | ||||
|                       localRepository.owner, | ||||
|                       localRepository.name, | ||||
|                       localUserName, | ||||
|                       issue.issueId, | ||||
|                       issue.title | ||||
|                     ) | ||||
|                     recordActivity(closeIssueInfo) | ||||
|                     PluginRegistry().getIssueHooks | ||||
|                       .foreach( | ||||
|                         _.closedByCommitComment(issue, localRepository, commit.fullMessage, loginAccount) | ||||
| @@ -224,7 +241,7 @@ trait MergeService { | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           pullreq.foreach { pullreq => | ||||
|           pullRequest.foreach { pullRequest => | ||||
|             callWebHookOf(localRepository.owner, localRepository.name, WebHook.Push, settings) { | ||||
|               for { | ||||
|                 ownerAccount <- getAccountByUserName(localRepository.owner) | ||||
| @@ -232,7 +249,7 @@ trait MergeService { | ||||
|                 WebHookService.WebHookPushPayload( | ||||
|                   git, | ||||
|                   loginAccount, | ||||
|                   pullreq.requestBranch, | ||||
|                   pullRequest.requestBranch, | ||||
|                   localRepository, | ||||
|                   commits, | ||||
|                   ownerAccount, | ||||
| @@ -247,6 +264,10 @@ trait MergeService { | ||||
|     }.toOption | ||||
|   } | ||||
|  | ||||
|   protected def getReceiveHooks(): Seq[ReceiveHook] = { | ||||
|     PluginRegistry().getReceiveHooks | ||||
|   } | ||||
|  | ||||
|   def mergePullRequest( | ||||
|     repository: RepositoryInfo, | ||||
|     issueId: Int, | ||||
| @@ -261,73 +282,52 @@ trait MergeService { | ||||
|         LockUtil.lock(s"${repository.owner}/${repository.name}") { | ||||
|           getPullRequest(repository.owner, repository.name, issueId) | ||||
|             .map { | ||||
|               case (issue, pullreq) => | ||||
|               case (issue, pullRequest) => | ||||
|                 Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|                   val (commits, _) = getRequestCompareInfo( | ||||
|                     repository.owner, | ||||
|                     repository.name, | ||||
|                     pullRequest.commitIdFrom, | ||||
|                     pullRequest.requestUserName, | ||||
|                     pullRequest.requestRepositoryName, | ||||
|                     pullRequest.commitIdTo | ||||
|                   ) | ||||
|  | ||||
|                   // merge git repository | ||||
|                   mergeGitRepository( | ||||
|                     git, | ||||
|                     repository, | ||||
|                     issue, | ||||
|                     pullRequest, | ||||
|                     loginAccount, | ||||
|                     message, | ||||
|                     strategy, | ||||
|                     commits, | ||||
|                     getReceiveHooks() | ||||
|                   ) match { | ||||
|                     case Some(newCommitId) => | ||||
|                       // mark issue as merged and close. | ||||
|                       val commentId = | ||||
|                     createComment(repository.owner, repository.name, loginAccount.userName, issueId, message, "merge") | ||||
|                         createComment( | ||||
|                           repository.owner, | ||||
|                           repository.name, | ||||
|                           loginAccount.userName, | ||||
|                           issueId, | ||||
|                           message, | ||||
|                           "merge" | ||||
|                         ) | ||||
|                       createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close") | ||||
|                       updateClosed(repository.owner, repository.name, issueId, true) | ||||
|  | ||||
|                       // record activity | ||||
|                   recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, message) | ||||
|                       val mergeInfo = | ||||
|                         MergeInfo(repository.owner, repository.name, loginAccount.userName, issueId, message) | ||||
|                       recordActivity(mergeInfo) | ||||
|                       updateLastActivityDate(repository.owner, repository.name) | ||||
|  | ||||
|                   val (commits, _) = getRequestCompareInfo( | ||||
|                     repository.owner, | ||||
|                     repository.name, | ||||
|                     pullreq.commitIdFrom, | ||||
|                     pullreq.requestUserName, | ||||
|                     pullreq.requestRepositoryName, | ||||
|                     pullreq.commitIdTo | ||||
|                   ) | ||||
|  | ||||
|                   val revCommits = Using | ||||
|                     .resource(new RevWalk(git.getRepository)) { revWalk => | ||||
|                       commits.flatten.map { commit => | ||||
|                         revWalk.parseCommit(git.getRepository.resolve(commit.id)) | ||||
|                       } | ||||
|                     } | ||||
|                     .reverse | ||||
|  | ||||
|                   // merge git repository | ||||
|                   (strategy match { | ||||
|                     case "merge-commit" => | ||||
|                       Some( | ||||
|                         mergePullRequest( | ||||
|                           git, | ||||
|                           pullreq.branch, | ||||
|                           issueId, | ||||
|                           s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + message, | ||||
|                           new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) | ||||
|                         ) | ||||
|                       ) | ||||
|                     case "rebase" => | ||||
|                       Some( | ||||
|                         rebasePullRequest( | ||||
|                           git, | ||||
|                           pullreq.branch, | ||||
|                           issueId, | ||||
|                           revCommits, | ||||
|                           new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) | ||||
|                         ) | ||||
|                       ) | ||||
|                     case "squash" => | ||||
|                       Some( | ||||
|                         squashPullRequest( | ||||
|                           git, | ||||
|                           pullreq.branch, | ||||
|                           issueId, | ||||
|                           s"${issue.title} (#${issueId})\n\n" + message, | ||||
|                           new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) | ||||
|                         ) | ||||
|                       ) | ||||
|                     case _ => | ||||
|                       None | ||||
|                   }) match { | ||||
|                     case Some(newCommitId) => | ||||
|                       // close issue by content of pull request | ||||
|                       val defaultBranch = getRepository(repository.owner, repository.name).get.repository.defaultBranch | ||||
|                       if (pullreq.branch == defaultBranch) { | ||||
|                       if (pullRequest.branch == defaultBranch) { | ||||
|                         commits.flatten.foreach { commit => | ||||
|                           closeIssuesFromMessage( | ||||
|                             commit.fullMessage, | ||||
| @@ -337,6 +337,14 @@ trait MergeService { | ||||
|                           ).foreach { issueId => | ||||
|                             getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||
|                               callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) | ||||
|                               val closeIssueInfo = CloseIssueInfo( | ||||
|                                 repository.owner, | ||||
|                                 repository.name, | ||||
|                                 loginAccount.userName, | ||||
|                                 issue.issueId, | ||||
|                                 issue.title | ||||
|                               ) | ||||
|                               recordActivity(closeIssueInfo) | ||||
|                               PluginRegistry().getIssueHooks | ||||
|                                 .foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount)) | ||||
|                             } | ||||
| @@ -351,6 +359,14 @@ trait MergeService { | ||||
|                         ).foreach { issueId => | ||||
|                           getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||
|                             callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) | ||||
|                             val closeIssueInfo = CloseIssueInfo( | ||||
|                               repository.owner, | ||||
|                               repository.name, | ||||
|                               loginAccount.userName, | ||||
|                               issue.issueId, | ||||
|                               issue.title | ||||
|                             ) | ||||
|                             recordActivity(closeIssueInfo) | ||||
|                             PluginRegistry().getIssueHooks | ||||
|                               .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) | ||||
|                           } | ||||
| @@ -359,6 +375,14 @@ trait MergeService { | ||||
|                           .foreach { issueId => | ||||
|                             getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||
|                               callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) | ||||
|                               val closeIssueInfo = CloseIssueInfo( | ||||
|                                 repository.owner, | ||||
|                                 repository.name, | ||||
|                                 loginAccount.userName, | ||||
|                                 issue.issueId, | ||||
|                                 issue.title | ||||
|                               ) | ||||
|                               recordActivity(closeIssueInfo) | ||||
|                               PluginRegistry().getIssueHooks | ||||
|                                 .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) | ||||
|                             } | ||||
| @@ -370,7 +394,7 @@ trait MergeService { | ||||
|                       updatePullRequests( | ||||
|                         repository.owner, | ||||
|                         repository.name, | ||||
|                         pullreq.branch, | ||||
|                         pullRequest.branch, | ||||
|                         loginAccount, | ||||
|                         "closed", | ||||
|                         settings | ||||
| @@ -394,6 +418,67 @@ trait MergeService { | ||||
|       } else Left("Strategy not allowed") | ||||
|     } else Left("Draft pull requests cannot be merged") | ||||
|   } | ||||
|  | ||||
|   private def mergeGitRepository( | ||||
|     git: Git, | ||||
|     repository: RepositoryInfo, | ||||
|     issue: Issue, | ||||
|     pullRequest: PullRequest, | ||||
|     loginAccount: Account, | ||||
|     message: String, | ||||
|     strategy: String, | ||||
|     commits: Seq[Seq[CommitInfo]], | ||||
|     receiveHooks: Seq[ReceiveHook] | ||||
|   )(implicit s: Session): Option[ObjectId] = { | ||||
|     val revCommits = Using | ||||
|       .resource(new RevWalk(git.getRepository)) { revWalk => | ||||
|         commits.flatten.map { commit => | ||||
|           revWalk.parseCommit(git.getRepository.resolve(commit.id)) | ||||
|         } | ||||
|       } | ||||
|       .reverse | ||||
|  | ||||
|     strategy match { | ||||
|       case "merge-commit" => | ||||
|         Some( | ||||
|           mergeWithMergeCommit( | ||||
|             git, | ||||
|             repository.owner, | ||||
|             repository.name, | ||||
|             pullRequest.branch, | ||||
|             issue.issueId, | ||||
|             s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message, | ||||
|             new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) | ||||
|           ) | ||||
|         ) | ||||
|       case "rebase" => | ||||
|         Some( | ||||
|           mergeWithRebase( | ||||
|             git, | ||||
|             repository.owner, | ||||
|             repository.name, | ||||
|             pullRequest.branch, | ||||
|             issue.issueId, | ||||
|             revCommits, | ||||
|             new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) | ||||
|           ) | ||||
|         ) | ||||
|       case "squash" => | ||||
|         Some( | ||||
|           mergeWithSquash( | ||||
|             git, | ||||
|             repository.owner, | ||||
|             repository.name, | ||||
|             pullRequest.branch, | ||||
|             issue.issueId, | ||||
|             s"${issue.title} (#${issue.issueId})\n\n" + message, | ||||
|             new PersonIdent(loginAccount.fullName, loginAccount.mailAddress) | ||||
|           ) | ||||
|         ) | ||||
|       case _ => | ||||
|         None | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| object MergeService { | ||||
| @@ -440,18 +525,22 @@ object MergeService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   class MergeCacheInfo(git: Git, branch: String, issueId: Int) { | ||||
|  | ||||
|     private val repository = git.getRepository | ||||
|  | ||||
|   class MergeCacheInfo( | ||||
|     git: Git, | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     receiveHooks: Seq[ReceiveHook] | ||||
|   ) { | ||||
|     private val mergedBranchName = s"refs/pull/${issueId}/merge" | ||||
|     private val conflictedBranchName = s"refs/pull/${issueId}/conflict" | ||||
|  | ||||
|     lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}") | ||||
|     lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head") | ||||
|     lazy val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}") | ||||
|     lazy val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head") | ||||
|  | ||||
|     def checkConflictCache(): Option[Option[String]] = { | ||||
|       Option(repository.resolve(mergedBranchName)) | ||||
|       Option(git.getRepository.resolve(mergedBranchName)) | ||||
|         .flatMap { merged => | ||||
|           if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) { | ||||
|             // merged branch exists | ||||
| @@ -460,7 +549,7 @@ object MergeService { | ||||
|             None | ||||
|           } | ||||
|         } | ||||
|         .orElse(Option(repository.resolve(conflictedBranchName)).flatMap { conflicted => | ||||
|         .orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted => | ||||
|           val commit = parseCommit(conflicted) | ||||
|           if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) { | ||||
|             // conflict branch exists | ||||
| @@ -476,19 +565,19 @@ object MergeService { | ||||
|     } | ||||
|  | ||||
|     def checkConflictForce(): Option[String] = { | ||||
|       val merger = MergeStrategy.RECURSIVE.newMerger(repository, true) | ||||
|       val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) | ||||
|       val conflicted = try { | ||||
|         !merger.merge(mergeBaseTip, mergeTip) | ||||
|       } catch { | ||||
|         case e: NoMergeBaseException => true | ||||
|       } | ||||
|       val mergeTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeTip)) | ||||
|       val mergeTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeTip)) | ||||
|       val committer = mergeTipCommit.getCommitterIdent | ||||
|  | ||||
|       def _updateBranch(treeId: ObjectId, message: String, branchName: String): Unit = { | ||||
|         // creates merge commit | ||||
|         val mergeCommitId = createMergeCommit(treeId, committer, message) | ||||
|         Util.updateRefs(repository, branchName, mergeCommitId, true, committer) | ||||
|         Util.updateRefs(git.getRepository, branchName, mergeCommitId, true, committer) | ||||
|       } | ||||
|  | ||||
|       if (!conflicted) { | ||||
| @@ -504,26 +593,48 @@ object MergeService { | ||||
|     } | ||||
|  | ||||
|     // update branch from cache | ||||
|     def merge(message: String, committer: PersonIdent): ObjectId = { | ||||
|     def merge(message: String, committer: PersonIdent)(implicit s: Session): ObjectId = { | ||||
|       if (checkConflict().isDefined) { | ||||
|         throw new RuntimeException("This pull request can't merge automatically.") | ||||
|       } | ||||
|       val mergeResultCommit = parseCommit(Option(repository.resolve(mergedBranchName)).getOrElse { | ||||
|       val mergeResultCommit = parseCommit(Option(git.getRepository.resolve(mergedBranchName)).getOrElse { | ||||
|         throw new RuntimeException(s"Not found branch ${mergedBranchName}") | ||||
|       }) | ||||
|       // creates merge commit | ||||
|       val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message) | ||||
|       // update refs | ||||
|       Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged")) | ||||
|  | ||||
|       val refName = s"refs/heads/${branch}" | ||||
|       val currentObjectId = git.getRepository.resolve(refName) | ||||
|       val receivePack = new ReceivePack(git.getRepository) | ||||
|       val receiveCommand = new ReceiveCommand(currentObjectId, mergeCommitId, refName) | ||||
|  | ||||
|       // call pre-commit hooks | ||||
|       val error = receiveHooks.flatMap { hook => | ||||
|         hook.preReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||
|       }.headOption | ||||
|  | ||||
|       error.foreach { error => | ||||
|         throw new RuntimeException(error) | ||||
|       } | ||||
|  | ||||
|     def rebase(committer: PersonIdent, commits: Seq[RevCommit]): ObjectId = { | ||||
|       // update refs | ||||
|       val objectId = Util.updateRefs(git.getRepository, refName, mergeCommitId, false, committer, Some("merged")) | ||||
|  | ||||
|       // call post-commit hook | ||||
|       receiveHooks.foreach { hook => | ||||
|         hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||
|       } | ||||
|  | ||||
|       objectId | ||||
|     } | ||||
|  | ||||
|     def rebase(committer: PersonIdent, commits: Seq[RevCommit])(implicit s: Session): ObjectId = { | ||||
|       if (checkConflict().isDefined) { | ||||
|         throw new RuntimeException("This pull request can't merge automatically.") | ||||
|       } | ||||
|  | ||||
|       def _cloneCommit(commit: RevCommit, parentId: ObjectId, baseId: ObjectId): CommitBuilder = { | ||||
|         val merger = MergeStrategy.RECURSIVE.newMerger(repository, true) | ||||
|         val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) | ||||
|         merger.merge(commit.toObjectId, baseId) | ||||
|  | ||||
|         val newCommit = new CommitBuilder() | ||||
| @@ -535,10 +646,10 @@ object MergeService { | ||||
|         newCommit | ||||
|       } | ||||
|  | ||||
|       val mergeBaseTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeBaseTip)) | ||||
|       val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip)) | ||||
|       var previousId = mergeBaseTipCommit.getId | ||||
|  | ||||
|       Using.resource(repository.newObjectInserter) { inserter => | ||||
|       Using.resource(git.getRepository.newObjectInserter) { inserter => | ||||
|         commits.foreach { commit => | ||||
|           val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId) | ||||
|           previousId = inserter.insert(nextCommit) | ||||
| @@ -546,17 +657,40 @@ object MergeService { | ||||
|         inserter.flush() | ||||
|       } | ||||
|  | ||||
|       Util.updateRefs(repository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased")) | ||||
|       val refName = s"refs/heads/${branch}" | ||||
|       val currentObjectId = git.getRepository.resolve(refName) | ||||
|       val receivePack = new ReceivePack(git.getRepository) | ||||
|       val receiveCommand = new ReceiveCommand(currentObjectId, previousId, refName) | ||||
|  | ||||
|       // call pre-commit hooks | ||||
|       val error = receiveHooks.flatMap { hook => | ||||
|         hook.preReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||
|       }.headOption | ||||
|  | ||||
|       error.foreach { error => | ||||
|         throw new RuntimeException(error) | ||||
|       } | ||||
|  | ||||
|     def squash(message: String, committer: PersonIdent): ObjectId = { | ||||
|       // update refs | ||||
|       val objectId = | ||||
|         Util.updateRefs(git.getRepository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased")) | ||||
|  | ||||
|       // call post-commit hook | ||||
|       receiveHooks.foreach { hook => | ||||
|         hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||
|       } | ||||
|  | ||||
|       objectId | ||||
|     } | ||||
|  | ||||
|     def squash(message: String, committer: PersonIdent)(implicit s: Session): ObjectId = { | ||||
|       if (checkConflict().isDefined) { | ||||
|         throw new RuntimeException("This pull request can't merge automatically.") | ||||
|       } | ||||
|  | ||||
|       val mergeBaseTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeBaseTip)) | ||||
|       val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip)) | ||||
|       val mergeBranchHeadCommit = | ||||
|         Using.resource(new RevWalk(repository))(_.parseCommit(repository.resolve(mergedBranchName))) | ||||
|         Using.resource(new RevWalk(git.getRepository))(_.parseCommit(git.getRepository.resolve(mergedBranchName))) | ||||
|  | ||||
|       // Create squash commit | ||||
|       val mergeCommit = new CommitBuilder() | ||||
| @@ -567,30 +701,52 @@ object MergeService { | ||||
|       mergeCommit.setMessage(message) | ||||
|  | ||||
|       // insertObject and got squash commit Object Id | ||||
|       val newCommitId = Using.resource(repository.newObjectInserter) { inserter => | ||||
|       val newCommitId = Using.resource(git.getRepository.newObjectInserter) { inserter => | ||||
|         val newCommitId = inserter.insert(mergeCommit) | ||||
|         inserter.flush() | ||||
|         newCommitId | ||||
|       } | ||||
|  | ||||
|       Util.updateRefs(repository, mergedBranchName, newCommitId, true, committer) | ||||
|       val refName = s"refs/heads/${branch}" | ||||
|       val currentObjectId = git.getRepository.resolve(refName) | ||||
|       val receivePack = new ReceivePack(git.getRepository) | ||||
|       val receiveCommand = new ReceiveCommand(currentObjectId, newCommitId, refName) | ||||
|  | ||||
|       // call pre-commit hooks | ||||
|       val error = receiveHooks.flatMap { hook => | ||||
|         hook.preReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||
|       }.headOption | ||||
|  | ||||
|       error.foreach { error => | ||||
|         throw new RuntimeException(error) | ||||
|       } | ||||
|  | ||||
|       // update refs | ||||
|       Util.updateRefs(git.getRepository, mergedBranchName, newCommitId, true, committer) | ||||
|  | ||||
|       // rebase to squash commit | ||||
|       Util.updateRefs( | ||||
|         repository, | ||||
|       val objectId = Util.updateRefs( | ||||
|         git.getRepository, | ||||
|         s"refs/heads/${branch}", | ||||
|         repository.resolve(mergedBranchName), | ||||
|         git.getRepository.resolve(mergedBranchName), | ||||
|         false, | ||||
|         committer, | ||||
|         Some("squashed") | ||||
|       ) | ||||
|  | ||||
|       // call post-commit hook | ||||
|       receiveHooks.foreach { hook => | ||||
|         hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||
|       } | ||||
|  | ||||
|       objectId | ||||
|     } | ||||
|  | ||||
|     // return treeId | ||||
|     private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) = | ||||
|       Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip)) | ||||
|       Util.createMergeCommit(git.getRepository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip)) | ||||
|  | ||||
|     private def parseCommit(id: ObjectId) = Using.resource(new RevWalk(repository))(_.parseCommit(id)) | ||||
|     private def parseCommit(id: ObjectId) = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(id)) | ||||
|  | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -13,8 +13,8 @@ trait MilestonesService { | ||||
|     title: String, | ||||
|     description: Option[String], | ||||
|     dueDate: Option[java.util.Date] | ||||
|   )(implicit s: Session): Unit = | ||||
|     Milestones insert Milestone( | ||||
|   )(implicit s: Session): Int = { | ||||
|     Milestones returning Milestones.map(_.milestoneId) insert Milestone( | ||||
|       userName = owner, | ||||
|       repositoryName = repository, | ||||
|       title = title, | ||||
| @@ -22,6 +22,7 @@ trait MilestonesService { | ||||
|       dueDate = dueDate, | ||||
|       closedDate = None | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   def updateMilestone(milestone: Milestone)(implicit s: Session): Unit = | ||||
|     Milestones | ||||
|   | ||||
| @@ -24,13 +24,15 @@ trait ProtectedBranchService { | ||||
|       } | ||||
|       .map { | ||||
|         case (t1, contexts) => | ||||
|           new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin) | ||||
|           new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin) | ||||
|       } | ||||
|  | ||||
|   def getProtectedBranchInfo(owner: String, repository: String, branch: String)( | ||||
|     implicit session: Session | ||||
|   ): ProtectedBranchInfo = | ||||
|     getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository)) | ||||
|     getProtectedBranchInfoOpt(owner, repository, branch).getOrElse( | ||||
|       ProtectedBranchInfo.disabled(owner, repository, branch) | ||||
|     ) | ||||
|  | ||||
|   def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] = | ||||
|     ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list | ||||
| @@ -66,7 +68,22 @@ object ProtectedBranchService { | ||||
|       repository: String, | ||||
|       receivePack: ReceivePack, | ||||
|       command: ReceiveCommand, | ||||
|       pusher: String | ||||
|       pusher: String, | ||||
|       mergePullRequest: Boolean | ||||
|     )(implicit session: Session): Option[String] = { | ||||
|       if (mergePullRequest == true) { | ||||
|         None | ||||
|       } else { | ||||
|         checkBranchProtection(owner, repository, receivePack, command, pusher) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     private def checkBranchProtection( | ||||
|       owner: String, | ||||
|       repository: String, | ||||
|       receivePack: ReceivePack, | ||||
|       command: ReceiveCommand, | ||||
|       pusher: String, | ||||
|     )(implicit session: Session): Option[String] = { | ||||
|       val branch = command.getRefName.stripPrefix("refs/heads/") | ||||
|       if (branch != command.getRefName) { | ||||
| @@ -91,6 +108,7 @@ object ProtectedBranchService { | ||||
|   case class ProtectedBranchInfo( | ||||
|     owner: String, | ||||
|     repository: String, | ||||
|     branch: String, | ||||
|     enabled: Boolean, | ||||
|     /** | ||||
|      * Require status checks to pass before merging | ||||
| @@ -151,7 +169,7 @@ object ProtectedBranchService { | ||||
|       if (contexts.isEmpty) { | ||||
|         Set.empty | ||||
|       } else { | ||||
|         contexts.toSet -- getCommitStatues(owner, repository, sha1) | ||||
|         contexts.toSet -- getCommitStatuses(owner, repository, sha1) | ||||
|           .filter(_.state == CommitState.SUCCESS) | ||||
|           .map(_.context) | ||||
|           .toSet | ||||
| @@ -165,7 +183,7 @@ object ProtectedBranchService { | ||||
|     } | ||||
|   } | ||||
|   object ProtectedBranchInfo { | ||||
|     def disabled(owner: String, repository: String): ProtectedBranchInfo = | ||||
|       ProtectedBranchInfo(owner, repository, false, Nil, false) | ||||
|     def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo = | ||||
|       ProtectedBranchInfo(owner, repository, branch, false, Nil, false) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -7,13 +7,14 @@ import difflib.{Delta, DiffUtils} | ||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||
| import gitbucket.core.api.JsonFormat | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.model.activity.OpenPullRequestInfo | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.SystemSettingsService.SystemSettings | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util.JGitUtil | ||||
| import gitbucket.core.util.StringUtil._ | ||||
| import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo} | ||||
| import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo, getBranches} | ||||
| import gitbucket.core.view | ||||
| import gitbucket.core.view.helpers | ||||
| import org.eclipse.jgit.api.Git | ||||
| @@ -57,6 +58,15 @@ trait PullRequestService { | ||||
|       .map(pr => pr.isDraft) | ||||
|       .update(false) | ||||
|  | ||||
|   def updateBaseBranch(owner: String, repository: String, issueId: Int, baseBranch: String, commitIdTo: String)( | ||||
|     implicit s: Session | ||||
|   ): Unit = { | ||||
|     PullRequests | ||||
|       .filter(_.byPrimaryKey(owner, repository, issueId)) | ||||
|       .map(pr => pr.branch -> pr.commitIdTo) | ||||
|       .update((baseBranch, commitIdTo)) | ||||
|   } | ||||
|  | ||||
|   def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])( | ||||
|     implicit s: Session | ||||
|   ): List[PullRequestCount] = | ||||
| @@ -135,13 +145,14 @@ trait PullRequestService { | ||||
|       ) | ||||
|  | ||||
|       // record activity | ||||
|       recordPullRequestActivity( | ||||
|       val openPullRequestInfo = OpenPullRequestInfo( | ||||
|         originRepository.owner, | ||||
|         originRepository.name, | ||||
|         loginAccount.userName, | ||||
|         issueId, | ||||
|         baseIssue.title | ||||
|       ) | ||||
|       recordActivity(openPullRequestInfo) | ||||
|  | ||||
|       // call web hook | ||||
|       callPullRequestWebHook("opened", originRepository, issueId, loginAccount, settings) | ||||
| @@ -291,6 +302,76 @@ trait PullRequestService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def updatePullRequestsByApi( | ||||
|     repository: RepositoryInfo, | ||||
|     issueId: Int, | ||||
|     loginAccount: Account, | ||||
|     settings: SystemSettings, | ||||
|     title: Option[String], | ||||
|     body: Option[String], | ||||
|     state: Option[String], | ||||
|     base: Option[String] | ||||
|   )( | ||||
|     implicit s: Session, | ||||
|     c: JsonFormat.Context | ||||
|   ): Unit = { | ||||
|     getPullRequest(repository.owner, repository.name, issueId).foreach { | ||||
|       case (issue, pr) => | ||||
|         if (Repositories.filter(_.byRepository(pr.userName, pr.repositoryName)).exists.run) { | ||||
|           // Update base branch | ||||
|           base.foreach { _base => | ||||
|             if (pr.branch != _base) { | ||||
|               Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|                 getBranches(git, repository.repository.defaultBranch, origin = true) | ||||
|                   .find(_.name == _base) | ||||
|                   .foreach(br => updateBaseBranch(repository.owner, repository.name, issueId, br.name, br.commitId)) | ||||
|               } | ||||
|               createComment( | ||||
|                 repository.owner, | ||||
|                 repository.name, | ||||
|                 loginAccount.userName, | ||||
|                 issue.issueId, | ||||
|                 pr.branch + "\r\n" + _base, | ||||
|                 "change_base_branch" | ||||
|               ) | ||||
|             } | ||||
|           } | ||||
|           // Update title and content | ||||
|           title.foreach { _title => | ||||
|             updateIssue(repository.owner, repository.name, issueId, _title, body) | ||||
|             if (issue.title != _title) { | ||||
|               createComment( | ||||
|                 repository.owner, | ||||
|                 repository.name, | ||||
|                 loginAccount.userName, | ||||
|                 issue.issueId, | ||||
|                 issue.title + "\r\n" + _title, | ||||
|                 "change_title" | ||||
|               ) | ||||
|             } | ||||
|           } | ||||
|           // Update state | ||||
|           val action = (state, issue.closed) match { | ||||
|             case (Some("open"), true) => | ||||
|               updateClosed(repository.owner, repository.name, issueId, closed = false) | ||||
|               "reopened" | ||||
|             case (Some("closed"), false) => | ||||
|               updateClosed(repository.owner, repository.name, issueId, closed = true) | ||||
|               "closed" | ||||
|             case _ => "edited" | ||||
|           } | ||||
|           // Call web hook | ||||
|           callPullRequestWebHookByRequestBranch( | ||||
|             action, | ||||
|             getRepository(repository.owner, repository.name).get, | ||||
|             pr.requestBranch, | ||||
|             loginAccount, | ||||
|             settings | ||||
|           ) | ||||
|         } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getPullRequestByRequestCommit( | ||||
|     userName: String, | ||||
|     repositoryName: String, | ||||
| @@ -530,7 +611,7 @@ object PullRequestService { | ||||
|  | ||||
|   case class MergeStatus( | ||||
|     conflictMessage: Option[String], | ||||
|     commitStatues: List[CommitStatus], | ||||
|     commitStatuses: List[CommitStatus], | ||||
|     branchProtection: ProtectedBranchService.ProtectedBranchInfo, | ||||
|     branchIsOutOfDate: Boolean, | ||||
|     hasUpdatePermission: Boolean, | ||||
| @@ -541,7 +622,7 @@ object PullRequestService { | ||||
|  | ||||
|     val hasConflict = conflictMessage.isDefined | ||||
|     val statuses: List[CommitStatus] = | ||||
|       commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet) | ||||
|       commitStatuses ++ (branchProtection.contexts.toSet -- commitStatuses.map(_.context).toSet) | ||||
|         .map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _)) | ||||
|     val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists( | ||||
|       context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS) | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| package gitbucket.core.service | ||||
| import gitbucket.core.api.JsonFormat | ||||
| import gitbucket.core.model.{Account, WebHook} | ||||
| import gitbucket.core.model.Profile._ | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo} | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.SystemSettingsService.SystemSettings | ||||
| import gitbucket.core.service.WebHookService.WebHookPushPayload | ||||
| @@ -17,7 +17,12 @@ import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack} | ||||
| import scala.util.Using | ||||
|  | ||||
| trait RepositoryCommitFileService { | ||||
|   self: AccountService with ActivityService with IssuesService with PullRequestService with WebHookPullRequestService => | ||||
|   self: AccountService | ||||
|     with ActivityService | ||||
|     with IssuesService | ||||
|     with PullRequestService | ||||
|     with WebHookPullRequestService | ||||
|     with RepositoryService => | ||||
|   import RepositoryCommitFileService._ | ||||
|  | ||||
|   def commitFiles( | ||||
| @@ -149,9 +154,9 @@ trait RepositoryCommitFileService { | ||||
|         val receivePack = new ReceivePack(git.getRepository) | ||||
|         val receiveCommand = new ReceiveCommand(headTip, commitId, headName) | ||||
|  | ||||
|         // call post commit hook | ||||
|         // call pre-commit hook | ||||
|         val error = PluginRegistry().getReceiveHooks.flatMap { hook => | ||||
|           hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName) | ||||
|           hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false) | ||||
|         }.headOption | ||||
|  | ||||
|         error match { | ||||
| @@ -175,8 +180,10 @@ trait RepositoryCommitFileService { | ||||
|             updatePullRequests(repository.owner, repository.name, branch, loginAccount, "synchronize", settings) | ||||
|  | ||||
|             // record activity | ||||
|             updateLastActivityDate(repository.owner, repository.name) | ||||
|             val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) | ||||
|             recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo)) | ||||
|             val pushInfo = PushInfo(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo)) | ||||
|             recordActivity(pushInfo) | ||||
|  | ||||
|             // create issue comment by commit message | ||||
|             createIssueComment(repository.owner, repository.name, commitInfo) | ||||
| @@ -186,15 +193,23 @@ trait RepositoryCommitFileService { | ||||
|               closeIssuesFromMessage(message, committerName, repository.owner, repository.name).foreach { issueId => | ||||
|                 getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||
|                   callIssuesWebHook("closed", repository, issue, loginAccount, settings) | ||||
|                   val closeIssueInfo = CloseIssueInfo( | ||||
|                     repository.owner, | ||||
|                     repository.name, | ||||
|                     loginAccount.userName, | ||||
|                     issue.issueId, | ||||
|                     issue.title | ||||
|                   ) | ||||
|                   recordActivity(closeIssueInfo) | ||||
|                   PluginRegistry().getIssueHooks | ||||
|                     .foreach(_.closedByCommitComment(issue, repository, message, loginAccount)) | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             // call post commit hook | ||||
|             // call post-commit hook | ||||
|             PluginRegistry().getReceiveHooks.foreach { hook => | ||||
|               hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName) | ||||
|               hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false) | ||||
|             } | ||||
|  | ||||
|             val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId)) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import java.nio.file.Files | ||||
| import java.util.concurrent.ConcurrentHashMap | ||||
|  | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.activity.{CreateRepositoryInfo, ForkInfo} | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil} | ||||
| import gitbucket.core.model.{Account, Role} | ||||
| @@ -160,7 +161,7 @@ trait RepositoryCreationService { | ||||
|         createWikiRepository(loginAccount, owner, name) | ||||
|  | ||||
|         // Record activity | ||||
|         recordCreateRepositoryActivity(owner, name, loginUserName) | ||||
|         recordActivity(CreateRepositoryInfo(owner, name, loginUserName)) | ||||
|  | ||||
|         // Call hooks | ||||
|         PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name)) | ||||
| @@ -180,12 +181,14 @@ trait RepositoryCreationService { | ||||
|         Database() withTransaction { implicit session => | ||||
|           val originUserName = repository.repository.originUserName.getOrElse(repository.owner) | ||||
|           val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name) | ||||
|           val originDefaultBranchName = repository.repository.defaultBranch | ||||
|  | ||||
|           insertRepository( | ||||
|             repositoryName = repository.name, | ||||
|             userName = accountName, | ||||
|             description = repository.repository.description, | ||||
|             isPrivate = repository.repository.isPrivate, | ||||
|             defaultBranch = originDefaultBranchName, | ||||
|             originRepositoryName = Some(originRepositoryName), | ||||
|             originUserName = Some(originUserName), | ||||
|             parentRepositoryName = Some(repository.name), | ||||
| @@ -227,7 +230,8 @@ trait RepositoryCreationService { | ||||
|           } | ||||
|  | ||||
|           // Record activity | ||||
|           recordForkActivity(repository.owner, repository.name, loginUserName, accountName) | ||||
|           val forkInfo = ForkInfo(repository.owner, repository.name, loginUserName, accountName) | ||||
|           recordActivity(forkInfo) | ||||
|  | ||||
|           // Call hooks | ||||
|           PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name)) | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package gitbucket.core.service | ||||
|  | ||||
| import gitbucket.core.api.JsonFormat | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.util._ | ||||
| import gitbucket.core.util.SyntaxSugars._ | ||||
| @@ -9,14 +8,11 @@ import gitbucket.core.model.Profile._ | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.Profile.dateColumnType | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.WebHookService.WebHookPushPayload | ||||
| import gitbucket.core.util.Directory.{getRepositoryDir, getRepositoryFilesDir, getTemporaryDir, getWikiRepositoryDir} | ||||
| import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo} | ||||
| import gitbucket.core.util.JGitUtil.FileInfo | ||||
| import org.apache.commons.io.FileUtils | ||||
| import org.eclipse.jgit.api.Git | ||||
| import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder} | ||||
| import org.eclipse.jgit.lib.{Repository => _, _} | ||||
| import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack} | ||||
| import scala.util.Using | ||||
|  | ||||
| trait RepositoryService { | ||||
| @@ -38,6 +34,7 @@ trait RepositoryService { | ||||
|     userName: String, | ||||
|     description: Option[String], | ||||
|     isPrivate: Boolean, | ||||
|     defaultBranch: String = "master", | ||||
|     originRepositoryName: Option[String] = None, | ||||
|     originUserName: Option[String] = None, | ||||
|     parentRepositoryName: Option[String] = None, | ||||
| @@ -49,7 +46,7 @@ trait RepositoryService { | ||||
|         repositoryName = repositoryName, | ||||
|         isPrivate = isPrivate, | ||||
|         description = description, | ||||
|         defaultBranch = "master", | ||||
|         defaultBranch = defaultBranch, | ||||
|         registeredDate = currentDate, | ||||
|         updatedDate = currentDate, | ||||
|         lastActivityDate = currentDate, | ||||
| @@ -119,15 +116,6 @@ trait RepositoryService { | ||||
|             } | ||||
|             .update(newUserName, newRepositoryName) | ||||
|  | ||||
|           // Updates activity fk before deleting repository because activity is sorted by activityId | ||||
|           // and it can't be changed by deleting-and-inserting record. | ||||
|           Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity => | ||||
|             Activities | ||||
|               .filter(_.activityId === activity.activityId.bind) | ||||
|               .map(x => (x.userName, x.repositoryName)) | ||||
|               .update(newUserName, newRepositoryName) | ||||
|           } | ||||
|  | ||||
|           deleteRepositoryOnModel(oldUserName, oldRepositoryName) | ||||
|  | ||||
|           RepositoryWebHooks.insertAll( | ||||
| @@ -213,50 +201,6 @@ trait RepositoryService { | ||||
|             collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)): _* | ||||
|           ) | ||||
|  | ||||
|           // Update activity messages | ||||
|           Activities | ||||
|             .filter { t => | ||||
|               (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || | ||||
|               (t.message like s"%:${oldUserName}/${oldRepositoryName}#%") || | ||||
|               (t.message like s"%:${oldUserName}/${oldRepositoryName}@%") | ||||
|             } | ||||
|             .map { t => | ||||
|               t.activityId -> t.message | ||||
|             } | ||||
|             .list | ||||
|             .foreach { | ||||
|               case (activityId, message) => | ||||
|                 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}#" | ||||
|                       ) | ||||
|                       .replace( | ||||
|                         s"[tag:${oldUserName}/${oldRepositoryName}#", | ||||
|                         s"[tag:${newUserName}/${newRepositoryName}#" | ||||
|                       ) | ||||
|                       .replace( | ||||
|                         s"[pullreq:${oldUserName}/${oldRepositoryName}#", | ||||
|                         s"[pullreq:${newUserName}/${newRepositoryName}#" | ||||
|                       ) | ||||
|                       .replace( | ||||
|                         s"[issue:${oldUserName}/${oldRepositoryName}#", | ||||
|                         s"[issue:${newUserName}/${newRepositoryName}#" | ||||
|                       ) | ||||
|                       .replace( | ||||
|                         s"[commit:${oldUserName}/${oldRepositoryName}@", | ||||
|                         s"[commit:${newUserName}/${newRepositoryName}@" | ||||
|                       ) | ||||
|                   ) | ||||
|             } | ||||
|           // Move git repository | ||||
|           defining(getRepositoryDir(oldUserName, oldRepositoryName)) { dir => | ||||
|             if (dir.isDirectory) { | ||||
| @@ -304,7 +248,7 @@ trait RepositoryService { | ||||
|   } | ||||
|  | ||||
|   private def deleteRepositoryOnModel(userName: String, repositoryName: String)(implicit s: Session): Unit = { | ||||
|     Activities.filter(_.byRepository(userName, repositoryName)).delete | ||||
| //    Activities.filter(_.byRepository(userName, repositoryName)).delete | ||||
|     Collaborators.filter(_.byRepository(userName, repositoryName)).delete | ||||
|     CommitComments.filter(_.byRepository(userName, repositoryName)).delete | ||||
|     IssueLabels.filter(_.byRepository(userName, repositoryName)).delete | ||||
| @@ -399,6 +343,10 @@ trait RepositoryService { | ||||
|             repository.originUserName.getOrElse(repository.userName), | ||||
|             repository.originRepositoryName.getOrElse(repository.repositoryName) | ||||
|           ), | ||||
|           getOpenMilestones( | ||||
|             repository.originUserName.getOrElse(repository.userName), | ||||
|             repository.originRepositoryName.getOrElse(repository.repositoryName) | ||||
|           ), | ||||
|           getRepositoryManagers(repository.userName, repository.repositoryName) | ||||
|         ) | ||||
|     } | ||||
| @@ -432,6 +380,21 @@ trait RepositoryService { | ||||
|       .list | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns the all public repositories. | ||||
|    * | ||||
|    * @return the repository information list | ||||
|    */ | ||||
|   def getPublicRepositories(withoutPhysicalInfo: Boolean = false)(implicit s: Session): List[RepositoryInfo] = { | ||||
|     Repositories | ||||
|       .filter { t1 => | ||||
|         t1.isPrivate === false.bind | ||||
|       } | ||||
|       .sortBy(_.lastActivityDate desc) | ||||
|       .list | ||||
|       .map(createRepositoryInfo(_, withoutPhysicalInfo)) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Returns the list of repositories which are owned by the specified user. | ||||
|    * This list includes group repositories if the specified user is a member of the group. | ||||
| @@ -482,7 +445,7 @@ trait RepositoryService { | ||||
|    * @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user) | ||||
|    * @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count, | ||||
|    *                            branches and tags | ||||
|    * @param limit if true then result will include only repositories associated with the login account. | ||||
|    * @param limit if true then result will include only repositories owned by the login account. otherwise, result will be all visible repositories. | ||||
|    * @return the repository information which is sorted in descending order of lastActivityDate. | ||||
|    */ | ||||
|   def getVisibleRepositories( | ||||
| @@ -754,6 +717,14 @@ trait RepositoryService { | ||||
|       (t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind) | ||||
|     }.length).first | ||||
|  | ||||
|   private def getOpenMilestones(userName: String, repositoryName: String)(implicit s: Session): Int = | ||||
|     Query( | ||||
|       Milestones | ||||
|         .filter(_.byRepository(userName, repositoryName)) | ||||
|         .filter(_.closedDate.isEmpty) | ||||
|         .length | ||||
|     ).first | ||||
|  | ||||
|   def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[Repository] = | ||||
|     Repositories | ||||
|       .filter { t => | ||||
| @@ -785,9 +756,10 @@ trait RepositoryService { | ||||
|  | ||||
|     // Get template file from project root. When didn't find, will lookup default folder. | ||||
|     Using.resource(Git.open(Directory.getRepositoryDir(repository.owner, repository.name))) { git => | ||||
|       choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".")) | ||||
|       // maxFiles = 1 means not get commit info because the only objectId and filename are necessary here | ||||
|       choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".", maxFiles = 1)) | ||||
|         .orElse { | ||||
|           choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".gitbucket")) | ||||
|           choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".gitbucket", maxFiles = 1)) | ||||
|         } | ||||
|         .map { file => | ||||
|           JGitUtil.getContentFromId(git, file.id, true).collect { | ||||
| @@ -806,6 +778,7 @@ object RepositoryService { | ||||
|     issueCount: Int, | ||||
|     pullCount: Int, | ||||
|     forkedCount: Int, | ||||
|     milestoneCount: Int, | ||||
|     branchList: Seq[String], | ||||
|     tags: Seq[JGitUtil.TagInfo], | ||||
|     managers: Seq[String] | ||||
| @@ -820,15 +793,27 @@ object RepositoryService { | ||||
|       issueCount: Int, | ||||
|       pullCount: Int, | ||||
|       forkedCount: Int, | ||||
|       milestoneCount: Int, | ||||
|       managers: Seq[String] | ||||
|     ) = | ||||
|       this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, repo.branchList, repo.tags, managers) | ||||
|       this( | ||||
|         repo.owner, | ||||
|         repo.name, | ||||
|         model, | ||||
|         issueCount, | ||||
|         pullCount, | ||||
|         forkedCount, | ||||
|         milestoneCount, | ||||
|         repo.branchList, | ||||
|         repo.tags, | ||||
|         managers | ||||
|       ) | ||||
|  | ||||
|     /** | ||||
|      * Creates instance without issue and  pull request count. | ||||
|      * Creates instance without issue, pull request, and milestone count. | ||||
|      */ | ||||
|     def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) = | ||||
|       this(repo.owner, repo.name, model, 0, 0, forkedCount, repo.branchList, repo.tags, managers) | ||||
|       this(repo.owner, repo.name, model, 0, 0, forkedCount, 0, repo.branchList, repo.tags, managers) | ||||
|  | ||||
|     def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name) | ||||
|     def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name) | ||||
| @@ -854,4 +839,9 @@ object RepositoryService { | ||||
|     } else None | ||||
|   def openRepoUrl(openUrl: String)(implicit context: Context): String = | ||||
|     s"github-${context.platform}://openRepo/${openUrl}" | ||||
|  | ||||
|   def readmeFiles: Seq[String] = | ||||
|     PluginRegistry().renderableExtensions.map { extension => | ||||
|       s"readme.${extension}" | ||||
|     } ++ Seq("readme.txt", "readme") | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| package gitbucket.core.service | ||||
|  | ||||
| import gitbucket.core.model.{Session, Issue, Account} | ||||
| import gitbucket.core.model.{Account, Issue, Repository, Session} | ||||
| import gitbucket.core.util.Implicits | ||||
| import gitbucket.core.controller.Context | ||||
| import Implicits.request2Session | ||||
| import gitbucket.core.model.Profile.{Accounts, Repositories} | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
|  | ||||
| /** | ||||
|  * This service is used for a view helper mainly. | ||||
| @@ -23,21 +25,41 @@ trait RequestCache | ||||
|   private implicit def context2Session(implicit context: Context): Session = | ||||
|     request2Session(context.request) | ||||
|  | ||||
|   def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: Context): Option[Issue] = { | ||||
|   def getIssueFromCache(userName: String, repositoryName: String, issueId: String)( | ||||
|     implicit context: Context | ||||
|   ): Option[Issue] = { | ||||
|     context.cache(s"issue.${userName}/${repositoryName}#${issueId}") { | ||||
|       super.getIssue(userName, repositoryName, issueId) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = { | ||||
|   def getAccountByUserNameFromCache(userName: String)(implicit context: Context): Option[Account] = { | ||||
|     context.cache(s"account.${userName}") { | ||||
|       super.getAccountByUserName(userName) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = { | ||||
|   def getAccountByMailAddressFromCache(mailAddress: String)(implicit context: Context): Option[Account] = { | ||||
|     context.cache(s"account.${mailAddress}") { | ||||
|       super.getAccountByMailAddress(mailAddress) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def getRepositoryInfoFromCache(userName: String, repositoryName: String)( | ||||
|     implicit context: Context | ||||
|   ): Option[Repository] = { | ||||
|     context.cache(s"repository.${userName}/${repositoryName}") { | ||||
|       Repositories | ||||
|         .join(Accounts) | ||||
|         .on(_.userName === _.userName) | ||||
|         .filter { | ||||
|           case (t1, t2) => | ||||
|             t1.byRepository(userName, repositoryName) && t2.removed === false.bind | ||||
|         } | ||||
|         .map { | ||||
|           case (t1, t2) => t1 | ||||
|         } | ||||
|         .firstOption | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,6 @@ trait SystemSettingsService { | ||||
|       props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString) | ||||
|       props.setProperty(Gravatar, settings.gravatar.toString) | ||||
|       props.setProperty(Notification, settings.notification.toString) | ||||
|       settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString)) | ||||
|       props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString) | ||||
|       props.setProperty(SshEnabled, settings.ssh.enabled.toString) | ||||
|       settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim)) | ||||
| @@ -83,6 +82,7 @@ trait SystemSettingsService { | ||||
|       props.setProperty(UploadTimeout, settings.upload.timeout.toString) | ||||
|       props.setProperty(UploadLargeMaxFileSize, settings.upload.largeMaxFileSize.toString) | ||||
|       props.setProperty(UploadLargeTimeout, settings.upload.largeTimeout.toString) | ||||
|       props.setProperty(RepositoryViewerMaxFiles, settings.repositoryViewer.maxFiles.toString) | ||||
|  | ||||
|       Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out => | ||||
|         props.store(out, null) | ||||
| @@ -112,7 +112,6 @@ trait SystemSettingsService { | ||||
|         ), | ||||
|         getValue(props, Gravatar, false), | ||||
|         getValue(props, Notification, false), | ||||
|         getOptionValue[Int](props, ActivityLogLimit, None), | ||||
|         getValue(props, LimitVisibleRepositories, false), | ||||
|         Ssh( | ||||
|           getValue(props, SshEnabled, false), | ||||
| @@ -179,6 +178,9 @@ trait SystemSettingsService { | ||||
|           getValue(props, UploadTimeout, 3 * 10000), | ||||
|           getValue(props, UploadLargeMaxFileSize, 3 * 1024 * 1024), | ||||
|           getValue(props, UploadLargeTimeout, 3 * 10000) | ||||
|         ), | ||||
|         RepositoryViewerSettings( | ||||
|           getValue(props, RepositoryViewerMaxFiles, 0) | ||||
|         ) | ||||
|       ) | ||||
|     } | ||||
| @@ -200,7 +202,6 @@ object SystemSettingsService { | ||||
|     repositoryOperation: RepositoryOperation, | ||||
|     gravatar: Boolean, | ||||
|     notification: Boolean, | ||||
|     activityLogLimit: Option[Int], | ||||
|     limitVisibleRepositories: Boolean, | ||||
|     ssh: Ssh, | ||||
|     useSMTP: Boolean, | ||||
| @@ -213,7 +214,8 @@ object SystemSettingsService { | ||||
|     userDefinedCss: Option[String], | ||||
|     showMailAddress: Boolean, | ||||
|     webHook: WebHook, | ||||
|     upload: Upload | ||||
|     upload: Upload, | ||||
|     repositoryViewer: RepositoryViewerSettings | ||||
|   ) { | ||||
|  | ||||
|     def baseUrl(request: HttpServletRequest): String = | ||||
| @@ -303,6 +305,8 @@ object SystemSettingsService { | ||||
|  | ||||
|   case class Upload(maxFileSize: Long, timeout: Long, largeMaxFileSize: Long, largeTimeout: Long) | ||||
|  | ||||
|   case class RepositoryViewerSettings(maxFiles: Int) | ||||
|  | ||||
|   val DefaultSshPort = 29418 | ||||
|   val DefaultSmtpPort = 25 | ||||
|   val DefaultLdapPort = 389 | ||||
| @@ -360,6 +364,7 @@ object SystemSettingsService { | ||||
|   private val UploadTimeout = "upload.timeout" | ||||
|   private val UploadLargeMaxFileSize = "upload.largeMaxFileSize" | ||||
|   private val UploadLargeTimeout = "upload.largeTimeout" | ||||
|   private val RepositoryViewerMaxFiles = "repository_viewer_max_files" | ||||
|  | ||||
|   private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = { | ||||
|     getConfigValue(key).getOrElse { | ||||
|   | ||||
| @@ -83,7 +83,7 @@ trait WebHookService { | ||||
|     implicit s: Session | ||||
|   ): Option[(RepositoryWebHook, Set[WebHook.Event])] = | ||||
|     RepositoryWebHooks | ||||
|       .filter(_.byPrimaryKey(owner, repository, url)) | ||||
|       .filter(_.byRepositoryUrl(owner, repository, url)) | ||||
|       .join(RepositoryWebHookEvents) | ||||
|       .on { (w, t) => | ||||
|         t.byRepositoryWebHook(w) | ||||
| @@ -95,6 +95,24 @@ trait WebHookService { | ||||
|       .mapValues(_.map(_._2).toSet) | ||||
|       .headOption | ||||
|  | ||||
|   /** get All WebHook informations of repository */ | ||||
|   def getWebHookById(id: Int)( | ||||
|     implicit s: Session | ||||
|   ): Option[(RepositoryWebHook, Set[WebHook.Event])] = | ||||
|     RepositoryWebHooks | ||||
|       .filter(_.byId(id)) | ||||
|       .join(RepositoryWebHookEvents) | ||||
|       .on { (w, t) => | ||||
|         t.byRepositoryWebHook(w) | ||||
|       } | ||||
|       .map { case (w, t) => w -> t.event } | ||||
|       .list | ||||
|       .groupBy(_._1) | ||||
|       .view | ||||
|       .mapValues(_.map(_._2).toSet) | ||||
|       .toList | ||||
|       .headOption | ||||
|  | ||||
|   def addWebHook( | ||||
|     owner: String, | ||||
|     repository: String, | ||||
| @@ -103,7 +121,13 @@ trait WebHookService { | ||||
|     ctype: WebHookContentType, | ||||
|     token: Option[String] | ||||
|   )(implicit s: Session): Unit = { | ||||
|     RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token) | ||||
|     RepositoryWebHooks insert RepositoryWebHook( | ||||
|       userName = owner, | ||||
|       repositoryName = repository, | ||||
|       url = url, | ||||
|       ctype = ctype, | ||||
|       token = token | ||||
|     ) | ||||
|     events.map { event: WebHook.Event => | ||||
|       RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event) | ||||
|     } | ||||
| @@ -118,7 +142,7 @@ trait WebHookService { | ||||
|     token: Option[String] | ||||
|   )(implicit s: Session): Unit = { | ||||
|     RepositoryWebHooks | ||||
|       .filter(_.byPrimaryKey(owner, repository, url)) | ||||
|       .filter(_.byRepositoryUrl(owner, repository, url)) | ||||
|       .map(w => (w.ctype, w.token)) | ||||
|       .update((ctype, token)) | ||||
|     RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete | ||||
| @@ -127,8 +151,30 @@ trait WebHookService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def updateWebHookByApi( | ||||
|     id: Int, | ||||
|     owner: String, | ||||
|     repository: String, | ||||
|     url: String, | ||||
|     events: Set[WebHook.Event], | ||||
|     ctype: WebHookContentType, | ||||
|     token: Option[String] | ||||
|   )(implicit s: Session): Unit = { | ||||
|     RepositoryWebHooks | ||||
|       .filter(_.byId(id)) | ||||
|       .map(w => (w.url, w.ctype, w.token)) | ||||
|       .update((url, ctype, token)) | ||||
|     RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete | ||||
|     events.map { event: WebHook.Event => | ||||
|       RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def deleteWebHook(owner: String, repository: String, url: String)(implicit s: Session): Unit = | ||||
|     RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete | ||||
|     RepositoryWebHooks.filter(_.byRepositoryUrl(owner, repository, url)).delete | ||||
|  | ||||
|   def deleteWebHookById(id: Int)(implicit s: Session): Unit = | ||||
|     RepositoryWebHooks.filter(_.byId(id)).delete | ||||
|  | ||||
|   /** get All AccountWebHook informations of user */ | ||||
|   def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] = | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import java.util | ||||
| import java.util.Date | ||||
|  | ||||
| import scala.util.Using | ||||
|  | ||||
| import gitbucket.core.api | ||||
| import gitbucket.core.model.WebHook | ||||
| import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry} | ||||
| @@ -16,6 +15,19 @@ import gitbucket.core.util.SyntaxSugars._ | ||||
| import gitbucket.core.util.Implicits._ | ||||
| import gitbucket.core.util._ | ||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | ||||
| import gitbucket.core.model.activity.{ | ||||
|   BaseActivityInfo, | ||||
|   CloseIssueInfo, | ||||
|   CreateBranchInfo, | ||||
|   CreateTagInfo, | ||||
|   CreateWikiPageInfo, | ||||
|   DeleteBranchInfo, | ||||
|   DeleteTagInfo, | ||||
|   DeleteWikiInfo, | ||||
|   EditWikiPageInfo, | ||||
|   PushInfo | ||||
| } | ||||
| import gitbucket.core.util.JGitUtil.CommitInfo | ||||
| // Imported names have higher precedence than names, defined in other files. | ||||
| // If Database is not bound by explicit import, then "Database" refers to the Database introduced by the wildcard import above. | ||||
| import gitbucket.core.servlet.Database | ||||
| @@ -240,7 +252,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|     with WebHookPullRequestService | ||||
|     with WebHookPullRequestReviewCommentService | ||||
|     with CommitsService | ||||
|     with SystemSettingsService { | ||||
|     with SystemSettingsService | ||||
|     with RequestCache { | ||||
|  | ||||
|   private val logger = LoggerFactory.getLogger(classOf[CommitLogHook]) | ||||
|   private var existIds: Seq[String] = Nil | ||||
| @@ -251,7 +264,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|         commands.asScala.foreach { command => | ||||
|           // call pre-commit hook | ||||
|           PluginRegistry().getReceiveHooks | ||||
|             .flatMap(_.preReceive(owner, repository, receivePack, command, pusher)) | ||||
|             .flatMap(_.preReceive(owner, repository, receivePack, command, pusher, false)) | ||||
|             .headOption | ||||
|             .foreach { error => | ||||
|               command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error) | ||||
| @@ -305,8 +318,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|  | ||||
|             // Retrieve all issue count in the repository | ||||
|             val issueCount = | ||||
|               countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) + | ||||
|                 countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository) | ||||
|               countIssue(IssueSearchCondition(state = "open"), IssueSearchOption.Issues, owner -> repository) + | ||||
|                 countIssue(IssueSearchCondition(state = "closed"), IssueSearchOption.Issues, owner -> repository) | ||||
|  | ||||
|             // Extract new commit and apply issue comment | ||||
|             val defaultBranch = repositoryInfo.repository.defaultBranch | ||||
| @@ -321,6 +334,9 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|                       closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository).foreach { issueId => | ||||
|                         getIssue(owner, repository, issueId.toString).foreach { issue => | ||||
|                           callIssuesWebHook("closed", repositoryInfo, issue, pusherAccount, settings) | ||||
|                           val closeIssueInfo = | ||||
|                             CloseIssueInfo(owner, repository, pusherAccount.userName, issue.issueId, issue.title) | ||||
|                           recordActivity(closeIssueInfo) | ||||
|                           PluginRegistry().getIssueHooks | ||||
|                             .foreach(_.closedByCommitComment(issue, repositoryInfo, commit.fullMessage, pusherAccount)) | ||||
|                         } | ||||
| @@ -348,17 +364,25 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|             // record activity | ||||
|             if (refName(1) == "heads") { | ||||
|               command.getType match { | ||||
|                 case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName) | ||||
|                 case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits) | ||||
|                 case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName) | ||||
|                 case ReceiveCommand.Type.CREATE => | ||||
|                   val createBranchInfo = CreateBranchInfo(owner, repository, pusher, branchName) | ||||
|                   recordActivity(createBranchInfo) | ||||
|                 case ReceiveCommand.Type.UPDATE => | ||||
|                   val pushInfo = PushInfo(owner, repository, pusher, branchName, newCommits) | ||||
|                   recordActivity(pushInfo) | ||||
|                 case ReceiveCommand.Type.DELETE => | ||||
|                   val deleteBranchInfo = DeleteBranchInfo(owner, repository, pusher, branchName) | ||||
|                   recordActivity(deleteBranchInfo) | ||||
|                 case _ => | ||||
|               } | ||||
|             } else if (refName(1) == "tags") { | ||||
|               command.getType match { | ||||
|                 case ReceiveCommand.Type.CREATE => | ||||
|                   recordCreateTagActivity(owner, repository, pusher, branchName, newCommits) | ||||
|                   val createTagInfo = CreateTagInfo(owner, repository, pusher, branchName) | ||||
|                   recordActivity(createTagInfo) | ||||
|                 case ReceiveCommand.Type.DELETE => | ||||
|                   recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits) | ||||
|                   val deleteTagInfo = DeleteTagInfo(owner, repository, pusher, branchName) | ||||
|                   recordActivity(deleteTagInfo) | ||||
|                 case _ => | ||||
|               } | ||||
|             } | ||||
| @@ -411,7 +435,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|             } | ||||
|  | ||||
|             // call post-commit hook | ||||
|             PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher)) | ||||
|             PluginRegistry().getReceiveHooks | ||||
|               .foreach(_.postReceive(owner, repository, receivePack, command, pusher, false)) | ||||
|           } | ||||
|         } | ||||
|         // update repository last modified time. | ||||
| @@ -432,7 +457,9 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|     with WebHookService | ||||
|     with AccountService | ||||
|     with RepositoryService | ||||
|     with SystemSettingsService { | ||||
|     with ActivityService | ||||
|     with SystemSettingsService | ||||
|     with RequestCache { | ||||
|  | ||||
|   private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook]) | ||||
|  | ||||
| @@ -460,18 +487,22 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|                   val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false) | ||||
|                   diffs.collect { | ||||
|                     case diff if diff.newPath.toLowerCase.endsWith(".md") => | ||||
|                       val action = if (diff.changeType == ChangeType.ADD) "created" else "edited" | ||||
|                       val action = mapToAction(diff.changeType) | ||||
|                       val fileName = diff.newPath | ||||
|                       updateLastActivityDate(owner, repository) | ||||
|                       buildWikiRecord(action, owner, repository, commit, fileName).foreach(recordActivity) | ||||
|                       (action, fileName, commit.id) | ||||
|                   } | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               val pages = commits | ||||
|                 .groupBy { case (action, fileName, commitId) => fileName } | ||||
|                 .groupBy { case (_, fileName, _) => fileName } | ||||
|                 .map { | ||||
|                   case (fileName, commits) => | ||||
|                     (commits.head._1, fileName, commits.last._3) | ||||
|                     val (commitHeadAction, _, _) = commits.head | ||||
|                     val (_, _, commitLastId) = commits.last | ||||
|                     (commitHeadAction, fileName, commitLastId) | ||||
|                 } | ||||
|  | ||||
|               callWebHookOf(owner, repository, WebHook.Gollum, settings) { | ||||
| @@ -494,6 +525,32 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private[this] def mapToAction(changeType: ChangeType): String = changeType match { | ||||
|     case ChangeType.ADD | ChangeType.RENAME => "created" | ||||
|     case ChangeType.MODIFY                  => "edited" | ||||
|     case ChangeType.DELETE                  => "deleted" | ||||
|     case other => | ||||
|       logger.error(s"Unsupported Wiki action: $other") | ||||
|       "unsupported action" | ||||
|   } | ||||
|  | ||||
|   private[this] def buildWikiRecord( | ||||
|     action: String, | ||||
|     owner: String, | ||||
|     repo: String, | ||||
|     commit: CommitInfo, | ||||
|     fileName: String | ||||
|   ): Option[BaseActivityInfo] = { | ||||
|     val pageName = fileName.dropRight(".md".length) | ||||
|     action match { | ||||
|       case "created" => Some(CreateWikiPageInfo(owner, repo, commit.committerName, pageName)) | ||||
|       case "edited"  => Some(EditWikiPageInfo(owner, repo, commit.committerName, pageName, commit.id)) | ||||
|       case "deleted" => Some(DeleteWikiInfo(owner, repo, commit.committerName, pageName)) | ||||
|       case other => | ||||
|         logger.info(s"Attempted to build wiki record for unsupported action: $other") | ||||
|         None | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| object GitLfs { | ||||
|   | ||||
| @@ -2,11 +2,10 @@ package gitbucket.core.servlet | ||||
|  | ||||
| import java.io.{File, FileOutputStream} | ||||
|  | ||||
| import akka.event.Logging | ||||
| import com.typesafe.config.ConfigFactory | ||||
| import gitbucket.core.GitBucketCoreModule | ||||
| import gitbucket.core.plugin.PluginRegistry | ||||
| import gitbucket.core.service.{ActivityService, SystemSettingsService} | ||||
| import gitbucket.core.service.SystemSettingsService | ||||
| import gitbucket.core.util.DatabaseConfig | ||||
| import gitbucket.core.util.Directory._ | ||||
| import gitbucket.core.util.JDBCUtil._ | ||||
| @@ -21,8 +20,6 @@ import javax.servlet.{ServletContextEvent, ServletContextListener} | ||||
|  | ||||
| import org.apache.commons.io.{FileUtils, IOUtils} | ||||
| import org.slf4j.LoggerFactory | ||||
| import akka.actor.{Actor, ActorSystem, Props} | ||||
| import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension | ||||
|  | ||||
| import scala.jdk.CollectionConverters._ | ||||
| import scala.util.Using | ||||
| @@ -35,23 +32,23 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi | ||||
|  | ||||
|   private val logger = LoggerFactory.getLogger(classOf[InitializeListener]) | ||||
|  | ||||
|   // ActorSystem for Quartz scheduler | ||||
|   private val system = ActorSystem( | ||||
|     "job", | ||||
|     ConfigFactory.parseString(""" | ||||
|       |akka { | ||||
|       |  daemonic = on | ||||
|       |  coordinated-shutdown.run-by-jvm-shutdown-hook = off | ||||
|       |  quartz { | ||||
|       |    schedules { | ||||
|       |      Daily { | ||||
|       |        expression = "0 0 0 * * ?" | ||||
|       |      } | ||||
|       |    } | ||||
|       |  } | ||||
|       |} | ||||
|     """.stripMargin) | ||||
|   ) | ||||
| //  // ActorSystem for Quartz scheduler | ||||
| //  private val system = ActorSystem( | ||||
| //    "job", | ||||
| //    ConfigFactory.parseString(""" | ||||
| //      |akka { | ||||
| //      |  daemonic = on | ||||
| //      |  coordinated-shutdown.run-by-jvm-shutdown-hook = off | ||||
| //      |  quartz { | ||||
| //      |    schedules { | ||||
| //      |      Daily { | ||||
| //      |        expression = "0 0 0 * * ?" | ||||
| //      |      } | ||||
| //      |    } | ||||
| //      |  } | ||||
| //      |} | ||||
| //    """.stripMargin) | ||||
| //  ) | ||||
|  | ||||
|   override def contextInitialized(event: ServletContextEvent): Unit = { | ||||
|     val dataDir = event.getServletContext.getInitParameter("gitbucket.home") | ||||
| @@ -95,10 +92,10 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi | ||||
|       PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn) | ||||
|     } | ||||
|  | ||||
|     // Start Quartz scheduler | ||||
|     val scheduler = QuartzSchedulerExtension(system) | ||||
|  | ||||
|     scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity") | ||||
| //    // Start Quartz scheduler | ||||
| //    val scheduler = QuartzSchedulerExtension(system) | ||||
| // | ||||
| //    scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity") | ||||
|   } | ||||
|  | ||||
|   private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = { | ||||
| @@ -172,8 +169,8 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi | ||||
|   } | ||||
|  | ||||
|   override def contextDestroyed(event: ServletContextEvent): Unit = { | ||||
|     // Shutdown Quartz scheduler | ||||
|     system.terminate() | ||||
| //    // Shutdown Quartz scheduler | ||||
| //    system.terminate() | ||||
|     // Shutdown plugins | ||||
|     PluginRegistry.shutdown(event.getServletContext, loadSystemSettings()) | ||||
|     // Close datasource | ||||
| @@ -181,21 +178,3 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService { | ||||
|  | ||||
|   private val logger = Logging(context.system, this) | ||||
|  | ||||
|   def receive = { | ||||
|     case s: String => { | ||||
|       loadSystemSettings().activityLogLimit.foreach { limit => | ||||
|         if (limit > 0) { | ||||
|           Database() withTransaction { implicit session => | ||||
|             val rows = deleteOldActivities(limit) | ||||
|             logger.info(s"Deleted ${rows} activity logs") | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -29,6 +29,8 @@ object Directory { | ||||
|  | ||||
|   val GitBucketConf = new File(GitBucketHome, "gitbucket.conf") | ||||
|  | ||||
|   val ActivityLog = new File(GitBucketHome, "activity.log") | ||||
|  | ||||
|   val RepositoryHome = s"${GitBucketHome}/repositories" | ||||
|  | ||||
|   val DatabaseHome = s"${GitBucketHome}/data" | ||||
|   | ||||
| @@ -374,6 +374,7 @@ object JGitUtil { | ||||
|    * @param path the directory path (optional) | ||||
|    * @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional) | ||||
|    * @param commitCount the number of commit of this repository (optional). If this number is greater than threshold, the commit info is cached in memory. | ||||
|    * @param maxFiles don't fetch commit info if the number of files in the directory is bigger than this number. | ||||
|    * @return The list of files in the specified directory. If the number of files are greater than threshold, the returned file list won't include the commit info. | ||||
|    */ | ||||
|   def getFileList( | ||||
| @@ -381,7 +382,8 @@ object JGitUtil { | ||||
|     revision: String, | ||||
|     path: String = ".", | ||||
|     baseUrl: Option[String] = None, | ||||
|     commitCount: Int = 0 | ||||
|     commitCount: Int = 0, | ||||
|     maxFiles: Int = 100 | ||||
|   ): List[FileInfo] = { | ||||
|     Using.resource(new RevWalk(git.getRepository)) { revWalk => | ||||
|       val objectId = git.getRepository.resolve(revision) | ||||
| @@ -436,13 +438,13 @@ object JGitUtil { | ||||
|       ): List[(ObjectId, FileMode, String, String, Option[String], Option[RevCommit])] = { | ||||
|         fileList.map { | ||||
|           case (id, mode, name, path, opt) => | ||||
|             if (maxFiles > 0 && fileList.size >= maxFiles) { | ||||
|               // Don't attempt to get the last commit if the number of files is very large. | ||||
|             if (fileList.size >= 100) { | ||||
|               (id, mode, name, path, opt, None) | ||||
|             } else if (commitCount < 10000) { | ||||
|               val i = git | ||||
|               (id, mode, name, path, opt, Some(getCommit(path))) | ||||
|             } else { | ||||
|               // Use in-memory cache if the commit count is too big. | ||||
|               val cached = objectCommitCache.getEntry(id) | ||||
|               if (cached == null) { | ||||
|                 val commit = getCommit(path) | ||||
|   | ||||
| @@ -151,8 +151,13 @@ object StringUtil { | ||||
|    * @return the iterator of issue id | ||||
|    */ | ||||
|   def extractCloseId(message: String): Seq[String] = | ||||
|     "(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r | ||||
|     "#(\\d+)".r | ||||
|       .findAllIn( | ||||
|         "(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(,\\s?#(\\d+))*(?!\\w)".r | ||||
|           .findAllIn(message) | ||||
|           .toSeq | ||||
|           .mkString(",") | ||||
|       ) | ||||
|       .matchData | ||||
|       .map(_.group(1)) | ||||
|       .toSeq | ||||
| @@ -177,4 +182,11 @@ object StringUtil { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def cutTail(txt: String, limit: Int, suffix: String = ""): String = { | ||||
|     txt.length match { | ||||
|       case x if x > limit => txt.substring(0, limit).concat(suffix) | ||||
|       case _              => txt | ||||
|     } | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache => | ||||
|  | ||||
|     val src = if (mailAddress.isEmpty) { | ||||
|       // by user name | ||||
|       getAccountByUserName(userName).map { account => | ||||
|       getAccountByUserNameFromCache(userName).map { account => | ||||
|         if (account.image.isEmpty && context.settings.gravatar) { | ||||
|           s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" | ||||
|         } else { | ||||
| @@ -28,7 +28,7 @@ trait AvatarImageProvider { self: RequestCache => | ||||
|       } | ||||
|     } else { | ||||
|       // by mail address | ||||
|       getAccountByMailAddress(mailAddress).map { account => | ||||
|       getAccountByMailAddressFromCache(mailAddress).map { account => | ||||
|         if (account.image.isEmpty && context.settings.gravatar) { | ||||
|           s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g""" | ||||
|         } else { | ||||
| @@ -45,11 +45,11 @@ trait AvatarImageProvider { self: RequestCache => | ||||
|  | ||||
|     if (tooltip) { | ||||
|       Html( | ||||
|         s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>""" | ||||
|         s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}" alt="@${userName}" />""" | ||||
|       ) | ||||
|     } else { | ||||
|       Html( | ||||
|         s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" />""" | ||||
|         s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" alt="@${userName}" />""" | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ trait LinkConverter { self: RequestCache => | ||||
|     val userName = repository.repository.userName | ||||
|     val repositoryName = repository.repository.repositoryName | ||||
|  | ||||
|     getIssue(userName, repositoryName, issueId.toString) match { | ||||
|     getIssueFromCache(userName, repositoryName, issueId.toString) match { | ||||
|       case Some(issue) => | ||||
|         s"""<a href="${context.path}/${userName}/${repositoryName}/${if (issue.isPullRequest) "pull" else "issues"}/${issueId}"><strong>${StringUtil | ||||
|           .escapeHtml(title)}</strong> #${issueId}</a>""" | ||||
| @@ -43,7 +43,7 @@ trait LinkConverter { self: RequestCache => | ||||
|     escaped | ||||
|     // convert username/project@SHA to link | ||||
|       .replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r) { m => | ||||
|         getAccountByUserName(m.group(2)).map { _ => | ||||
|         getAccountByUserNameFromCache(m.group(2)).map { _ => | ||||
|           s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group( | ||||
|             3 | ||||
|           )}@${m.group(4).substring(0, 7)}</a></code>""" | ||||
| @@ -53,13 +53,15 @@ trait LinkConverter { self: RequestCache => | ||||
|       // convert username/project#Num to link | ||||
|       .replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { | ||||
|         m => | ||||
|           getIssue(m.group(2), m.group(3), m.group(4)) match { | ||||
|             case Some(issue) if (issue.isPullRequest) => | ||||
|               Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group( | ||||
|           getIssueFromCache(m.group(2), m.group(3), m.group(4)) match { | ||||
|             case Some(pull) if (pull.isPullRequest) => | ||||
|               Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m | ||||
|                 .group(4)}" title="${pull.title}">${m.group(2)}/${m.group( | ||||
|                 3 | ||||
|               )}#${m.group(4)}</a>""") | ||||
|             case Some(_) => | ||||
|               Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/issues/${m.group(4)}">${m.group(2)}/${m | ||||
|             case Some(issue) => | ||||
|               Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/issues/${m | ||||
|                 .group(4)}" title="${issue.title}">${m.group(2)}/${m | ||||
|                 .group(3)}#${m.group(4)}</a>""") | ||||
|             case None => | ||||
|               Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""") | ||||
| @@ -68,7 +70,7 @@ trait LinkConverter { self: RequestCache => | ||||
|  | ||||
|       // convert username@SHA to link | ||||
|       .replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r) { m => | ||||
|         getAccountByUserName(m.group(2)).map { _ => | ||||
|         getAccountByUserNameFromCache(m.group(2)).map { _ => | ||||
|           s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m | ||||
|             .group(3) | ||||
|             .substring(0, 7)}</a></code>""" | ||||
| @@ -77,7 +79,7 @@ trait LinkConverter { self: RequestCache => | ||||
|  | ||||
|       // convert username#Num to link | ||||
|       .replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { m => | ||||
|         getIssue(m.group(2), repository.name, m.group(3)) match { | ||||
|         getIssueFromCache(m.group(2), repository.name, m.group(3)) match { | ||||
|           case Some(issue) if (issue.isPullRequest) => | ||||
|             Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m | ||||
|               .group(3)}</a>""") | ||||
| @@ -92,12 +94,14 @@ trait LinkConverter { self: RequestCache => | ||||
|       // convert issue id to link | ||||
|       .replaceBy(("(?<=(^|\\W))(GH-|(?<!&)" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r) { m => | ||||
|         val prefix = if (m.group(2) == "issue:") "#" else m.group(2) | ||||
|         getIssue(repository.owner, repository.name, m.group(3)) match { | ||||
|           case Some(issue) if (issue.isPullRequest) => | ||||
|             Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m | ||||
|         getIssueFromCache(repository.owner, repository.name, m.group(3)) match { | ||||
|           case Some(pull) if (pull.isPullRequest) => | ||||
|             Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m | ||||
|               .group(3)}" title="${pull.title}">${prefix}${m | ||||
|               .group(3)}</a>""") | ||||
|           case Some(_) => | ||||
|             Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(3)}">${prefix}${m | ||||
|           case Some(issue) => | ||||
|             Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m | ||||
|               .group(3)}"  title="${issue.title}">${prefix}${m | ||||
|               .group(3)}</a>""") | ||||
|           case None => | ||||
|             Some(s"""${m.group(2)}${m.group(3)}""") | ||||
| @@ -106,7 +110,7 @@ trait LinkConverter { self: RequestCache => | ||||
|  | ||||
|       // convert @username to link | ||||
|       .replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r) { m => | ||||
|         getAccountByUserName(m.group(2)).map { _ => | ||||
|         getAccountByUserNameFromCache(m.group(2)).map { _ => | ||||
|           s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>""" | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package gitbucket.core.view | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.{Date, Locale, TimeZone} | ||||
|  | ||||
| import com.nimbusds.jose.util.JSONObjectUtils | ||||
| import gitbucket.core.controller.Context | ||||
| import gitbucket.core.model.CommitState | ||||
| import gitbucket.core.model.PullRequest | ||||
| @@ -187,16 +186,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache | ||||
|   def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html = | ||||
|     Html(decorateHtml(convertRefsLinks(value, repository), repository)) | ||||
|  | ||||
|   def cut(value: String, length: Int): String = | ||||
|     if (value.length > length) { | ||||
|       value.substring(0, length) + "..." | ||||
|     } else { | ||||
|       value | ||||
|     } | ||||
|  | ||||
|   import scala.util.matching.Regex._ | ||||
|   implicit class RegexReplaceString(private val s: String) extends AnyVal { | ||||
|     def replaceAll(pattern: String, replacer: (Match) => String): String = { | ||||
|     def replaceAll(pattern: String)(replacer: Match => String): String = { | ||||
|       pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$")) | ||||
|     } | ||||
|   } | ||||
| @@ -204,50 +196,66 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache | ||||
|   /** | ||||
|    * Convert link notations in the activity message. | ||||
|    */ | ||||
|   // format: off | ||||
|   def activityMessage(message: String)(implicit context: Context): Html = | ||||
|     Html( | ||||
|       message | ||||
|         .replaceAll( | ||||
|           "\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]", | ||||
|           s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>""" | ||||
|         ) | ||||
|         .replaceAll( | ||||
|           "\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]", | ||||
|           s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>""" | ||||
|         ) | ||||
|         .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""") | ||||
|         .replaceAll( | ||||
|           "\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", | ||||
|           (m: Match) => | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil | ||||
|               .escapeHtml( | ||||
|                 m.group(3) | ||||
|               )}</a>""" | ||||
|         ) | ||||
|         .replaceAll( | ||||
|           "\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]", | ||||
|           (m: Match) => | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil | ||||
|               .escapeHtml( | ||||
|                 m.group(3) | ||||
|               )}</a>""" | ||||
|         ) | ||||
|         .replaceAll("\\[user:([^\\s]+?)\\]", (m: Match) => user(m.group(1)).body) | ||||
|         .replaceAll( | ||||
|           "\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]", | ||||
|           (m: Match) => | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m | ||||
|               .group(2)}@${m.group(3).substring(0, 7)}</a>""" | ||||
|         ) | ||||
|         .replaceAll( | ||||
|           "\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]", | ||||
|           (m: Match) => | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil | ||||
|               .escapeHtml( | ||||
|                 m.group(4) | ||||
|               )}</a>""" | ||||
|         ) | ||||
|         .replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m => | ||||
|           val issue = getIssueFromCache(m.group(1), m.group(2), m.group(3)) | ||||
|           if (issue.isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}" title="${issue.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>""" | ||||
|           } else { | ||||
|             s"${m.group(1)}/${m.group(2)}#${m.group(3)}" | ||||
|           } | ||||
|         } | ||||
|         .replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m => | ||||
|           val pullreq = getIssueFromCache(m.group(1), m.group(2), m.group(3)) | ||||
|           if (pullreq.isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}" title="${pullreq.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>""" | ||||
|           } else { | ||||
|             s"${m.group(1)}/${m.group(2)}#${m.group(3)}" | ||||
|           } | ||||
|         } | ||||
|         .replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m => | ||||
|           if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}">${m.group(1)}/${m.group(2)}</a>""" | ||||
|           } else { | ||||
|             s"${m.group(1)}/${m.group(2)}" | ||||
|           } | ||||
|         } | ||||
|         .replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m => | ||||
|           if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>""" | ||||
|           } else { | ||||
|             StringUtil.escapeHtml(m.group(3)) | ||||
|           } | ||||
|         } | ||||
|         .replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m => | ||||
|           if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>""" | ||||
|           } else { | ||||
|             StringUtil.escapeHtml(m.group(3)) | ||||
|           } | ||||
|         } | ||||
|         .replaceAll("\\[user:([^\\s]+?)\\]") { m => | ||||
|           user(m.group(1)).body | ||||
|         } | ||||
|         .replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]") { m => | ||||
|           if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>""" | ||||
|           } else { | ||||
|             s"${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}" | ||||
|           } | ||||
|         } | ||||
|         .replaceAll("\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]") { m => | ||||
|           if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) { | ||||
|             s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(4))}</a>""" | ||||
|           } else { | ||||
|             StringUtil.escapeHtml(m.group(4)) | ||||
|           } | ||||
|         } | ||||
|     ) | ||||
|   // format: off | ||||
|  | ||||
|   /** | ||||
|    * Remove html tags from the given Html instance. | ||||
| @@ -333,9 +341,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache | ||||
|     content: Html | ||||
|   )(implicit context: Context): Html = | ||||
|     (if (mailAddress.isEmpty) { | ||||
|        getAccountByUserName(userName) | ||||
|        getAccountByUserNameFromCache(userName) | ||||
|      } else { | ||||
|        getAccountByMailAddress(mailAddress) | ||||
|        getAccountByMailAddressFromCache(mailAddress) | ||||
|      }).map { account => | ||||
|       Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""") | ||||
|     } getOrElse content | ||||
|   | ||||
| @@ -34,7 +34,7 @@ | ||||
|               <fieldset class="form-group" id="extraMailAddresses"> | ||||
|                 <span class="strong">Additional Mail Address:</span> | ||||
|                 @extraMailAddresses.zipWithIndex.map { case (mail, idx) => | ||||
|                   <input type="text" name="extraMailAddresses[@idx]" id="extraMailAddresses[@idx]" class="form-control extraMailAddress" value="@mail"/> | ||||
|                   <input type="text" name="extraMailAddresses[@idx]" id="extraMailAddresses[@idx]" class="form-control extraMailAddress" value="@mail" aria-label="Additional mail address"/> | ||||
|                   <span id="error-extraMailAddresses_@idx" class="error"></span> | ||||
|                 } | ||||
|               </fieldset> | ||||
|   | ||||
| @@ -17,14 +17,14 @@ | ||||
|       } | ||||
|     </fieldset> | ||||
|     <fieldset class="form-group"> | ||||
|       <label class="strong">URL (Optional)</label> | ||||
|       <label class="strong" for="url">URL (Optional)</label> | ||||
|       <div> | ||||
|         <span id="error-url" class="error"></span> | ||||
|       </div> | ||||
|       <input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/> | ||||
|     </fieldset> | ||||
|     <fieldset class="form-group"> | ||||
|       <label for="groupDescription" class="strong">Description (Optional)</label> | ||||
|       <label for="description" class="strong">Description (Optional)</label> | ||||
|       <div> | ||||
|         <textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea> | ||||
|       </div> | ||||
| @@ -119,7 +119,7 @@ $(function(){ | ||||
|       .append(' ') | ||||
|       .append($('<a>').attr('href', '@context.path/' + userName).text(userName)) | ||||
|       .append(' ') | ||||
|       .append($('<a href="#" class="remove pull-right"><span class="octicon octicon-x"></span></a>'))); | ||||
|       .append($('<a href="#" class="remove pull-right"><span class="octicon octicon-x" aria-label="Remove"></span></a>'))); | ||||
|   } | ||||
|  | ||||
|   function updateMembers(){ | ||||
|   | ||||
| @@ -11,9 +11,9 @@ | ||||
|       </div> | ||||
|       <div class="panel-body"> | ||||
|         <p> | ||||
|           Webhooks allow external services to be notified when certain events happen within your repository. | ||||
|           When the specified events happen, we’ll send a POST request to each of the URLs you provide. | ||||
|           Learn more in <a href="https://github.com/takezoe/gitbucket/wiki/API-WebHook" target="_blank">GitBucket Wiki Webhook Page</a>. | ||||
|           These webhooks notify external services when certain events occur within any of your repositories. | ||||
|           When any of the specified events occur, GitBucket will send a POST request to all of the endpoints (URLs) you provide. | ||||
|           Learn more about this feature on the <a href="https://github.com/gitbucket/gitbucket/wiki/API-WebHook" target="_blank" rel="noopener">GitBucket Wiki</a>. | ||||
|         </p> | ||||
|         <a href="@helpers.url(account.userName)/_hooks/new" class="btn btn-success pull-right" style="margin-bottom: 10px;">Add webhook</a> | ||||
|  | ||||
| @@ -27,10 +27,10 @@ | ||||
|           </td><td> | ||||
|             <div class="btn-group pull-right"> | ||||
|               <a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="btn btn-default"> | ||||
|                 <span class="octicon octicon-pencil"></span> | ||||
|                 <span class="octicon octicon-pencil" aria-label="Edit hook"></span> | ||||
|               </a> | ||||
|               <a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')"> | ||||
|                 <span class="octicon octicon-x"></span> | ||||
|                 <span class="octicon octicon-x" aria-label="Remove hook"></span> | ||||
|               </a> | ||||
|             </div> | ||||
|           </td></tr> | ||||
|   | ||||
| @@ -41,6 +41,11 @@ | ||||
|             <i class="menu-icon octicon octicon-zap"></i> <span>Service Hooks</span> | ||||
|           </a> | ||||
|         </li> | ||||
|         <li class="menu-item-hover @if(active=="preferences"){active}"> | ||||
|           <a href="@context.path/@userName/_preferences"> | ||||
|             <i class="menu-icon octicon octicon-star"></i> <span>Preferences</span> | ||||
|           </a> | ||||
|         </li> | ||||
|         @gitbucket.core.plugin.PluginRegistry().getAccountSettingMenus.map { menu => | ||||
|           @menu(context).map { link => | ||||
|             <li class="menu-item-hover @if(active==link.id){active}"> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C | ||||
|   <div class="content body"> | ||||
|     <h2>Create a new repository</h2> | ||||
|     <p class="muted"> | ||||
|       A repository contains all the files for your project including the revision history. | ||||
|       A repository contains all your project's files, including revision history. | ||||
|     </p> | ||||
|     <form id="form" method="post" action="@context.path/new" validate="true" autocomplete="off"> | ||||
|       <fieldset class="border-top form-group"> | ||||
| @@ -32,7 +32,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C | ||||
|         <dl> | ||||
|           <dt>Repository name</dt> | ||||
|           <dd style="margin-left: 0px;"> | ||||
|             <input type="text" name="name" id="name" class="form-control" style="width: 300px; display: inline;" autofocus/> | ||||
|             <input type="text" name="name" id="name" class="form-control" style="width: 300px; display: inline;" autofocus aria-label="Repository name"/> | ||||
|             <span id="error-name" class="error"></span> | ||||
|           </dd> | ||||
|         </dl> | ||||
| @@ -62,31 +62,31 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C | ||||
|           <input type="radio" name="initOption" value="EMPTY" checked/> | ||||
|           <span class="strong">Create an empty repository</span> | ||||
|           <div class="normal muted"> | ||||
|             Create an empty repository. You have to initialize by yourself initially. | ||||
|             Create an empty repository. You must initialize it yourself. | ||||
|           </div> | ||||
|         </label> | ||||
|         <label class="radio"> | ||||
|           <input type="radio" name="initOption" value="EMPTY_COMMIT"/> | ||||
|           <span class="strong">Initialize this repository with an empty commit</span> | ||||
|           <div class="normal muted"> | ||||
|             Create an empty repository with empty commit. You can clone the repository immediately. | ||||
|             Create an empty repository with an empty commit. You can clone the repository immediately. | ||||
|           </div> | ||||
|         </label> | ||||
|         <label class="radio"> | ||||
|           <input type="radio" name="initOption" value="README"/> | ||||
|           <span class="strong">Initialize this repository with a README</span> | ||||
|           <div class="normal muted"> | ||||
|             Create a repository which has README.md. You can clone the repository immediately. | ||||
|             Create a repository and commit README.md. You can clone the repository immediately. | ||||
|           </div> | ||||
|         </label> | ||||
|         <label class="radio"> | ||||
|           <input type="radio" name="initOption" value="COPY"/> | ||||
|           <span class="strong">Copy existing git repository</span> | ||||
|           <div class="normal muted"> | ||||
|             Create new repository from existing git repository. | ||||
|             Create a new repository by cloning an existing git repository. | ||||
|           </div> | ||||
|         </label> | ||||
|         <input type="text" class="form-control" name="sourceUrl" id="sourceUrl" disabled placeholder="Source git repository URL..."/> | ||||
|         <input type="text" class="form-control" name="sourceUrl" id="sourceUrl" disabled placeholder="Source git repository URL..." aria-label="Source URL"/> | ||||
|         <span id="error-sourceUrl" class="error"></span> | ||||
|       </fieldset> | ||||
|       <fieldset class="border-top form-actions"> | ||||
|   | ||||
							
								
								
									
										82
									
								
								src/main/twirl/gitbucket/core/account/preferences.scala.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/main/twirl/gitbucket/core/account/preferences.scala.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| @(account: gitbucket.core.model.Account, currentTheme: String)(implicit context: gitbucket.core.controller.Context) | ||||
| @gitbucket.core.html.main("Preferences", highlighterTheme = currentTheme){ | ||||
|   @gitbucket.core.account.html.menu("preferences", context.loginAccount.get.userName, false){ | ||||
|     <form method="POST" action="@context.path/@account.userName/_preferences/highlighter" validate="true" autocomplete="off"> | ||||
|       <div class="panel panel-default"> | ||||
|         <div class="panel-heading strong">Syntax Highlighter Theme</div> | ||||
|         <div class="panel-body"> | ||||
|           <div class="row"> | ||||
|             <div class="col-md-4"> | ||||
|               <fieldset class="form-group"> | ||||
|                 <label for="highlighterTheme" class="strong">Theme</label> | ||||
|                 <select id="highlighterTheme" name="highlighterTheme" class="form-control"> | ||||
|                 @Seq( | ||||
|                   ("atelier-cave-dark", "Atelier Cave Dark"), | ||||
|                   ("atelier-cave-light", "Atelier Cave Light"), | ||||
|                   ("atelier-dune-dark", "Atelier Dune Dark"), | ||||
|                   ("atelier-dune-light", "Atelier Dune Light"), | ||||
|                   ("atelier-estuary-dark", "Atelier Estuary Dark"), | ||||
|                   ("atelier-estuary-light", "Atelier Estuary Light"), | ||||
|                   ("atelier-forest-dark", "Atelier Forest Dark"), | ||||
|                   ("atelier-forest-light", "Atelier Forest Light"), | ||||
|                   ("atelier-heath-dark", "Atelier Heath Dark"), | ||||
|                   ("atelier-heath-light", "Atelier Heath Light"), | ||||
|                   ("atelier-lakeside-dark", "Atelier Lakeside Dark"), | ||||
|                   ("atelier-lakeside-light", "Atelier Lakeside Light"), | ||||
|                   ("atelier-plateau-dark", "Atelier Plateau Dark"), | ||||
|                   ("atelier-plateau-light", "Atelier Plateau Light"), | ||||
|                   ("atelier-savanna-dark", "Atelier Savanna Dark"), | ||||
|                   ("atelier-savanna-light", "Atelier Savanna Light"), | ||||
|                   ("atelier-seaside-dark", "Atelier Seaside Dark"), | ||||
|                   ("atelier-seaside-light", "Atelier Seaside Light"), | ||||
|                   ("atelier-sulphurpool-dark", "Atelier Sulphurpool Dark"), | ||||
|                   ("atelier-sulphurpool-light", "Atelier Sulphurpool Light"), | ||||
|                   ("github", "GitHub"), | ||||
|                   ("github-v2", "GitHub v2"), | ||||
|                   ("hemisu-dark", "Hemisu Dark"), | ||||
|                   ("hemisu-light", "Hemisu Light"), | ||||
|                   ("tomorrow", "Tomorrow"), | ||||
|                   ("tomorrow-night", "Tomorrow Night"), | ||||
|                   ("tomorrow-night-blue", "Tomorrow Night Blue"), | ||||
|                   ("tomorrow-night-bright", "Tomorrow Night Bright"), | ||||
|                   ("tomorrow-night-eighties", "Tomorrow Night Eighties"), | ||||
|                   ("vibrant-ink", "Vibrant Ink") | ||||
|                 ).map{ theme => | ||||
|                   <option value="@theme._1"@if(theme._1 == currentTheme){ selected=""}>@theme._2</option> | ||||
|                 } | ||||
|                 </select> | ||||
|               </fieldset> | ||||
|               <input type="submit" class="btn btn-success" value="Save"/> | ||||
|             </div> | ||||
|             <div class="col-md-8"> | ||||
|               <div style="margin: 5px"> | ||||
|                 <pre class="prettyprint linenums">@{ | ||||
|                   """#include <iostream> | ||||
|                     | | ||||
|                     |using namespace std; | ||||
|                     | | ||||
|                     |int main(){ | ||||
|                     |  cout << 'Hello world.' << endl; | ||||
|                     |  return 0; | ||||
|                     |}""".stripMargin} | ||||
|                 </pre> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|     <script> | ||||
|             $(function(){ | ||||
|               $('#highlighterTheme').change(function(evt) { | ||||
|                 var that = $(evt.target); | ||||
|                 var themeCss = $('link[rel="stylesheet"][href*="color-themes-for-google-code-prettify"]'); | ||||
|                 var oldVal = new RegExp('color-themes-for-google-code-prettify/(.*?).min.css').exec(themeCss.attr('href'))[1]; | ||||
|                 themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val())); | ||||
|                 $(document.body).removeClass(oldVal).addClass(that.val()); | ||||
|               }); | ||||
|               prettyPrint(); | ||||
|             }); | ||||
|     </script> | ||||
|   } | ||||
| } | ||||
| @@ -21,7 +21,7 @@ | ||||
|       <div class="panel-heading strong">Import</div> | ||||
|       <div class="panel-body"> | ||||
|         <form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form"> | ||||
|           <input type="file" name="file" id="file"> | ||||
|           <input type="file" name="file" id="file" aria-label="Upload file"> | ||||
|           <input type="submit" class="btn btn-success pull-right" value="Import" id="import"> | ||||
|         </form> | ||||
|       </div> | ||||
|   | ||||
| @@ -5,19 +5,19 @@ | ||||
|   @gitbucket.core.admin.html.menu("system"){ | ||||
|     @gitbucket.core.helper.html.information(info) | ||||
|     <form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal" autocomplete="off"> | ||||
|       <ul class="nav nav-tabs fill-width" id="pullreq-tab"> | ||||
|       <ul class="nav nav-tabs fill-width" id="settings-tab"> | ||||
|         <li><a href="#system">System settings</a></li> | ||||
|         <li><a href="#integrations">Integrations</a></li> | ||||
|         <li><a href="#authentication">Authentication</a></li> | ||||
|       </ul> | ||||
|       <div class="tab-content fill-width" style="padding-top: 20px;"> | ||||
|         <div class="tab-pane" id="system"> | ||||
|         <div class="tab-pane" id="tab-system"> | ||||
|           @settings_system(info) | ||||
|         </div> | ||||
|         <div class="tab-pane" id="integrations"> | ||||
|         <div class="tab-pane" id="tab-integrations"> | ||||
|           @settings_integrations(info) | ||||
|         </div> | ||||
|         <div class="tab-pane" id="authentication"> | ||||
|         <div class="tab-pane" id="tab-authentication"> | ||||
|           @settings_authentication(info) | ||||
|         </div> | ||||
|       </div> | ||||
| @@ -30,25 +30,29 @@ | ||||
| } | ||||
| <script> | ||||
| $(function(){ | ||||
|   function updateTabs(){ | ||||
|     $('ul.nav-tabs li').removeClass('active'); | ||||
|     $('div.tab-pane').removeClass('active'); | ||||
|  | ||||
|     // Determine active tab from hash | ||||
|     if(location.hash == '#authentication'){ | ||||
|       $('li:has(a[href="#authentication"])').addClass('active'); | ||||
|     $('div#authentication').addClass('active'); | ||||
|       $('div#tab-authentication').addClass('active'); | ||||
|     } else if(location.hash == '#integrations'){ | ||||
|       $('li:has(a[href="#integrations"])').addClass('active'); | ||||
|     $('div#integrations').addClass('active'); | ||||
|       $('div#tab-integrations').addClass('active'); | ||||
|     } else { | ||||
|       $('li:has(a[href="#system"])').addClass('active'); | ||||
|     $('div#system').addClass('active'); | ||||
|       $('div#tab-system').addClass('active'); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Set hash when tab is clicked | ||||
|   $('ul.nav-tabs li a').click(function(e){ | ||||
|   $('#settings-tab li a').click(function(e){ | ||||
|     location.href = $(e.delegateTarget).attr("href"); | ||||
|     updateTabs(); | ||||
|   }); | ||||
|  | ||||
|   $('#pullreq-tab a').click(function (e) { | ||||
|     e.preventDefault(); | ||||
|     $(this).tab('show'); | ||||
|   }); | ||||
|   updateTabs(); | ||||
| }); | ||||
| </script> | ||||
|   | ||||
| @@ -22,8 +22,8 @@ | ||||
|     <td class="danger"> | ||||
|       <p>@gitbucket.core.util.DatabaseConfig.url</p> | ||||
|       <p> | ||||
|         Your GitBucket is running on embedded H2 database. | ||||
|         Recommend to <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure to use external database</a> if you would like to use GitBucket for important purpose. | ||||
|         GitBucket is using the embedded H2 database. | ||||
|         It's recommended that you <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure GitBucket to use an external database</a> if you're running GitBucket in a production environment. | ||||
|       </p> | ||||
|     </td> | ||||
|     }else{ | ||||
| @@ -35,7 +35,7 @@ | ||||
| <!-- Base URL --> | ||||
| <!--====================================================================--> | ||||
| <hr> | ||||
| <label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label> | ||||
| <label for="baseUrl"><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label> | ||||
| <fieldset> | ||||
|     <div class="controls"> | ||||
|       <input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@context.settings.baseUrl"/> | ||||
| @@ -43,17 +43,17 @@ | ||||
|     </div> | ||||
| </fieldset> | ||||
| <p class="muted"> | ||||
|   The base URL is used for redirect, notification email, git repository URL box and more. | ||||
|   If the base URL is empty, GitBucket generates URL from the request information. | ||||
|   You can use this property to adjust to URL differences between the reverse proxy and GitBucket. | ||||
|   The base URL is used for redirects, notification emails, git repository URL boxes, and more. | ||||
|   If the base URL is empty, GitBucket generates the URL from the request information. | ||||
|   You can use this property to adjust to URL differences between a reverse proxy and GitBucket. | ||||
| </p> | ||||
| <!--====================================================================--> | ||||
| <!-- Information --> | ||||
| <!--====================================================================--> | ||||
| <hr> | ||||
| <label><span class="strong">Information</span> (HTML is available)</label> | ||||
| <label for="information"><span class="strong">Site notification</span> (Supports HTML)</label> | ||||
| <fieldset> | ||||
|   <textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea> | ||||
|   <textarea id="information" name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea> | ||||
| </fieldset> | ||||
| <!--====================================================================--> | ||||
| <!-- AdminLTE SkinName --> | ||||
| @@ -97,9 +97,9 @@ | ||||
| <!-- User-defined CSS --> | ||||
| <!--====================================================================--> | ||||
| <hr> | ||||
| <label><span class="strong">User-defined CSS</span></label> | ||||
| <label for="userDefinedCss"><span class="strong">User-defined CSS</span></label> | ||||
| <fieldset> | ||||
|   <textarea name="userDefinedCss" class="form-control" style="height: 100px;">@context.settings.userDefinedCss</textarea> | ||||
|   <textarea id="userDefinedCss" name="userDefinedCss" class="form-control" style="height: 100px;">@context.settings.userDefinedCss</textarea> | ||||
| </fieldset> | ||||
| <!--====================================================================--> | ||||
| <!-- Account registration --> | ||||
| @@ -266,34 +266,39 @@ | ||||
|   </div> | ||||
| </fieldset> | ||||
| <!--====================================================================--> | ||||
| <!-- Activity --> | ||||
| <!--====================================================================--> | ||||
| <hr> | ||||
| <label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label> | ||||
| <fieldset> | ||||
|   <div class="form-group"> | ||||
|     <label class="control-label col-md-2" for="activityLogLimit">Limit</label> | ||||
|     <div class="col-md-10"> | ||||
|       <input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/> | ||||
|       <span id="error-activityLogLimit" class="error"></span> | ||||
|     </div> | ||||
|   </div> | ||||
| </fieldset> | ||||
| <!--====================================================================--> | ||||
| <!-- Sidebar --> | ||||
| <!--====================================================================--> | ||||
| <hr> | ||||
| <label><span class="strong">Show Repositories in Sidebar</span></label> | ||||
| <label><span class="strong">Show repositories in sidebar</span></label> | ||||
| <fieldset> | ||||
|   <label class="radio"> | ||||
|     <input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}> | ||||
|     <span class="strong">All</span> <span class="normal">- Show all repositories in sidebar.</span> | ||||
|     <span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span> | ||||
|   </label> | ||||
|   <label class="radio"> | ||||
|     <input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}> | ||||
|     <span class="strong">Limited</span> <span class="normal">- Limit the visible repositories in sidebar.</span> | ||||
|     <span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span> | ||||
|   </label> | ||||
| </fieldset> | ||||
| <!--====================================================================--> | ||||
| <!-- Repository viewer --> | ||||
| <!--====================================================================--> | ||||
| <hr> | ||||
| <label><span class="strong">Repository viewer</span></label> | ||||
| <fieldset> | ||||
|   <div class="form-group"> | ||||
|     <label class="control-label col-md-2" for="repositoryViewerMaxFiles">Max files in directory</label> | ||||
|     <div class="col-md-10"> | ||||
|       <input type="text" name="repositoryViewer.maxFiles" id="repositoryViewerMaxFiles" class="form-control" value="@context.settings.repositoryViewer.maxFiles"/> | ||||
|       <span id="error-repositoryViewerMaxFiles" class="error"></span> | ||||
|       <p class="muted"> | ||||
|         If the number of files in the directory is bigger than this number, GitBucket doesn't show detailed commit info on the repository viewer | ||||
|         because it can make the entire system quite heavy. 0 or negative number means no limitation. | ||||
|       </p> | ||||
|     </div> | ||||
|   </div> | ||||
| </fieldset> | ||||
|  | ||||
| <script> | ||||
| $(function(){ | ||||
|   $('#skinName').change(function(evt) { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user