mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-10-30 01:56:09 +01:00 
			
		
		
		
	Compare commits
	
		
			91 Commits
		
	
	
		
			disable-gi
			...
			4.35.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 41d13db5d4 | ||
|  | c63e20ce7d | ||
|  | 736fdafea4 | ||
|  | 1dfe76e21c | ||
|  | e7ddfc7ebb | ||
|  | 66be84289d | ||
|  | 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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										44
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,15 +1,43 @@ | ||||
| # Changelog | ||||
| All changes to the project will be documented in this file. | ||||
|  | ||||
| ### 4.33.0 - 31 Dec 2019 | ||||
| ### 4.35.1 - 29 Dec 2020 | ||||
| - Fix database migration issue which happens if webhook is configured | ||||
| - Call push webhook when pull request is merged | ||||
| - Show commit status at commits tab of pull request | ||||
|  | ||||
| ### 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 +45,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 +145,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 | ||||
|   | ||||
							
								
								
									
										22
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								README.md
									
									
									
									
									
								
							| @@ -55,13 +55,23 @@ 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.35.x | ||||
| ------------- | ||||
| ### 4.33.0 - 31 Dec 2019 | ||||
| ### 4.35.1 - 29 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 | ||||
| - Fix database migration issue which happens if webhook is configured | ||||
| - Call push webhook when pull request is merged | ||||
| - Show commit status at commits tab of pull request | ||||
|  | ||||
| ### 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 | ||||
|  | ||||
| See the [change log](CHANGELOG.md) for all of the updates. | ||||
|   | ||||
							
								
								
									
										33
									
								
								build.sbt
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								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.1" | ||||
| val ScalatraVersion = "2.7.1" | ||||
| val JettyVersion = "9.4.32.v20200930" | ||||
| val JgitVersion = "5.9.0.202009080501-r" | ||||
|  | ||||
| lazy val root = (project in file(".")) | ||||
|   .enablePlugins(SbtTwirl, ScalatraPlugin) | ||||
| @@ -17,7 +17,7 @@ sourcesInBase := false | ||||
| organization := Organization | ||||
| name := Name | ||||
| version := GitBucketVersion | ||||
| scalaVersion := "2.13.1" | ||||
| scalaVersion := "2.13.3" | ||||
|  | ||||
| scalafmtOnCompile := true | ||||
|  | ||||
| @@ -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,11 +70,12 @@ 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 | ||||
| scalacOptions := Seq("-deprecation", "-language:postfixOps", "-opt:l:method") | ||||
| scalacOptions := Seq("-deprecation", "-language:postfixOps", "-opt:l:method", "-feature") | ||||
| javacOptions in compile ++= Seq("-target", "8", "-source", "8") | ||||
| javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" | ||||
|  | ||||
| @@ -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.6 | ||||
|   | ||||
| @@ -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> | ||||
							
								
								
									
										39
									
								
								src/main/resources/update/gitbucket-core_4.35.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/main/resources/update/gitbucket-core_4.35.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <changeSet> | ||||
|   <!--================================================================================================--> | ||||
|   <!-- WEB_HOOK --> | ||||
|   <!--================================================================================================--> | ||||
|   <dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/> | ||||
|   <dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK"/> | ||||
|   <dropPrimaryKey constraintName="IDX_WEB_HOOK_PK" tableName="WEB_HOOK"/> | ||||
|    | ||||
|   <createTable tableName="WEB_HOOK_2"> | ||||
|     <column name="HOOK_ID" type="int" nullable="true" autoIncrement="true" unique="false" primaryKeyName="IDX_WEB_HOOK_PK" primaryKey="true" /> | ||||
|     <column name="USER_NAME" type="varchar(100)" nullable="false"/> | ||||
|     <column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/> | ||||
|     <column name="URL" type="varchar(200)" nullable="false"/> | ||||
|     <column name="TOKEN" type="varchar(100)" nullable="true"/> | ||||
|     <column name="CTYPE" type="varchar(10)" nullable="true"/> | ||||
|   </createTable> | ||||
|  | ||||
|   <sql> | ||||
|     INSERT INTO WEB_HOOK_2 (USER_NAME, REPOSITORY_NAME, URL, TOKEN, CTYPE) SELECT USER_NAME, REPOSITORY_NAME, URL, TOKEN, CTYPE FROM WEB_HOOK | ||||
|   </sql> | ||||
|  | ||||
|   <renameTable newTableName="WEB_HOOK_BK" oldTableName="WEB_HOOK"/> | ||||
|   <renameTable newTableName="WEB_HOOK" oldTableName="WEB_HOOK_2"/> | ||||
|  | ||||
|   <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"/> | ||||
|  | ||||
|   <!--================================================================================================--> | ||||
|   <!-- ACCOUNT_PREFERENCE --> | ||||
|   <!--================================================================================================--> | ||||
|   <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,40 @@ 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")), | ||||
|       new Version("4.35.1"), | ||||
|     ) | ||||
|   | ||||
| @@ -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 { | ||||
|  | ||||
|   | ||||
| @@ -421,7 +421,7 @@ trait AccountManagementControllerBase extends ControllerBase { | ||||
|     "new" | ||||
|   ) | ||||
|  | ||||
|   protected def reservedNames(): Constraint = new Constraint() { | ||||
|   protected def reservedNames: Constraint = new Constraint() { | ||||
|     override def validate(name: String, value: String, messages: Messages): Option[String] = | ||||
|       if (allReservedNames.contains(value.toLowerCase)) { | ||||
|         Some(s"${value} is reserved") | ||||
|   | ||||
| @@ -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)) | ||||
|   | ||||
| @@ -37,7 +37,7 @@ class FileUploadController | ||||
|     execute( | ||||
|       { (file, fileId) => | ||||
|         FileUtils | ||||
|           .writeByteArrayToFile(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)), file.get) | ||||
|           .writeByteArrayToFile(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)), file.get()) | ||||
|         session += Keys.Session.Upload(fileId) -> file.name | ||||
|       }, | ||||
|       FileUtil.isImage | ||||
| @@ -49,7 +49,7 @@ class FileUploadController | ||||
|     execute( | ||||
|       { (file, fileId) => | ||||
|         FileUtils | ||||
|           .writeByteArrayToFile(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)), file.get) | ||||
|           .writeByteArrayToFile(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(fileId)), file.get()) | ||||
|         session += Keys.Session.Upload(fileId) -> file.name | ||||
|       }, | ||||
|       _ => true | ||||
| @@ -65,7 +65,7 @@ class FileUploadController | ||||
|             getAttachedDir(params("owner"), params("repository")), | ||||
|             FileUtil.checkFilename(fileId + "." + FileUtil.getExtension(file.getName)) | ||||
|           ), | ||||
|           file.get | ||||
|           file.get() | ||||
|         ) | ||||
|       }, | ||||
|       _ => true | ||||
| @@ -144,7 +144,7 @@ class FileUploadController | ||||
|             { (file, fileId) => | ||||
|               FileUtils.writeByteArrayToFile( | ||||
|                 new File(getReleaseFilesDir(owner, repository), FileUtil.checkFilename(tag + "/" + fileId)), | ||||
|                 file.get | ||||
|                 file.get() | ||||
|               ) | ||||
|             }, | ||||
|             _ => true | ||||
|   | ||||
| @@ -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 | ||||
| @@ -167,10 +169,16 @@ trait PullRequestsControllerBase extends ControllerBase { | ||||
|             val (commits, diffs) = | ||||
|               getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) | ||||
|  | ||||
|             val commitsWithStatus = commits.map { day => | ||||
|               day.map { commit => | ||||
|                 (commit, getCommitStatusWithSummary(repository.owner, repository.name, commit.id)) | ||||
|               } | ||||
|             } | ||||
|  | ||||
|             html.commits( | ||||
|               issue, | ||||
|               pullreq, | ||||
|               commits, | ||||
|               commitsWithStatus, | ||||
|               getPullRequestComments(owner, name, issue.issueId, commits.flatten), | ||||
|               diffs.size, | ||||
|               isManageable(repository), | ||||
| @@ -217,7 +225,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 +279,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 +643,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 | ||||
| @@ -210,15 +230,79 @@ trait ApiPullRequestControllerBase extends ControllerBase { | ||||
|       if (checkConflict(repository.owner, repository.name, pullReq.branch, issueId).isDefined) { | ||||
|         NoContent | ||||
|       } else { | ||||
|         NotFound | ||||
|         NotFound() | ||||
|       } | ||||
|     }).getOrElse(NotFound) | ||||
|     }).getOrElse(NotFound()) | ||||
|   }) | ||||
|  | ||||
|   /* | ||||
|    * 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 | ||||
|  */ | ||||
|  | ||||
| } | ||||
| @@ -10,7 +10,7 @@ trait AccessTokenComponent { self: Profile => | ||||
|     val userName = column[String]("USER_NAME") | ||||
|     val tokenHash = column[String]("TOKEN_HASH") | ||||
|     val note = column[String]("NOTE") | ||||
|     def * = (accessTokenId, userName, tokenHash, note) <> (AccessToken.tupled, AccessToken.unapply) | ||||
|     def * = (accessTokenId, userName, tokenHash, note).<>(AccessToken.tupled, AccessToken.unapply) | ||||
|   } | ||||
| } | ||||
| case class AccessToken( | ||||
|   | ||||
| @@ -35,7 +35,7 @@ trait AccountComponent { self: Profile => | ||||
|         groupAccount, | ||||
|         removed, | ||||
|         description.? | ||||
|       ) <> (Account.tupled, Account.unapply) | ||||
|       ).<>(Account.tupled, Account.unapply) | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ trait AccountExtraMailAddressComponent { self: Profile => | ||||
|     val userName = column[String]("USER_NAME", O PrimaryKey) | ||||
|     val extraMailAddress = column[String]("EXTRA_MAIL_ADDRESS", O PrimaryKey) | ||||
|     def * = | ||||
|       (userName, extraMailAddress) <> (AccountExtraMailAddress.tupled, AccountExtraMailAddress.unapply) | ||||
|       (userName, extraMailAddress).<>(AccountExtraMailAddress.tupled, AccountExtraMailAddress.unapply) | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ trait AccountFederationComponent { self: Profile => | ||||
|     val issuer = column[String]("ISSUER") | ||||
|     val subject = column[String]("SUBJECT") | ||||
|     val userName = column[String]("USER_NAME") | ||||
|     def * = (issuer, subject, userName) <> (AccountFederation.tupled, AccountFederation.unapply) | ||||
|     def * = (issuer, subject, userName).<>(AccountFederation.tupled, AccountFederation.unapply) | ||||
|  | ||||
|     def byPrimaryKey(issuer: String, subject: String): Rep[Boolean] = | ||||
|       (this.issuer === issuer.bind) && (this.subject === subject.bind) | ||||
|   | ||||
							
								
								
									
										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" | ||||
| ) | ||||
| @@ -12,7 +12,7 @@ trait AccountWebHookComponent extends TemplateComponent { self: Profile => | ||||
|     val url = column[String]("URL") | ||||
|     val token = column[Option[String]]("TOKEN") | ||||
|     val ctype = column[WebHookContentType]("CTYPE") | ||||
|     def * = (userName, url, ctype, token) <> ((AccountWebHook.apply _).tupled, AccountWebHook.unapply) | ||||
|     def * = (userName, url, ctype, token).<>((AccountWebHook.apply _).tupled, AccountWebHook.unapply) | ||||
|  | ||||
|     def byPrimaryKey(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) | ||||
|   } | ||||
|   | ||||
| @@ -14,7 +14,7 @@ trait AccountWebHookEventComponent extends TemplateComponent { | ||||
|     val url = column[String]("URL") | ||||
|     val event = column[WebHook.Event]("EVENT") | ||||
|  | ||||
|     def * = (userName, url, event) <> ((AccountWebHookEvent.apply _).tupled, AccountWebHookEvent.unapply) | ||||
|     def * = (userName, url, event).<>((AccountWebHookEvent.apply _).tupled, AccountWebHookEvent.unapply) | ||||
|  | ||||
|     def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| ) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ trait CollaboratorComponent extends TemplateComponent { self: Profile => | ||||
|   class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { | ||||
|     val collaboratorName = column[String]("COLLABORATOR_NAME") | ||||
|     val role = column[String]("ROLE") | ||||
|     def * = (userName, repositoryName, collaboratorName, role) <> (Collaborator.tupled, Collaborator.unapply) | ||||
|     def * = (userName, repositoryName, collaboratorName, role).<>(Collaborator.tupled, Collaborator.unapply) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String, collaborator: String) = | ||||
|       byRepository(owner, repository) && (collaboratorName === collaborator.bind) | ||||
|   | ||||
| @@ -20,7 +20,8 @@ trait IssueCommentComponent extends TemplateComponent { self: Profile => | ||||
|     val registeredDate = column[java.util.Date]("REGISTERED_DATE") | ||||
|     val updatedDate = column[java.util.Date]("UPDATED_DATE") | ||||
|     def * = | ||||
|       (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) <> (IssueComment.tupled, IssueComment.unapply) | ||||
|       (userName, repositoryName, issueId, commentId, action, commentedUserName, content, registeredDate, updatedDate) | ||||
|         .<>(IssueComment.tupled, IssueComment.unapply) | ||||
|  | ||||
|     def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind | ||||
|   } | ||||
| @@ -74,7 +75,7 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile => | ||||
|         originalCommitId, | ||||
|         originalOldLine, | ||||
|         originalNewLine | ||||
|       ) <> (CommitComment.tupled, CommitComment.unapply) | ||||
|       ).<>(CommitComment.tupled, CommitComment.unapply) | ||||
|  | ||||
|     def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind | ||||
|   } | ||||
|   | ||||
| @@ -29,7 +29,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile => | ||||
|         creator, | ||||
|         registeredDate, | ||||
|         updatedDate | ||||
|       ) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply) | ||||
|       ).<>((CommitStatus.apply _).tupled, CommitStatus.unapply) | ||||
|     def byPrimaryKey(id: Int) = commitStatusId === id.bind | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ trait DeployKeyComponent extends TemplateComponent { self: Profile => | ||||
|     val publicKey = column[String]("PUBLIC_KEY") | ||||
|     val allowWrite = column[Boolean]("ALLOW_WRITE") | ||||
|     def * = | ||||
|       (userName, repositoryName, deployKeyId, title, publicKey, allowWrite) <> (DeployKey.tupled, DeployKey.unapply) | ||||
|       (userName, repositoryName, deployKeyId, title, publicKey, allowWrite).<>(DeployKey.tupled, DeployKey.unapply) | ||||
|  | ||||
|     def byPrimaryKey(userName: String, repositoryName: String, deployKeyId: Int) = | ||||
|       (this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.deployKeyId === deployKeyId.bind) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ trait GpgKeyComponent { self: Profile => | ||||
|     val gpgKeyId = column[Long]("GPG_KEY_ID") | ||||
|     val title = column[String]("TITLE") | ||||
|     val publicKey = column[String]("PUBLIC_KEY") | ||||
|     def * = (userName, keyId, gpgKeyId, title, publicKey) <> (GpgKey.tupled, GpgKey.unapply) | ||||
|     def * = (userName, keyId, gpgKeyId, title, publicKey).<>(GpgKey.tupled, GpgKey.unapply) | ||||
|  | ||||
|     def byPrimaryKey(userName: String, keyId: Int) = | ||||
|       (this.userName === userName.bind) && (this.keyId === keyId.bind) | ||||
|   | ||||
| @@ -9,7 +9,7 @@ trait GroupMemberComponent { self: Profile => | ||||
|     val groupName = column[String]("GROUP_NAME", O PrimaryKey) | ||||
|     val userName = column[String]("USER_NAME", O PrimaryKey) | ||||
|     val isManager = column[Boolean]("MANAGER") | ||||
|     def * = (groupName, userName, isManager) <> (GroupMember.tupled, GroupMember.unapply) | ||||
|     def * = (groupName, userName, isManager).<>(GroupMember.tupled, GroupMember.unapply) | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -49,7 +49,7 @@ trait IssueComponent extends TemplateComponent { self: Profile => | ||||
|         registeredDate, | ||||
|         updatedDate, | ||||
|         pullRequest | ||||
|       ) <> (Issue.tupled, Issue.unapply) | ||||
|       ).<>(Issue.tupled, Issue.unapply) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) | ||||
|   } | ||||
|   | ||||
| @@ -6,7 +6,7 @@ trait IssueLabelComponent extends TemplateComponent { self: Profile => | ||||
|   lazy val IssueLabels = TableQuery[IssueLabels] | ||||
|  | ||||
|   class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { | ||||
|     def * = (userName, repositoryName, issueId, labelId) <> (IssueLabel.tupled, IssueLabel.unapply) | ||||
|     def * = (userName, repositoryName, issueId, labelId).<>(IssueLabel.tupled, IssueLabel.unapply) | ||||
|     def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = | ||||
|       byIssue(owner, repository, issueId) && (this.labelId === labelId.bind) | ||||
|   } | ||||
|   | ||||
| @@ -9,7 +9,7 @@ trait LabelComponent extends TemplateComponent { self: Profile => | ||||
|     override val labelId = column[Int]("LABEL_ID", O AutoInc) | ||||
|     override val labelName = column[String]("LABEL_NAME") | ||||
|     val color = column[String]("COLOR") | ||||
|     def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply) | ||||
|     def * = (userName, repositoryName, labelId, labelName, color).<>(Label.tupled, Label.unapply) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) | ||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], labelId: Rep[Int]) = | ||||
|   | ||||
| @@ -13,7 +13,8 @@ trait MilestoneComponent extends TemplateComponent { self: Profile => | ||||
|     val dueDate = column[Option[java.util.Date]]("DUE_DATE") | ||||
|     val closedDate = column[Option[java.util.Date]]("CLOSED_DATE") | ||||
|     def * = | ||||
|       (userName, repositoryName, milestoneId, title, description, dueDate, closedDate) <> (Milestone.tupled, Milestone.unapply) | ||||
|       (userName, repositoryName, milestoneId, title, description, dueDate, closedDate) | ||||
|         .<>(Milestone.tupled, Milestone.unapply) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) | ||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], milestoneId: Rep[Int]) = | ||||
|   | ||||
| @@ -13,7 +13,8 @@ trait PriorityComponent extends TemplateComponent { self: Profile => | ||||
|     val isDefault = column[Boolean]("IS_DEFAULT") | ||||
|     val color = column[String]("COLOR") | ||||
|     def * = | ||||
|       (userName, repositoryName, priorityId, priorityName, description.?, isDefault, ordering, color) <> (Priority.tupled, Priority.unapply) | ||||
|       (userName, repositoryName, priorityId, priorityName, description.?, isDefault, ordering, color) | ||||
|         .<>(Priority.tupled, Priority.unapply) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String, priorityId: Int) = byPriority(owner, repository, priorityId) | ||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = | ||||
|   | ||||
| @@ -70,5 +70,6 @@ trait CoreProfile | ||||
|     with ReleaseTagComponent | ||||
|     with ReleaseAssetComponent | ||||
|     with AccountExtraMailAddressComponent | ||||
|     with AccountPreferenceComponent | ||||
|  | ||||
| object Profile extends CoreProfile | ||||
|   | ||||
| @@ -7,7 +7,7 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile => | ||||
|   lazy val ProtectedBranches = TableQuery[ProtectedBranches] | ||||
|   class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate { | ||||
|     val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") | ||||
|     def * = (userName, repositoryName, branch, statusCheckAdmin) <> (ProtectedBranch.tupled, ProtectedBranch.unapply) | ||||
|     def * = (userName, repositoryName, branch, statusCheckAdmin).<>(ProtectedBranch.tupled, ProtectedBranch.unapply) | ||||
|     def byPrimaryKey(userName: String, repositoryName: String, branch: String) = | ||||
|       byBranch(userName, repositoryName, branch) | ||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) = | ||||
| @@ -20,7 +20,7 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile => | ||||
|       with BranchTemplate { | ||||
|     val context = column[String]("CONTEXT") | ||||
|     def * = | ||||
|       (userName, repositoryName, branch, context) <> (ProtectedBranchContext.tupled, ProtectedBranchContext.unapply) | ||||
|       (userName, repositoryName, branch, context).<>(ProtectedBranchContext.tupled, ProtectedBranchContext.unapply) | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -25,7 +25,7 @@ trait PullRequestComponent extends TemplateComponent { self: Profile => | ||||
|         commitIdFrom, | ||||
|         commitIdTo, | ||||
|         isDraft | ||||
|       ) <> (PullRequest.tupled, PullRequest.unapply) | ||||
|       ).<>(PullRequest.tupled, PullRequest.unapply) | ||||
|  | ||||
|     def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = | ||||
|       byIssue(userName, repositoryName, issueId) | ||||
|   | ||||
| @@ -21,7 +21,8 @@ trait ReleaseAssetComponent extends TemplateComponent { | ||||
|     val updatedDate = column[Date]("UPDATED_DATE") | ||||
|  | ||||
|     def * = | ||||
|       (userName, repositoryName, tag, releaseAssetId, fileName, label, size, uploader, registeredDate, updatedDate) <> (ReleaseAsset.tupled, ReleaseAsset.unapply) | ||||
|       (userName, repositoryName, tag, releaseAssetId, fileName, label, size, uploader, registeredDate, updatedDate) | ||||
|         .<>(ReleaseAsset.tupled, ReleaseAsset.unapply) | ||||
|     def byPrimaryKey(owner: String, repository: String, tag: String, fileName: String) = | ||||
|       byTag(owner, repository, tag) && (this.fileName === fileName.bind) | ||||
|     def byTag(owner: String, repository: String, tag: String) = | ||||
|   | ||||
| @@ -17,7 +17,8 @@ trait ReleaseTagComponent extends TemplateComponent { | ||||
|     val updatedDate = column[java.util.Date]("UPDATED_DATE") | ||||
|  | ||||
|     def * = | ||||
|       (userName, repositoryName, name, tag, author, content, registeredDate, updatedDate) <> (ReleaseTag.tupled, ReleaseTag.unapply) | ||||
|       (userName, repositoryName, name, tag, author, content, registeredDate, updatedDate) | ||||
|         .<>(ReleaseTag.tupled, ReleaseTag.unapply) | ||||
|     def byPrimaryKey(owner: String, repository: String, tag: String) = byTag(owner, repository, tag) | ||||
|     def byTag(owner: String, repository: String, tag: String) = | ||||
|       byRepository(owner, repository) && (this.tag === tag.bind) | ||||
|   | ||||
| @@ -42,7 +42,8 @@ trait RepositoryComponent extends TemplateComponent { self: Profile => | ||||
|           parentRepositoryName.? | ||||
|         ), | ||||
|         (issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork, mergeOptions, defaultMergeOption) | ||||
|       ).shaped <> ({ | ||||
|       ).shaped.<>( | ||||
|         { | ||||
|           case (repository, options) => | ||||
|             Repository( | ||||
|               repository._1, | ||||
| @@ -81,7 +82,8 @@ trait RepositoryComponent extends TemplateComponent { self: Profile => | ||||
|               ) | ||||
|             ) | ||||
|           ) | ||||
|       }) | ||||
|         } | ||||
|       ) | ||||
|  | ||||
|     def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) | ||||
|   } | ||||
|   | ||||
| @@ -9,20 +9,26 @@ 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] | ||||
|   | ||||
| @@ -12,7 +12,7 @@ trait RepositoryWebHookEventComponent extends TemplateComponent { self: Profile | ||||
|     val url = column[String]("URL") | ||||
|     val event = column[WebHook.Event]("EVENT") | ||||
|     def * = | ||||
|       (userName, repositoryName, url, event) <> ((RepositoryWebHookEvent.apply _).tupled, RepositoryWebHookEvent.unapply) | ||||
|       (userName, repositoryName, url, event).<>((RepositoryWebHookEvent.apply _).tupled, RepositoryWebHookEvent.unapply) | ||||
|  | ||||
|     def byRepositoryWebHook(owner: String, repository: String, url: String) = | ||||
|       byRepository(owner, repository) && (this.url === url.bind) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ trait SshKeyComponent { self: Profile => | ||||
|     val sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc) | ||||
|     val title = column[String]("TITLE") | ||||
|     val publicKey = column[String]("PUBLIC_KEY") | ||||
|     def * = (userName, sshKeyId, title, publicKey) <> (SshKey.tupled, SshKey.unapply) | ||||
|     def * = (userName, sshKeyId, title, publicKey).<>(SshKey.tupled, SshKey.unapply) | ||||
|  | ||||
|     def byPrimaryKey(userName: String, sshKeyId: Int) = | ||||
|       (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind) | ||||
|   | ||||
| @@ -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) | ||||
| @@ -103,13 +105,13 @@ trait AccountService { | ||||
|   } | ||||
|  | ||||
|   def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = | ||||
|     Accounts filter (t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption | ||||
|     Accounts filter (t => (t.userName === userName.bind).&&(t.removed === false.bind, !includeRemoved)) firstOption | ||||
|  | ||||
|   def getAccountByUserNameIgnoreCase(userName: String, includeRemoved: Boolean = false)( | ||||
|     implicit s: Session | ||||
|   ): Option[Account] = | ||||
|     Accounts filter ( | ||||
|       t => (t.userName.toLowerCase === userName.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved) | ||||
|       t => (t.userName.toLowerCase === userName.toLowerCase.bind).&&(t.removed === false.bind, !includeRemoved) | ||||
|     ) firstOption | ||||
|  | ||||
|   def getAccountsByUserNames(userNames: Set[String], knowns: Set[Account], includeRemoved: Boolean = false)( | ||||
| @@ -121,7 +123,7 @@ trait AccountService { | ||||
|       map | ||||
|     } else { | ||||
|       map ++ Accounts | ||||
|         .filter(t => (t.userName inSetBind needs) && (t.removed === false.bind, !includeRemoved)) | ||||
|         .filter(t => (t.userName inSetBind needs).&&(t.removed === false.bind, !includeRemoved)) | ||||
|         .list | ||||
|         .map(a => a.userName -> a) | ||||
|         .toMap | ||||
| @@ -138,15 +140,15 @@ trait AccountService { | ||||
|             (x.map { e => | ||||
|                 e.extraMailAddress.toLowerCase === mailAddress.toLowerCase.bind | ||||
|               } | ||||
|               .getOrElse(false.bind))) && (a.removed === false.bind, !includeRemoved) | ||||
|               .getOrElse(false.bind))).&&(a.removed === false.bind, !includeRemoved) | ||||
|       } | ||||
|       .map { case (a, e) => a } firstOption | ||||
|  | ||||
|   def getAllUsers(includeRemoved: Boolean = true, includeGroups: Boolean = true)(implicit s: Session): List[Account] = { | ||||
|     Accounts filter { t => | ||||
|       (1.bind === 1.bind) && | ||||
|       (t.groupAccount === false.bind, !includeGroups) && | ||||
|       (t.removed === false.bind, !includeRemoved) | ||||
|       (1.bind === 1.bind) | ||||
|         .&&(t.groupAccount === false.bind, !includeGroups) | ||||
|         .&&(t.removed === false.bind, !includeRemoved) | ||||
|     } sortBy (_.userName) list | ||||
|   } | ||||
|  | ||||
| @@ -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,103 @@ 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 = { | ||||
|     import scala.language.reflectiveCalls | ||||
|     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 => | ||||
| @@ -358,45 +351,64 @@ trait IssuesService { | ||||
|        } else { | ||||
|          ((t1.userName ++ "/" ++ t1.repositoryName) inSetBind (repos.map { case (owner, repo) => s"$owner/$repo" })) | ||||
|        }) && | ||||
|       (t1.closed === (condition.state == "closed").bind) && | ||||
|       (t1.milestoneId.? isEmpty, condition.milestone == Some(None)) && | ||||
|       (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) && | ||||
|       (t1.closed === (condition.state == "closed").bind) | ||||
|         .&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) | ||||
|         .&&(t1.priorityId.? isEmpty, condition.priority == Some(None)) | ||||
|         .&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) | ||||
|         .&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && | ||||
|       (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 => | ||||
|         .&&( | ||||
|           Milestones filter { t2 => | ||||
|             (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && | ||||
|             (t2.title === condition.milestone.get.get.bind) | ||||
|       } exists, condition.milestone.flatten.isDefined) && | ||||
|           } exists, | ||||
|           condition.milestone.flatten.isDefined | ||||
|         ) | ||||
|         // Priority filter | ||||
|       (Priorities filter { t2 => | ||||
|         .&&( | ||||
|           Priorities filter { t2 => | ||||
|             (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.priorityId)) && | ||||
|             (t2.priorityName === condition.priority.get.get.bind) | ||||
|       } exists, condition.priority.flatten.isDefined) && | ||||
|           } exists, | ||||
|           condition.priority.flatten.isDefined | ||||
|         ) | ||||
|         // Assignee filter | ||||
|       (t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) && | ||||
|         .&&(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) | ||||
|         // Label filter | ||||
|       (IssueLabels filter { t2 => | ||||
|         .&&( | ||||
|           IssueLabels filter { t2 => | ||||
|             (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && | ||||
|             (t2.labelId in | ||||
|               (Labels filter { t3 => | ||||
|                 (t3.byRepository(t1.userName, t1.repositoryName)) && | ||||
|                 (t3.labelName inSetBind condition.labels) | ||||
|               } map (_.labelId))) | ||||
|       } exists, condition.labels.nonEmpty) && | ||||
|           } exists, | ||||
|           condition.labels.nonEmpty | ||||
|         ) | ||||
|         // Visibility filter | ||||
|       (Repositories filter { t2 => | ||||
|         .&&( | ||||
|           Repositories filter { t2 => | ||||
|             (t2.byRepository(t1.userName, t1.repositoryName)) && | ||||
|             (t2.isPrivate === (condition.visibility == Some("private")).bind) | ||||
|       } exists, condition.visibility.nonEmpty) && | ||||
|           } exists, | ||||
|           condition.visibility.nonEmpty | ||||
|         ) | ||||
|         // Organization (group) filter | ||||
|       (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && | ||||
|         .&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) | ||||
|         // Mentioned filter | ||||
|       ((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind || | ||||
|         .&&( | ||||
|           (t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind || | ||||
|             (IssueComments filter { t2 => | ||||
|               (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind) | ||||
|       } exists), condition.mentioned.isDefined) | ||||
|             } exists), | ||||
|           condition.mentioned.isDefined | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|   def insertIssue( | ||||
| @@ -635,7 +647,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 +659,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 | ||||
|         ) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -682,8 +707,8 @@ trait IssuesService { | ||||
|         case (t1, t2) => | ||||
|           keywords | ||||
|             .map { keyword => | ||||
|               (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || | ||||
|               (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) | ||||
|               (t1.title.toLowerCase.like(s"%${likeEncode(keyword)}%", '^')) || | ||||
|               (t1.content.toLowerCase.like(s"%${likeEncode(keyword)}%", '^')) | ||||
|             } | ||||
|             .reduceLeft(_ && _) | ||||
|       } | ||||
| @@ -710,7 +735,7 @@ trait IssuesService { | ||||
|           t2.pullRequest === pullRequest.bind && | ||||
|             keywords | ||||
|               .map { query => | ||||
|                 t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') | ||||
|                 t1.content.toLowerCase.like(s"%${likeEncode(query)}%", '^') | ||||
|               } | ||||
|               .reduceLeft(_ && _) | ||||
|       } | ||||
| @@ -910,27 +935,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,19 @@ 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.service.WebHookService.WebHookPushPayload | ||||
| 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} | ||||
| @@ -25,7 +28,8 @@ trait MergeService { | ||||
|     with IssuesService | ||||
|     with RepositoryService | ||||
|     with PullRequestService | ||||
|     with WebHookPullRequestService => | ||||
|     with WebHookPullRequestService | ||||
|     with WebHookService => | ||||
|  | ||||
|   import MergeService._ | ||||
|  | ||||
| @@ -35,7 +39,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 +56,98 @@ 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, | ||||
|     repository: RepositoryInfo, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     message: String, | ||||
|     committer: PersonIdent | ||||
|   ): ObjectId = { | ||||
|     new MergeCacheInfo(git, branch, issueId).merge(message, committer) | ||||
|     loginAccount: Account, | ||||
|     settings: SystemSettings | ||||
|   )(implicit s: Session, c: JsonFormat.Context): ObjectId = { | ||||
|     val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}") | ||||
|     val afterCommitId = new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks()) | ||||
|       .merge(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) | ||||
|     callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings) | ||||
|     afterCommitId | ||||
|   } | ||||
|  | ||||
|   /** rebase to the head of the pull request branch */ | ||||
|   def rebasePullRequest( | ||||
|   def mergeWithRebase( | ||||
|     git: Git, | ||||
|     repository: RepositoryInfo, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     commits: Seq[RevCommit], | ||||
|     committer: PersonIdent | ||||
|   ): ObjectId = { | ||||
|     new MergeCacheInfo(git, branch, issueId).rebase(committer, commits) | ||||
|     loginAccount: Account, | ||||
|     settings: SystemSettings | ||||
|   )(implicit s: Session, c: JsonFormat.Context): ObjectId = { | ||||
|     val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}") | ||||
|     val afterCommitId = | ||||
|       new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks()) | ||||
|         .rebase(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress), commits) | ||||
|     callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings) | ||||
|     afterCommitId | ||||
|   } | ||||
|  | ||||
|   /** squash commits in the pull request and append it */ | ||||
|   def squashPullRequest( | ||||
|   def mergeWithSquash( | ||||
|     git: Git, | ||||
|     repository: RepositoryInfo, | ||||
|     branch: String, | ||||
|     issueId: Int, | ||||
|     message: String, | ||||
|     committer: PersonIdent | ||||
|   ): ObjectId = { | ||||
|     new MergeCacheInfo(git, branch, issueId).squash(message, committer) | ||||
|     loginAccount: Account, | ||||
|     settings: SystemSettings | ||||
|   )(implicit s: Session, c: JsonFormat.Context): ObjectId = { | ||||
|     val beforeCommitId = git.getRepository.resolve(s"refs/heads/${branch}") | ||||
|     val afterCommitId = | ||||
|       new MergeCacheInfo(git, repository.owner, repository.name, branch, issueId, getReceiveHooks()) | ||||
|         .squash(message, new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)) | ||||
|     callWebHook(git, repository, branch, beforeCommitId, afterCommitId, loginAccount, settings) | ||||
|     afterCommitId | ||||
|   } | ||||
|  | ||||
|   private def callWebHook( | ||||
|     git: Git, | ||||
|     repository: RepositoryInfo, | ||||
|     branch: String, | ||||
|     beforeCommitId: ObjectId, | ||||
|     afterCommitId: ObjectId, | ||||
|     loginAccount: Account, | ||||
|     settings: SystemSettings | ||||
|   )( | ||||
|     implicit s: Session, | ||||
|     c: JsonFormat.Context | ||||
|   ): Unit = { | ||||
|     callWebHookOf(repository.owner, repository.name, WebHook.Push, settings) { | ||||
|       getAccountByUserName(repository.owner).map { ownerAccount => | ||||
|         WebHookPushPayload( | ||||
|           git, | ||||
|           loginAccount, | ||||
|           s"refs/heads/${branch}", | ||||
|           repository, | ||||
|           git | ||||
|             .log() | ||||
|             .addRange(beforeCommitId, afterCommitId) | ||||
|             .call() | ||||
|             .asScala | ||||
|             .map { commit => | ||||
|               new JGitUtil.CommitInfo(commit) | ||||
|             } | ||||
|             .toList | ||||
|             .reverse, | ||||
|           ownerAccount, | ||||
|           oldId = beforeCommitId, | ||||
|           newId = afterCommitId | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** fetch remote branch to my repository refs/pull/{issueId}/head */ | ||||
| @@ -168,7 +229,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 +261,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 +277,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 +294,7 @@ trait MergeService { | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           pullreq.foreach { pullreq => | ||||
|           pullRequest.foreach { pullRequest => | ||||
|             callWebHookOf(localRepository.owner, localRepository.name, WebHook.Push, settings) { | ||||
|               for { | ||||
|                 ownerAccount <- getAccountByUserName(localRepository.owner) | ||||
| @@ -232,7 +302,7 @@ trait MergeService { | ||||
|                 WebHookService.WebHookPushPayload( | ||||
|                   git, | ||||
|                   loginAccount, | ||||
|                   pullreq.requestBranch, | ||||
|                   pullRequest.requestBranch, | ||||
|                   localRepository, | ||||
|                   commits, | ||||
|                   ownerAccount, | ||||
| @@ -247,6 +317,10 @@ trait MergeService { | ||||
|     }.toOption | ||||
|   } | ||||
|  | ||||
|   protected def getReceiveHooks(): Seq[ReceiveHook] = { | ||||
|     PluginRegistry().getReceiveHooks | ||||
|   } | ||||
|  | ||||
|   def mergePullRequest( | ||||
|     repository: RepositoryInfo, | ||||
|     issueId: Int, | ||||
| @@ -261,73 +335,53 @@ 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(), | ||||
|                     settings | ||||
|                   ) 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 +391,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 +413,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 +429,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 +448,7 @@ trait MergeService { | ||||
|                       updatePullRequests( | ||||
|                         repository.owner, | ||||
|                         repository.name, | ||||
|                         pullreq.branch, | ||||
|                         pullRequest.branch, | ||||
|                         loginAccount, | ||||
|                         "closed", | ||||
|                         settings | ||||
| @@ -394,6 +472,68 @@ 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], | ||||
|     settings: SystemSettings | ||||
|   )(implicit s: Session, c: JsonFormat.Context): 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, | ||||
|             pullRequest.branch, | ||||
|             issue.issueId, | ||||
|             s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message, | ||||
|             loginAccount, | ||||
|             settings | ||||
|           ) | ||||
|         ) | ||||
|       case "rebase" => | ||||
|         Some( | ||||
|           mergeWithRebase( | ||||
|             git, | ||||
|             repository, | ||||
|             pullRequest.branch, | ||||
|             issue.issueId, | ||||
|             revCommits, | ||||
|             loginAccount, | ||||
|             settings | ||||
|           ) | ||||
|         ) | ||||
|       case "squash" => | ||||
|         Some( | ||||
|           mergeWithSquash( | ||||
|             git, | ||||
|             repository, | ||||
|             pullRequest.branch, | ||||
|             issue.issueId, | ||||
|             s"${issue.title} (#${issue.issueId})\n\n" + message, | ||||
|             loginAccount, | ||||
|             settings | ||||
|           ) | ||||
|         ) | ||||
|       case _ => | ||||
|         None | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| object MergeService { | ||||
| @@ -440,18 +580,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 +604,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 | ||||
| @@ -472,23 +616,23 @@ object MergeService { | ||||
|     } | ||||
|  | ||||
|     def checkConflict(): Option[String] = { | ||||
|       checkConflictCache.getOrElse(checkConflictForce) | ||||
|       checkConflictCache().getOrElse(checkConflictForce()) | ||||
|     } | ||||
|  | ||||
|     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 +648,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 +701,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 +712,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 +756,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)) | ||||
|  | ||||
|   } | ||||
|  | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user