mirror of
				https://github.com/gitbucket/gitbucket.git
				synced 2025-10-31 10:36:05 +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 | insert_final_newline = true | ||||||
| indent_style = space | indent_style = space | ||||||
| indent_size = 2 | indent_size = 2 | ||||||
|  | end_of_line = lf | ||||||
|  |  | ||||||
|  | [*.md] | ||||||
|  | trim_trailing_whitespace = false | ||||||
|  |  | ||||||
| [*.java] | [*.java] | ||||||
| trim_trailing_whitespace = true | 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 | name: build | ||||||
|  |  | ||||||
| on: | on: [push, pull_request] | ||||||
|   push: |  | ||||||
|     branches: [ master ] |  | ||||||
|   pull_request: |  | ||||||
|     branches: [ master ] |  | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
| @@ -14,6 +10,16 @@ jobs: | |||||||
|         java: [8, 11] |         java: [8, 11] | ||||||
|     steps: |     steps: | ||||||
|     - uses: actions/checkout@v2 |     - 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 |     - name: Set up JDK | ||||||
|       uses: actions/setup-java@v1 |       uses: actions/setup-java@v1 | ||||||
|       with: |       with: | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -10,6 +10,7 @@ lib_managed/ | |||||||
| src_managed/ | src_managed/ | ||||||
| project/boot/ | project/boot/ | ||||||
| project/plugins/project/ | project/plugins/project/ | ||||||
|  | .bsp/ | ||||||
|  |  | ||||||
| # Scala-IDE specific | # Scala-IDE specific | ||||||
| .scala_dependencies | .scala_dependencies | ||||||
| @@ -28,4 +29,8 @@ project/plugins/project/ | |||||||
| # Metals specific | # Metals specific | ||||||
| .metals | .metals | ||||||
| .bloop | .bloop | ||||||
| project/metals.sbt | **/metals.sbt | ||||||
|  |  | ||||||
|  | # Visual Studio Code specific | ||||||
|  | .vscode | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,15 +1,43 @@ | |||||||
| # Changelog | # Changelog | ||||||
| All changes to the project will be documented in this file. | 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 | - All CLI options are configurable by environment variables | ||||||
| - Folding pull request files | - Folding pull request files | ||||||
| - WebHook security options | - WebHook security options | ||||||
| - Add assignee and assignees properties to some Web APIs' response | - 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 | - Bump to Scala 2.13.0 and Scalatra 2.7.0 | ||||||
| - Draft pull request | - Draft pull request | ||||||
| - Drop network installation of plugins | - 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 | - Apply default priority to pull requests | ||||||
| - Focus title after clicking issue / pull request edit button | - 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 | - Bug and security fix | ||||||
|  |  | ||||||
| ### 4.31.1 - 17 Mar 2019 | ## 4.31.1 - 17 Mar 2019 | ||||||
| - Bug fix | - Bug fix | ||||||
|  |  | ||||||
| ### 4.31.0 - 17 Mar 2019 | ## 4.31.0 - 17 Mar 2019 | ||||||
| - Docker support in CI plugin | - Docker support in CI plugin | ||||||
| - Verify GPG key signed commit | - Verify GPG key signed commit | ||||||
| - OAuth2 Token (sent as a parameter) authentication support and new APIs in Web API | - OAuth2 Token (sent as a parameter) authentication support and new APIs in Web API | ||||||
| - OGP (Open Graph protocol) support | - OGP (Open Graph protocol) support | ||||||
| - Username completion with avatars | - Username completion with avatars | ||||||
|  |  | ||||||
| ### 4.30.1 - 22 Dec 2018 | ## 4.30.1 - 22 Dec 2018 | ||||||
| - Bug fix for several WebHooks and Web API | - Bug fix for several WebHooks and Web API | ||||||
|  |  | ||||||
| ## 4.30.0 - 15 Dec 2018 | ## 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 | - Submodule links to web page | ||||||
| - Clarify close/reopen button | - Clarify close/reopen button | ||||||
|  |  | ||||||
| # 4.20.0 - 23 Dec 2017 | ## 4.20.0 - 23 Dec 2017 | ||||||
| - Squash and rebase merge strategy for pull requests | - Squash and rebase merge strategy for pull requests | ||||||
| - Quick pull request creation | - Quick pull request creation | ||||||
| - Download patch from the diff view | - 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. | - 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. | - 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 | - Fix database migration issue which happens if webhook is configured | ||||||
| - Folding pull request files | - Call push webhook when pull request is merged | ||||||
| - WebHook security options | - Show commit status at commits tab of pull request | ||||||
| - Add assignee and assignees properties to some Web APIs' response |  | ||||||
|  | ### 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. | 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 Organization = "io.github.gitbucket" | ||||||
| val Name = "gitbucket" | val Name = "gitbucket" | ||||||
| val GitBucketVersion = "4.33.0" | val GitBucketVersion = "4.35.1" | ||||||
| val ScalatraVersion = "2.7.0-RC1" | val ScalatraVersion = "2.7.1" | ||||||
| val JettyVersion = "9.4.30.v20200611" | val JettyVersion = "9.4.32.v20200930" | ||||||
| val JgitVersion = "5.8.0.202006091008-r" | val JgitVersion = "5.9.0.202009080501-r" | ||||||
|  |  | ||||||
| lazy val root = (project in file(".")) | lazy val root = (project in file(".")) | ||||||
|   .enablePlugins(SbtTwirl, ScalatraPlugin) |   .enablePlugins(SbtTwirl, ScalatraPlugin) | ||||||
| @@ -17,7 +17,7 @@ sourcesInBase := false | |||||||
| organization := Organization | organization := Organization | ||||||
| name := Name | name := Name | ||||||
| version := GitBucketVersion | version := GitBucketVersion | ||||||
| scalaVersion := "2.13.1" | scalaVersion := "2.13.3" | ||||||
|  |  | ||||||
| scalafmtOnCompile := true | scalafmtOnCompile := true | ||||||
|  |  | ||||||
| @@ -36,30 +36,28 @@ libraryDependencies ++= Seq( | |||||||
|   "org.scalatra"                    %% "scalatra"                    % ScalatraVersion, |   "org.scalatra"                    %% "scalatra"                    % ScalatraVersion, | ||||||
|   "org.scalatra"                    %% "scalatra-json"               % ScalatraVersion, |   "org.scalatra"                    %% "scalatra-json"               % ScalatraVersion, | ||||||
|   "org.scalatra"                    %% "scalatra-forms"              % ScalatraVersion, |   "org.scalatra"                    %% "scalatra-forms"              % ScalatraVersion, | ||||||
|   "org.json4s"                      %% "json4s-jackson"              % "3.6.9", |   "org.json4s"                      %% "json4s-jackson"              % "3.6.10", | ||||||
|   "commons-io"                      % "commons-io"                   % "2.7", |   "commons-io"                      % "commons-io"                   % "2.8.0", | ||||||
|   "io.github.gitbucket"             % "solidbase"                    % "1.0.3", |   "io.github.gitbucket"             % "solidbase"                    % "1.0.3", | ||||||
|   "io.github.gitbucket"             % "markedj"                      % "1.0.16", |   "io.github.gitbucket"             % "markedj"                      % "1.0.16", | ||||||
|   "org.apache.commons"              % "commons-compress"             % "1.20", |   "org.apache.commons"              % "commons-compress"             % "1.20", | ||||||
|   "org.apache.commons"              % "commons-email"                % "1.5", |   "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.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.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", |   "org.apache.tika"                 % "tika-core"                    % "1.24.1", | ||||||
|   "com.github.takezoe"              %% "blocking-slick-32"           % "0.0.12", |   "com.github.takezoe"              %% "blocking-slick-32"           % "0.0.12", | ||||||
|   "com.novell.ldap"                 % "jldap"                        % "2009-10-07", |   "com.novell.ldap"                 % "jldap"                        % "2009-10-07", | ||||||
|   "com.h2database"                  % "h2"                           % "1.4.199", |   "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", |   "org.postgresql"                  % "postgresql"                   % "42.2.6", | ||||||
|   "ch.qos.logback"                  % "logback-classic"              % "1.2.3", |   "ch.qos.logback"                  % "logback-classic"              % "1.2.3", | ||||||
|   "com.zaxxer"                      % "HikariCP"                     % "3.4.5", |   "com.zaxxer"                      % "HikariCP"                     % "3.4.5", | ||||||
|   "com.typesafe"                    % "config"                       % "1.4.0", |   "com.typesafe"                    % "config"                       % "1.4.0", | ||||||
|   "com.typesafe.akka"               %% "akka-actor"                  % "2.5.27", |  | ||||||
|   "fr.brouillard.oss.security.xhub" % "xhub4j-core"                  % "1.1.0", |   "fr.brouillard.oss.security.xhub" % "xhub4j-core"                  % "1.1.0", | ||||||
|   "com.github.bkromhout"            % "java-diff-utils"              % "2.1.1", |   "com.github.bkromhout"            % "java-diff-utils"              % "2.1.1", | ||||||
|   "org.cache2k"                     % "cache2k-all"                  % "1.2.4.Final", |   "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.12", | ||||||
|   "net.coobird"                     % "thumbnailator"                % "0.4.11", |  | ||||||
|   "com.github.zafarkhaja"           % "java-semver"                  % "0.9.0", |   "com.github.zafarkhaja"           % "java-semver"                  % "0.9.0", | ||||||
|   "com.nimbusds"                    % "oauth2-oidc-sdk"              % "5.64.4", |   "com.nimbusds"                    % "oauth2-oidc-sdk"              % "5.64.4", | ||||||
|   "org.eclipse.jetty"               % "jetty-webapp"                 % JettyVersion % "provided", |   "org.eclipse.jetty"               % "jetty-webapp"                 % JettyVersion % "provided", | ||||||
| @@ -72,11 +70,12 @@ libraryDependencies ++= Seq( | |||||||
|   "org.testcontainers"              % "postgresql"                   % "1.14.3" % "test", |   "org.testcontainers"              % "postgresql"                   % "1.14.3" % "test", | ||||||
|   "net.i2p.crypto"                  % "eddsa"                        % "0.3.0", |   "net.i2p.crypto"                  % "eddsa"                        % "0.3.0", | ||||||
|   "is.tagomor.woothee"              % "woothee-java"                 % "1.11.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 | // 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") | javacOptions in compile ++= Seq("-target", "8", "-source", "8") | ||||||
| javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" | javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml" | ||||||
|  |  | ||||||
| @@ -122,6 +121,12 @@ libraryDependencies ++= Seq( | |||||||
|   "org.eclipse.jetty" % "jetty-util"         % JettyVersion % "executable" |   "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") | val executableKey = TaskKey[File]("executable") | ||||||
| executableKey := { | executableKey := { | ||||||
|   import java.util.jar.Attributes.{Name => AttrName} |   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 | notifications:1.9.0 | ||||||
| gist:4.18.0 | gist:4.20.0 | ||||||
| emoji:4.6.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 | package gitbucket.core | ||||||
|  |  | ||||||
| import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration} | import java.io.FileOutputStream | ||||||
| import io.github.gitbucket.solidbase.model.{Version, Module} | 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 | object GitBucketCoreModule | ||||||
|     extends Module( |     extends Module( | ||||||
| @@ -65,5 +80,40 @@ object GitBucketCoreModule | |||||||
|       new Version("4.31.1"), |       new Version("4.31.1"), | ||||||
|       new Version("4.31.2"), |       new Version("4.31.2"), | ||||||
|       new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")), |       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, |   name: String, | ||||||
|   commit: ApiBranchCommit |   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._ | import org.json4s._ | ||||||
|  |  | ||||||
| /** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ | /** 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) |   def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -15,13 +19,36 @@ object ApiBranchProtection { | |||||||
|  |  | ||||||
|   def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = |   def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = | ||||||
|     ApiBranchProtection( |     ApiBranchProtection( | ||||||
|  |       url = Some( | ||||||
|  |         ApiPath( | ||||||
|  |           s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection" | ||||||
|  |         ) | ||||||
|  |       ), | ||||||
|       enabled = info.enabled, |       enabled = info.enabled, | ||||||
|       required_status_checks = Some( |       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) |   sealed class EnforcementLevel(val name: String) | ||||||
|   case object Off extends EnforcementLevel("off") |   case object Off extends EnforcementLevel("off") | ||||||
|   case object NonAdmins extends EnforcementLevel("non_admins") |   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, |   forks: Int, | ||||||
|   `private`: Boolean, |   `private`: Boolean, | ||||||
|   default_branch: String, |   default_branch: String, | ||||||
|   owner: ApiUser |   owner: ApiUser, | ||||||
|  |   has_issues: Boolean | ||||||
| ) { | ) { | ||||||
|   val id = 0 // dummy id |   val id = 0 // dummy id | ||||||
|   val forks_count = forks |   val forks_count = forks | ||||||
|   val watchers_count = watchers |   val watchers_count = watchers | ||||||
|   val url = ApiPath(s"/api/v3/repos/${full_name}") |   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 clone_url = ApiPath(s"/git/${full_name}.git") | ||||||
|   val html_url = ApiPath(s"/${full_name}") |   val html_url = ApiPath(s"/${full_name}") | ||||||
|   val ssh_url = Some(SshPath(s":${full_name}.git")) |   val ssh_url = Some(SshPath(s":${full_name}.git")) | ||||||
| @@ -39,11 +39,16 @@ object ApiRepository { | |||||||
|       forks = forkedCount, |       forks = forkedCount, | ||||||
|       `private` = repository.isPrivate, |       `private` = repository.isPrivate, | ||||||
|       default_branch = repository.defaultBranch, |       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 = |   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 = |   def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository = | ||||||
|     this(repositoryInfo, ApiUser(owner)) |     this(repositoryInfo, ApiUser(owner)) | ||||||
| @@ -57,6 +62,7 @@ object ApiRepository { | |||||||
|       forks = 0, |       forks = 0, | ||||||
|       `private` = false, |       `private` = false, | ||||||
|       default_branch = "master", |       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, |   base: String, | ||||||
|   maintainer_can_modify: Option[Boolean] |   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 WebHookService | ||||||
|     with PrioritiesService |     with PrioritiesService | ||||||
|     with RepositoryCreationService |     with RepositoryCreationService | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait AccountControllerBase extends AccountManagementControllerBase { | trait AccountControllerBase extends AccountManagementControllerBase { | ||||||
|   self: AccountService |   self: AccountService | ||||||
| @@ -81,6 +82,8 @@ trait AccountControllerBase extends AccountManagementControllerBase { | |||||||
|  |  | ||||||
|   case class PersonalTokenForm(note: String) |   case class PersonalTokenForm(note: String) | ||||||
|  |  | ||||||
|  |   case class SyntaxHighlighterThemeForm(theme: String) | ||||||
|  |  | ||||||
|   val newForm = mapping( |   val newForm = mapping( | ||||||
|     "userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), |     "userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))), | ||||||
|     "password" -> trim(label("Password", text(required, maxlength(20)))), |     "password" -> trim(label("Password", text(required, maxlength(20)))), | ||||||
| @@ -121,6 +124,10 @@ trait AccountControllerBase extends AccountManagementControllerBase { | |||||||
|     "note" -> trim(label("Token", text(required, maxlength(100)))) |     "note" -> trim(label("Token", text(required, maxlength(100)))) | ||||||
|   )(PersonalTokenForm.apply) |   )(PersonalTokenForm.apply) | ||||||
|  |  | ||||||
|  |   val syntaxHighlighterThemeForm = mapping( | ||||||
|  |     "highlighterTheme" -> trim(label("Theme", text(required))) | ||||||
|  |   )(SyntaxHighlighterThemeForm.apply) | ||||||
|  |  | ||||||
|   case class NewGroupForm( |   case class NewGroupForm( | ||||||
|     groupName: String, |     groupName: String, | ||||||
|     description: Option[String], |     description: Option[String], | ||||||
| @@ -441,6 +448,29 @@ trait AccountControllerBase extends AccountManagementControllerBase { | |||||||
|     redirect(s"/${userName}/_application") |     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 { |   get("/:userName/_hooks")(managersOnly { | ||||||
|     val userName = params("userName") |     val userName = params("userName") | ||||||
|     getAccountByUserName(userName).map { account => |     getAccountByUserName(userName).map { account => | ||||||
| @@ -521,7 +551,8 @@ trait AccountControllerBase extends AccountManagementControllerBase { | |||||||
|     val url = params("url") |     val url = params("url") | ||||||
|     val token = Some(params("token")) |     val token = Some(params("token")) | ||||||
|     val ctype = WebHookContentType.valueOf(params("ctype")) |     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 dummyPayload = { | ||||||
|       val ownerAccount = getAccountByUserName(userName).get |       val ownerAccount = getAccountByUserName(userName).get | ||||||
|       WebHookPushPayload.createDummyPayload(ownerAccount) |       WebHookPushPayload.createDummyPayload(ownerAccount) | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ class ApiController | |||||||
|     with ApiIssueCommentControllerBase |     with ApiIssueCommentControllerBase | ||||||
|     with ApiIssueControllerBase |     with ApiIssueControllerBase | ||||||
|     with ApiIssueLabelControllerBase |     with ApiIssueLabelControllerBase | ||||||
|  |     with ApiIssueMilestoneControllerBase | ||||||
|     with ApiOrganizationControllerBase |     with ApiOrganizationControllerBase | ||||||
|     with ApiPullRequestControllerBase |     with ApiPullRequestControllerBase | ||||||
|     with ApiReleaseControllerBase |     with ApiReleaseControllerBase | ||||||
| @@ -22,6 +23,7 @@ class ApiController | |||||||
|     with ApiRepositoryContentsControllerBase |     with ApiRepositoryContentsControllerBase | ||||||
|     with ApiRepositoryControllerBase |     with ApiRepositoryControllerBase | ||||||
|     with ApiRepositoryStatusControllerBase |     with ApiRepositoryStatusControllerBase | ||||||
|  |     with ApiRepositoryWebhookControllerBase | ||||||
|     with ApiUserControllerBase |     with ApiUserControllerBase | ||||||
|     with RepositoryService |     with RepositoryService | ||||||
|     with AccountService |     with AccountService | ||||||
| @@ -52,6 +54,7 @@ class ApiController | |||||||
|     with ReferrerAuthenticator |     with ReferrerAuthenticator | ||||||
|     with ReadableUsersAuthenticator |     with ReadableUsersAuthenticator | ||||||
|     with WritableUsersAuthenticator |     with WritableUsersAuthenticator | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait ApiControllerBase extends ControllerBase { | trait ApiControllerBase extends ControllerBase { | ||||||
|  |  | ||||||
|   | |||||||
| @@ -421,7 +421,7 @@ trait AccountManagementControllerBase extends ControllerBase { | |||||||
|     "new" |     "new" | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   protected def reservedNames(): Constraint = new Constraint() { |   protected def reservedNames: Constraint = new Constraint() { | ||||||
|     override def validate(name: String, value: String, messages: Messages): Option[String] = |     override def validate(name: String, value: String, messages: Messages): Option[String] = | ||||||
|       if (allReservedNames.contains(value.toLowerCase)) { |       if (allReservedNames.contains(value.toLowerCase)) { | ||||||
|         Some(s"${value} is reserved") |         Some(s"${value} is reserved") | ||||||
|   | |||||||
| @@ -21,10 +21,17 @@ class DashboardController | |||||||
|     with WebHookPullRequestService |     with WebHookPullRequestService | ||||||
|     with WebHookPullRequestReviewCommentService |     with WebHookPullRequestReviewCommentService | ||||||
|     with MilestonesService |     with MilestonesService | ||||||
|  |     with CommitStatusService | ||||||
|     with UsersAuthenticator |     with UsersAuthenticator | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait DashboardControllerBase extends ControllerBase { | 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 { |   get("/dashboard/repos")(usersOnly { | ||||||
|     val repos = getVisibleRepositories( |     val repos = getVisibleRepositories( | ||||||
| @@ -85,12 +92,13 @@ trait DashboardControllerBase extends ControllerBase { | |||||||
|     val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) |     val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName) | ||||||
|     val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name) |     val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name) | ||||||
|     val page = IssueSearchCondition.page(request) |     val page = IssueSearchCondition.page(request) | ||||||
|  |     val issues = searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, userRepos: _*) | ||||||
|  |  | ||||||
|     html.issues( |     html.issues( | ||||||
|       searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), |       issues.map(issue => (issue, None)), | ||||||
|       page, |       page, | ||||||
|       countIssue(condition.copy(state = "open"), false, userRepos: _*), |       countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, userRepos: _*), | ||||||
|       countIssue(condition.copy(state = "closed"), false, userRepos: _*), |       countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, userRepos: _*), | ||||||
|       filter match { |       filter match { | ||||||
|         case "assigned"  => condition.copy(assigned = Some(Some(userName))) |         case "assigned"  => condition.copy(assigned = Some(Some(userName))) | ||||||
|         case "mentioned" => condition.copy(mentioned = 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 condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName) | ||||||
|     val allRepos = getAllRepositories(userName) |     val allRepos = getAllRepositories(userName) | ||||||
|     val page = IssueSearchCondition.page(request) |     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( |     html.pulls( | ||||||
|       searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), |       issues.zip(status), | ||||||
|       page, |       page, | ||||||
|       countIssue(condition.copy(state = "open"), true, allRepos: _*), |       countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, allRepos: _*), | ||||||
|       countIssue(condition.copy(state = "closed"), true, allRepos: _*), |       countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, allRepos: _*), | ||||||
|       filter match { |       filter match { | ||||||
|         case "assigned"  => condition.copy(assigned = Some(Some(userName))) |         case "assigned"  => condition.copy(assigned = Some(Some(userName))) | ||||||
|         case "mentioned" => condition.copy(mentioned = Some(userName)) |         case "mentioned" => condition.copy(mentioned = Some(userName)) | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ class FileUploadController | |||||||
|     execute( |     execute( | ||||||
|       { (file, fileId) => |       { (file, fileId) => | ||||||
|         FileUtils |         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 |         session += Keys.Session.Upload(fileId) -> file.name | ||||||
|       }, |       }, | ||||||
|       FileUtil.isImage |       FileUtil.isImage | ||||||
| @@ -49,7 +49,7 @@ class FileUploadController | |||||||
|     execute( |     execute( | ||||||
|       { (file, fileId) => |       { (file, fileId) => | ||||||
|         FileUtils |         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 |         session += Keys.Session.Upload(fileId) -> file.name | ||||||
|       }, |       }, | ||||||
|       _ => true |       _ => true | ||||||
| @@ -65,7 +65,7 @@ class FileUploadController | |||||||
|             getAttachedDir(params("owner"), params("repository")), |             getAttachedDir(params("owner"), params("repository")), | ||||||
|             FileUtil.checkFilename(fileId + "." + FileUtil.getExtension(file.getName)) |             FileUtil.checkFilename(fileId + "." + FileUtil.getExtension(file.getName)) | ||||||
|           ), |           ), | ||||||
|           file.get |           file.get() | ||||||
|         ) |         ) | ||||||
|       }, |       }, | ||||||
|       _ => true |       _ => true | ||||||
| @@ -144,7 +144,7 @@ class FileUploadController | |||||||
|             { (file, fileId) => |             { (file, fileId) => | ||||||
|               FileUtils.writeByteArrayToFile( |               FileUtils.writeByteArrayToFile( | ||||||
|                 new File(getReleaseFilesDir(owner, repository), FileUtil.checkFilename(tag + "/" + fileId)), |                 new File(getReleaseFilesDir(owner, repository), FileUtil.checkFilename(tag + "/" + fileId)), | ||||||
|                 file.get |                 file.get() | ||||||
|               ) |               ) | ||||||
|             }, |             }, | ||||||
|             _ => true |             _ => true | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ class IndexController | |||||||
|     with AccessTokenService |     with AccessTokenService | ||||||
|     with AccountFederationService |     with AccountFederationService | ||||||
|     with OpenIDConnectService |     with OpenIDConnectService | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait IndexControllerBase extends ControllerBase { | trait IndexControllerBase extends ControllerBase { | ||||||
|   self: RepositoryService |   self: RepositoryService | ||||||
| @@ -78,7 +79,7 @@ trait IndexControllerBase extends ControllerBase { | |||||||
|       } |       } | ||||||
|       .getOrElse { |       .getOrElse { | ||||||
|         gitbucket.core.html.index( |         gitbucket.core.html.index( | ||||||
|           getRecentActivities(), |           getRecentPublicActivities(), | ||||||
|           getVisibleRepositories(None, withoutPhysicalInfo = true), |           getVisibleRepositories(None, withoutPhysicalInfo = true), | ||||||
|           showBannerToCreatePersonalAccessToken = false |           showBannerToCreatePersonalAccessToken = false | ||||||
|         ) |         ) | ||||||
| @@ -161,7 +162,7 @@ trait IndexControllerBase extends ControllerBase { | |||||||
|  |  | ||||||
|   get("/activities.atom") { |   get("/activities.atom") { | ||||||
|     contentType = "application/atom+xml; type=feed" |     contentType = "application/atom+xml; type=feed" | ||||||
|     xml.feed(getRecentActivities()) |     xml.feed(getRecentPublicActivities()) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   post("/sidebar-collapse") { |   post("/sidebar-collapse") { | ||||||
| @@ -212,8 +213,10 @@ trait IndexControllerBase extends ControllerBase { | |||||||
|             } |             } | ||||||
|             .map { t => |             .map { t => | ||||||
|               Map( |               Map( | ||||||
|                 "label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(t.userName)}</b> ${StringUtil |                 "label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml( | ||||||
|                   .escapeHtml(t.fullName)}", |                   StringUtil.cutTail(t.userName, 25, "...") | ||||||
|  |                 )}</b> ${StringUtil | ||||||
|  |                   .escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))}", | ||||||
|                 "value" -> t.userName |                 "value" -> t.userName | ||||||
|               ) |               ) | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ class IssuesController | |||||||
|     with WebHookPullRequestReviewCommentService |     with WebHookPullRequestReviewCommentService | ||||||
|     with CommitsService |     with CommitsService | ||||||
|     with PrioritiesService |     with PrioritiesService | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait IssuesControllerBase extends ControllerBase { | trait IssuesControllerBase extends ControllerBase { | ||||||
|   self: IssuesService |   self: IssuesService | ||||||
| @@ -111,6 +112,7 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|                 getLabels(owner, name), |                 getLabels(owner, name), | ||||||
|                 isIssueEditable(repository), |                 isIssueEditable(repository), | ||||||
|                 isIssueManageable(repository), |                 isIssueManageable(repository), | ||||||
|  |                 isIssueCommentManageable(repository), | ||||||
|                 repository |                 repository | ||||||
|               ) |               ) | ||||||
|             } |             } | ||||||
| @@ -237,8 +239,8 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|     defining(repository.owner, repository.name) { |     defining(repository.owner, repository.name) { | ||||||
|       case (owner, name) => |       case (owner, name) => | ||||||
|         getComment(owner, name, params("id")).map { comment => |         getComment(owner, name, params("id")).map { comment => | ||||||
|           if (isEditableContent(owner, name, comment.commentedUserName)) { |           if (isDeletableComment(owner, name, comment.commentedUserName)) { | ||||||
|             Ok(deleteComment(comment.issueId, comment.commentId)) |             Ok(deleteComment(repository.owner, repository.name, comment.issueId, comment.commentId)) | ||||||
|           } else Unauthorized() |           } else Unauthorized() | ||||||
|         } getOrElse NotFound() |         } getOrElse NotFound() | ||||||
|     } |     } | ||||||
| @@ -368,6 +370,9 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|             } |             } | ||||||
|           case _ => BadRequest() |           case _ => BadRequest() | ||||||
|         } |         } | ||||||
|  |         if (params("uri").nonEmpty) { | ||||||
|  |           redirect(params("uri")) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @@ -376,6 +381,9 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|       executeBatch(repository) { issueId => |       executeBatch(repository) { issueId => | ||||||
|         getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { |         getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { | ||||||
|           registerIssueLabel(repository.owner, repository.name, issueId, labelId, true) |           registerIssueLabel(repository.owner, repository.name, issueId, labelId, true) | ||||||
|  |           if (params("uri").nonEmpty) { | ||||||
|  |             redirect(params("uri")) | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } getOrElse NotFound() |     } getOrElse NotFound() | ||||||
| @@ -386,6 +394,9 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|       executeBatch(repository) { |       executeBatch(repository) { | ||||||
|         updateAssignedUserName(repository.owner, repository.name, _, value, true) |         updateAssignedUserName(repository.owner, repository.name, _, value, true) | ||||||
|       } |       } | ||||||
|  |       if (params("uri").nonEmpty) { | ||||||
|  |         redirect(params("uri")) | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
| @@ -416,6 +427,29 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|     }) getOrElse NotFound() |     }) 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 assignedUserName = (key: String) => params.get(key) filter (_.trim != "") | ||||||
|   val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) |   val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) | ||||||
|   val priorityId: 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 { |     params("from") match { | ||||||
|       case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") |       case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues") | ||||||
|       case "pulls"  => redirect(s"/${repository.owner}/${repository.name}/pulls") |       case "pulls"  => redirect(s"/${repository.owner}/${repository.name}/pulls") | ||||||
|  |       case _        => | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -432,20 +467,22 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|     defining(repository.owner, repository.name) { |     defining(repository.owner, repository.name) { | ||||||
|       case (owner, repoName) => |       case (owner, repoName) => | ||||||
|         val page = IssueSearchCondition.page(request) |         val page = IssueSearchCondition.page(request) | ||||||
|  |  | ||||||
|         // retrieve search condition |         // retrieve search condition | ||||||
|         val condition = IssueSearchCondition(request) |         val condition = IssueSearchCondition(request) | ||||||
|  |         // search issues | ||||||
|  |         val issues = | ||||||
|  |           searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, owner -> repoName) | ||||||
|  |  | ||||||
|         html.list( |         html.list( | ||||||
|           "issues", |           "issues", | ||||||
|           searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), |           issues.map(issue => (issue, None)), | ||||||
|           page, |           page, | ||||||
|           getAssignableUserNames(owner, repoName), |           getAssignableUserNames(owner, repoName), | ||||||
|           getMilestones(owner, repoName), |           getMilestones(owner, repoName), | ||||||
|           getPriorities(owner, repoName), |           getPriorities(owner, repoName), | ||||||
|           getLabels(owner, repoName), |           getLabels(owner, repoName), | ||||||
|           countIssue(condition.copy(state = "open"), false, owner -> repoName), |           countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, owner -> repoName), | ||||||
|           countIssue(condition.copy(state = "closed"), false, owner -> repoName), |           countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, owner -> repoName), | ||||||
|           condition, |           condition, | ||||||
|           repository, |           repository, | ||||||
|           isIssueEditable(repository), |           isIssueEditable(repository), | ||||||
| @@ -462,4 +499,13 @@ trait IssuesControllerBase extends ControllerBase { | |||||||
|   ): Boolean = { |   ): Boolean = { | ||||||
|     hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName |     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 | package gitbucket.core.controller | ||||||
|  |  | ||||||
| import gitbucket.core.issues.milestones.html | 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.Implicits._ | ||||||
| import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} | import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator} | ||||||
| import gitbucket.core.util.SyntaxSugars._ | import gitbucket.core.util.SyntaxSugars._ | ||||||
|  | import gitbucket.core.view.helpers.{getAssignableUserNames, getLabels, getPriorities, searchIssue} | ||||||
| import org.scalatra.forms._ | import org.scalatra.forms._ | ||||||
| import org.scalatra.i18n.Messages | import org.scalatra.i18n.Messages | ||||||
|  |  | ||||||
| @@ -13,11 +21,16 @@ class MilestonesController | |||||||
|     with MilestonesService |     with MilestonesService | ||||||
|     with RepositoryService |     with RepositoryService | ||||||
|     with AccountService |     with AccountService | ||||||
|  |     with CommitStatusService | ||||||
|     with ReferrerAuthenticator |     with ReferrerAuthenticator | ||||||
|     with WritableUsersAuthenticator |     with WritableUsersAuthenticator | ||||||
|  |  | ||||||
| trait MilestonesControllerBase extends ControllerBase { | 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]) |   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 { |   get("/:owner/:repository/issues/milestones/new")(writableUsersOnly { | ||||||
|     html.edit(None, _) |     html.edit(None, _) | ||||||
|   }) |   }) | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| package gitbucket.core.controller | package gitbucket.core.controller | ||||||
|  |  | ||||||
|  | import gitbucket.core.model.activity.DeleteBranchInfo | ||||||
| import gitbucket.core.pulls.html | import gitbucket.core.pulls.html | ||||||
| import gitbucket.core.service.CommitStatusService | import gitbucket.core.service.CommitStatusService | ||||||
| import gitbucket.core.service.MergeService | import gitbucket.core.service.MergeService | ||||||
| @@ -36,6 +37,7 @@ class PullRequestsController | |||||||
|     with MergeService |     with MergeService | ||||||
|     with ProtectedBranchService |     with ProtectedBranchService | ||||||
|     with PrioritiesService |     with PrioritiesService | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait PullRequestsControllerBase extends ControllerBase { | trait PullRequestsControllerBase extends ControllerBase { | ||||||
|   self: RepositoryService |   self: RepositoryService | ||||||
| @@ -167,10 +169,16 @@ trait PullRequestsControllerBase extends ControllerBase { | |||||||
|             val (commits, diffs) = |             val (commits, diffs) = | ||||||
|               getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo) |               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( |             html.commits( | ||||||
|               issue, |               issue, | ||||||
|               pullreq, |               pullreq, | ||||||
|               commits, |               commitsWithStatus, | ||||||
|               getPullRequestComments(owner, name, issue.issueId, commits.flatten), |               getPullRequestComments(owner, name, issue.issueId, commits.flatten), | ||||||
|               diffs.size, |               diffs.size, | ||||||
|               isManageable(repository), |               isManageable(repository), | ||||||
| @@ -217,7 +225,7 @@ trait PullRequestsControllerBase extends ControllerBase { | |||||||
|             val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) |             val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch) | ||||||
|             val mergeStatus = PullRequestService.MergeStatus( |             val mergeStatus = PullRequestService.MergeStatus( | ||||||
|               conflictMessage = conflictMessage, |               conflictMessage = conflictMessage, | ||||||
|               commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo), |               commitStatuses = getCommitStatuses(owner, name, pullreq.commitIdTo), | ||||||
|               branchProtection = branchProtection, |               branchProtection = branchProtection, | ||||||
|               branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom), |               branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom), | ||||||
|               needStatusCheck = context.loginAccount |               needStatusCheck = context.loginAccount | ||||||
| @@ -271,7 +279,8 @@ trait PullRequestsControllerBase extends ControllerBase { | |||||||
|           val userName = context.loginAccount.get.userName |           val userName = context.loginAccount.get.userName | ||||||
|           Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => |           Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||||
|             git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call() |             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( |           createComment( | ||||||
|             baseRepository.owner, |             baseRepository.owner, | ||||||
| @@ -634,20 +643,33 @@ trait PullRequestsControllerBase extends ControllerBase { | |||||||
|     defining(repository.owner, repository.name) { |     defining(repository.owner, repository.name) { | ||||||
|       case (owner, repoName) => |       case (owner, repoName) => | ||||||
|         val page = IssueSearchCondition.page(request) |         val page = IssueSearchCondition.page(request) | ||||||
|  |  | ||||||
|         // retrieve search condition |         // retrieve search condition | ||||||
|         val condition = IssueSearchCondition(request) |         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( |         gitbucket.core.issues.html.list( | ||||||
|           "pulls", |           "pulls", | ||||||
|           searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), |           issues.zip(status), | ||||||
|           page, |           page, | ||||||
|           getAssignableUserNames(owner, repoName), |           getAssignableUserNames(owner, repoName), | ||||||
|           getMilestones(owner, repoName), |           getMilestones(owner, repoName), | ||||||
|           getPriorities(owner, repoName), |           getPriorities(owner, repoName), | ||||||
|           getLabels(owner, repoName), |           getLabels(owner, repoName), | ||||||
|           countIssue(condition.copy(state = "open"), true, owner -> repoName), |           countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, owner -> repoName), | ||||||
|           countIssue(condition.copy(state = "closed"), true, owner -> repoName), |           countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, owner -> repoName), | ||||||
|           condition, |           condition, | ||||||
|           repository, |           repository, | ||||||
|           isEditable(repository), |           isEditable(repository), | ||||||
|   | |||||||
| @@ -2,7 +2,15 @@ package gitbucket.core.controller | |||||||
|  |  | ||||||
| import java.io.File | 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._ | ||||||
| import gitbucket.core.util.Directory._ | import gitbucket.core.util.Directory._ | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| @@ -22,6 +30,7 @@ class ReleaseController | |||||||
|     with ReadableUsersAuthenticator |     with ReadableUsersAuthenticator | ||||||
|     with ReferrerAuthenticator |     with ReferrerAuthenticator | ||||||
|     with WritableUsersAuthenticator |     with WritableUsersAuthenticator | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait ReleaseControllerBase extends ControllerBase { | trait ReleaseControllerBase extends ControllerBase { | ||||||
|   self: RepositoryService |   self: RepositoryService | ||||||
| @@ -119,7 +128,8 @@ trait ReleaseControllerBase extends ControllerBase { | |||||||
|         createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount) |         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}") |     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.Implicits._ | ||||||
| import gitbucket.core.util.Directory._ | import gitbucket.core.util.Directory._ | ||||||
| import gitbucket.core.model.WebHookContentType | import gitbucket.core.model.WebHookContentType | ||||||
|  | import gitbucket.core.model.activity.RenameRepositoryInfo | ||||||
| import org.scalatra.forms._ | import org.scalatra.forms._ | ||||||
| import org.scalatra.i18n.Messages | import org.scalatra.i18n.Messages | ||||||
| import org.eclipse.jgit.api.Git | import org.eclipse.jgit.api.Git | ||||||
| @@ -30,8 +31,10 @@ class RepositorySettingsController | |||||||
|     with ProtectedBranchService |     with ProtectedBranchService | ||||||
|     with CommitStatusService |     with CommitStatusService | ||||||
|     with DeployKeyService |     with DeployKeyService | ||||||
|  |     with ActivityService | ||||||
|     with OwnerAuthenticator |     with OwnerAuthenticator | ||||||
|     with UsersAuthenticator |     with UsersAuthenticator | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait RepositorySettingsControllerBase extends ControllerBase { | trait RepositorySettingsControllerBase extends ControllerBase { | ||||||
|   self: RepositoryService |   self: RepositoryService | ||||||
| @@ -40,6 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|     with ProtectedBranchService |     with ProtectedBranchService | ||||||
|     with CommitStatusService |     with CommitStatusService | ||||||
|     with DeployKeyService |     with DeployKeyService | ||||||
|  |     with ActivityService | ||||||
|     with OwnerAuthenticator |     with OwnerAuthenticator | ||||||
|     with UsersAuthenticator => |     with UsersAuthenticator => | ||||||
|  |  | ||||||
| @@ -97,9 +101,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|       "events" -> webhookEvents, |       "events" -> webhookEvents, | ||||||
|       "ctype" -> label("ctype", text()), |       "ctype" -> label("ctype", text()), | ||||||
|       "token" -> optional(trim(label("token", text(maxlength(100))))) |       "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 |   // for rename repository | ||||||
|   case class RenameRepositoryForm(repositoryName: String) |   case class RenameRepositoryForm(repositoryName: String) | ||||||
| @@ -176,14 +178,15 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   /** Branch protection for branch */ |   /** Branch protection for branch */ | ||||||
|   get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository => |   get("/:owner/:repository/settings/branches/*")(ownerOnly { repository => | ||||||
|     import gitbucket.core.api._ |     import gitbucket.core.api._ | ||||||
|     val branch = params("branch") |     val branch = params("splat") | ||||||
|  |  | ||||||
|     if (!repository.branchList.contains(branch)) { |     if (!repository.branchList.contains(branch)) { | ||||||
|       redirect(s"/${repository.owner}/${repository.name}/settings/branches") |       redirect(s"/${repository.owner}/${repository.name}/settings/branches") | ||||||
|     } else { |     } else { | ||||||
|       val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) |       val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch)) | ||||||
|       val lastWeeks = getRecentStatuesContexts( |       val lastWeeks = getRecentStatusContexts( | ||||||
|         repository.owner, |         repository.owner, | ||||||
|         repository.name, |         repository.name, | ||||||
|         Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC)) |         Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC)) | ||||||
| @@ -225,7 +228,13 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|    * Display the web hook edit page. |    * Display the web hook edit page. | ||||||
|    */ |    */ | ||||||
|   get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository => |   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) |     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. |    * Send the test request to registered web hook URLs. | ||||||
|    */ |    */ | ||||||
|   ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository => |   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) |         Array(h.getName, h.getValue) | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -267,7 +277,13 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|         val url = params("url") |         val url = params("url") | ||||||
|         val token = Some(params("token")) |         val token = Some(params("token")) | ||||||
|         val ctype = WebHookContentType.valueOf(params("ctype")) |         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 dummyPayload = { | ||||||
|           val ownerAccount = getAccountByUserName(repository.owner).get |           val ownerAccount = getAccountByUserName(repository.owner).get | ||||||
|           val commits = |           val commits = | ||||||
| @@ -371,7 +387,16 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|   post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) => |   post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) => | ||||||
|     if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) { |     if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) { | ||||||
|       if (repository.name != form.repositoryName) { |       if (repository.name != form.repositoryName) { | ||||||
|  |         // Update database and move git repository | ||||||
|         renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName) |         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}") |       redirect(s"/${repository.owner}/${form.repositoryName}") | ||||||
|     } else Forbidden() |     } else Forbidden() | ||||||
| @@ -384,7 +409,16 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|     if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) { |     if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) { | ||||||
|       // Change repository owner |       // Change repository owner | ||||||
|       if (repository.owner != form.newOwner) { |       if (repository.owner != form.newOwner) { | ||||||
|  |         // Update database and move git repository | ||||||
|         renameRepository(repository.owner, repository.name, form.newOwner, repository.name) |         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}") |       redirect(s"/${form.newOwner}/${repository.name}") | ||||||
|     } else Forbidden() |     } else Forbidden() | ||||||
| @@ -435,7 +469,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|   /** |   /** | ||||||
|    * Provides duplication check for web hook url. |    * 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] = |       override def validate(name: String, value: String, messages: Messages): Option[String] = | ||||||
|         if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) { |         if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) { | ||||||
|           Some(if (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] = { |       def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = { | ||||||
|         WebHook.Event.values.flatMap { t => |         WebHook.Event.values.flatMap { t => | ||||||
|           params.get(name + "." + t.name).map(_ => t) |           params.get(name + "." + t.name).map(_ => t) | ||||||
| @@ -480,7 +516,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|   /** |   /** | ||||||
|    * Duplicate check for the rename repository name. |    * Duplicate check for the rename repository name. | ||||||
|    */ |    */ | ||||||
|   private def renameRepositoryName: Constraint = new Constraint() { |   private def renameRepositoryName: Constraint = | ||||||
|  |     new Constraint() { | ||||||
|       override def validate( |       override def validate( | ||||||
|         name: String, |         name: String, | ||||||
|         value: 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( |       override def validate( | ||||||
|         name: String, |         name: String, | ||||||
|         value: String, |         value: String, | ||||||
| @@ -513,7 +550,8 @@ trait RepositorySettingsControllerBase extends ControllerBase { | |||||||
|   /** |   /** | ||||||
|    * Provides Constraint to validate the repository transfer user. |    * 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] = |       override def validate(name: String, value: String, messages: Messages): Option[String] = | ||||||
|         getAccountByUserName(value) match { |         getAccountByUserName(value) match { | ||||||
|           case None => Some("User does not exist.") |           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] = { |       override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = { | ||||||
|         params.getOrElse("mergeOptions", Nil) |         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) |         val mergeOptions = params.getOrElse("mergeOptions", Nil) | ||||||
|         if (mergeOptions.isEmpty) { |         if (mergeOptions.isEmpty) { | ||||||
|           Seq("mergeOptions" -> "At least one option must be enabled.") |           Seq("mergeOptions" -> "At least one option must be enabled.") | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ import java.io.{File, FileInputStream, FileOutputStream} | |||||||
|  |  | ||||||
| import scala.util.Using | import scala.util.Using | ||||||
| import javax.servlet.http.{HttpServletRequest, HttpServletResponse} | import javax.servlet.http.{HttpServletRequest, HttpServletResponse} | ||||||
| import gitbucket.core.plugin.PluginRegistry |  | ||||||
| import gitbucket.core.repo.html | import gitbucket.core.repo.html | ||||||
| import gitbucket.core.helper | import gitbucket.core.helper | ||||||
|  | import gitbucket.core.model.activity.DeleteBranchInfo | ||||||
| import gitbucket.core.service._ | import gitbucket.core.service._ | ||||||
| import gitbucket.core.service.RepositoryCommitFileService.CommitFile | import gitbucket.core.service.RepositoryCommitFileService.CommitFile | ||||||
| import gitbucket.core.util._ | import gitbucket.core.util._ | ||||||
| @@ -14,7 +14,8 @@ import gitbucket.core.util.StringUtil._ | |||||||
| import gitbucket.core.util.SyntaxSugars._ | import gitbucket.core.util.SyntaxSugars._ | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| import gitbucket.core.util.Directory._ | 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.util.JGitUtil.CommitInfo | ||||||
| import gitbucket.core.view | import gitbucket.core.view | ||||||
| import gitbucket.core.view.helpers | import gitbucket.core.view.helpers | ||||||
| @@ -60,6 +61,7 @@ class RepositoryViewerController | |||||||
|     with WebHookPullRequestService |     with WebHookPullRequestService | ||||||
|     with WebHookPullRequestReviewCommentService |     with WebHookPullRequestReviewCommentService | ||||||
|     with ProtectedBranchService |     with ProtectedBranchService | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * The repository viewer. |  * The repository viewer. | ||||||
| @@ -88,7 +90,9 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     branch: String, |     branch: String, | ||||||
|     path: String, |     path: String, | ||||||
|     uploadFiles: String, |     uploadFiles: String, | ||||||
|     message: Option[String] |     message: Option[String], | ||||||
|  |     commit: String, | ||||||
|  |     newBranch: Boolean | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   case class EditorForm( |   case class EditorForm( | ||||||
| @@ -100,7 +104,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     lineSeparator: String, |     lineSeparator: String, | ||||||
|     newFileName: String, |     newFileName: String, | ||||||
|     oldFileName: Option[String], |     oldFileName: Option[String], | ||||||
|     commit: String |     commit: String, | ||||||
|  |     newBranch: Boolean | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   case class DeleteForm( |   case class DeleteForm( | ||||||
| @@ -108,7 +113,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     path: String, |     path: String, | ||||||
|     message: Option[String], |     message: Option[String], | ||||||
|     fileName: String, |     fileName: String, | ||||||
|     commit: String |     commit: String, | ||||||
|  |     newBranch: Boolean | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   case class CommentForm( |   case class CommentForm( | ||||||
| @@ -131,6 +137,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     "path" -> trim(label("Path", text())), |     "path" -> trim(label("Path", text())), | ||||||
|     "uploadFiles" -> trim(label("Upload files", text(required))), |     "uploadFiles" -> trim(label("Upload files", text(required))), | ||||||
|     "message" -> trim(label("Message", optional(text()))), |     "message" -> trim(label("Message", optional(text()))), | ||||||
|  |     "commit" -> trim(label("Commit", text(required, conflict))), | ||||||
|  |     "newBranch" -> trim(label("New Branch", boolean())) | ||||||
|   )(UploadForm.apply) |   )(UploadForm.apply) | ||||||
|  |  | ||||||
|   val editorForm = mapping( |   val editorForm = mapping( | ||||||
| @@ -142,7 +150,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     "lineSeparator" -> trim(label("Line Separator", text(required))), |     "lineSeparator" -> trim(label("Line Separator", text(required))), | ||||||
|     "newFileName" -> trim(label("Filename", text(required))), |     "newFileName" -> trim(label("Filename", text(required))), | ||||||
|     "oldFileName" -> trim(label("Old filename", optional(text()))), |     "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) |   )(EditorForm.apply) | ||||||
|  |  | ||||||
|   val deleteForm = mapping( |   val deleteForm = mapping( | ||||||
| @@ -150,7 +159,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     "path" -> trim(label("Path", text())), |     "path" -> trim(label("Path", text())), | ||||||
|     "message" -> trim(label("Message", optional(text()))), |     "message" -> trim(label("Message", optional(text()))), | ||||||
|     "fileName" -> trim(label("Filename", text(required))), |     "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) |   )(DeleteForm.apply) | ||||||
|  |  | ||||||
|   val commentForm = mapping( |   val commentForm = mapping( | ||||||
| @@ -252,23 +262,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     val (branchName, path) = repository.splitPath(multiParams("splat").head) |     val (branchName, path) = repository.splitPath(multiParams("splat").head) | ||||||
|     val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) |     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))) { |     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { | ||||||
|       git => |       git => | ||||||
|         def getTags(sha: String): List[String] = { |  | ||||||
|           JGitUtil.getTagsOnCommit(git, sha) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         JGitUtil.getCommitLog(git, branchName, page, 30, path) match { |         JGitUtil.getCommitLog(git, branchName, page, 30, path) match { | ||||||
|           case Right((logs, hasNext)) => |           case Right((logs, hasNext)) => | ||||||
|             html.commits( |             html.commits( | ||||||
| @@ -277,34 +272,33 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|               repository, |               repository, | ||||||
|               logs |               logs | ||||||
|                 .map { |                 .map { | ||||||
|                   c => |                   commit => | ||||||
|  |                     ( | ||||||
|                       CommitInfo( |                       CommitInfo( | ||||||
|                       id = c.id, |                         id = commit.id, | ||||||
|                       shortMessage = c.shortMessage, |                         shortMessage = commit.shortMessage, | ||||||
|                       fullMessage = c.fullMessage, |                         fullMessage = commit.fullMessage, | ||||||
|                       parents = c.parents, |                         parents = commit.parents, | ||||||
|                       authorTime = c.authorTime, |                         authorTime = commit.authorTime, | ||||||
|                       authorName = c.authorName, |                         authorName = commit.authorName, | ||||||
|                       authorEmailAddress = c.authorEmailAddress, |                         authorEmailAddress = commit.authorEmailAddress, | ||||||
|                       commitTime = c.commitTime, |                         commitTime = commit.commitTime, | ||||||
|                       committerName = c.committerName, |                         committerName = commit.committerName, | ||||||
|                       committerEmailAddress = c.committerEmailAddress, |                         committerEmailAddress = commit.committerEmailAddress, | ||||||
|                       commitSign = c.commitSign, |                         commitSign = commit.commitSign, | ||||||
|                       verified = c.commitSign |                         verified = commit.commitSign.flatMap(GpgUtil.verifySign) | ||||||
|                         .flatMap { s => |                       ), | ||||||
|                           GpgUtil.verifySign(s) |                       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) |                     view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) | ||||||
|                 }, |                 }, | ||||||
|               page, |               page, | ||||||
|               hasNext, |               hasNext, | ||||||
|               hasDeveloperRole(repository.owner, repository.name, context.loginAccount), |               hasDeveloperRole(repository.owner, repository.name, context.loginAccount) | ||||||
|               getStatuses, |  | ||||||
|               getSummary, |  | ||||||
|               getTags |  | ||||||
|             ) |             ) | ||||||
|           case Left(_) => NotFound() |           case Left(_) => NotFound() | ||||||
|         } |         } | ||||||
| @@ -335,7 +329,16 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     val (branch, path) = repository.splitPath(multiParams("splat").head) |     val (branch, path) = repository.splitPath(multiParams("splat").head) | ||||||
|     val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch) |     val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch) | ||||||
|       .needStatusCheck(context.loginAccount.get.userName) |       .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) => |   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}") |       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( |       commitFiles( | ||||||
|         repository = repository, |         repository = repository, | ||||||
|       branch = form.branch, |         branch = branchName, | ||||||
|         path = form.path, |         path = form.path, | ||||||
|         files = files.toIndexedSeq, |         files = files.toIndexedSeq, | ||||||
|         message = form.message.getOrElse("Add files via upload"), |         message = form.message.getOrElse("Add files via upload"), | ||||||
| @@ -373,11 +392,6 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|             builder.finish() |             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) => |   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( |       commitFile( | ||||||
|         repository = repository, |         repository = repository, | ||||||
|       branch = form.branch, |         branch = branchName, | ||||||
|         path = form.path, |         path = form.path, | ||||||
|         newFileName = Some(form.newFileName), |         newFileName = Some(form.newFileName), | ||||||
|         oldFileName = None, |         oldFileName = None, | ||||||
| @@ -445,17 +474,28 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|         loginAccount = context.loginAccount.get, |         loginAccount = context.loginAccount.get, | ||||||
|         settings = context.settings |         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) => |   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( |       commitFile( | ||||||
|         repository = repository, |         repository = repository, | ||||||
|       branch = form.branch, |         branch = branchName, | ||||||
|         path = form.path, |         path = form.path, | ||||||
|         newFileName = Some(form.newFileName), |         newFileName = Some(form.newFileName), | ||||||
|         oldFileName = form.oldFileName, |         oldFileName = form.oldFileName, | ||||||
| @@ -470,17 +510,28 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|         loginAccount = context.loginAccount.get, |         loginAccount = context.loginAccount.get, | ||||||
|         settings = context.settings |         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) => |   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( |       commitFile( | ||||||
|         repository = repository, |         repository = repository, | ||||||
|       branch = form.branch, |         branch = branchName, | ||||||
|         path = form.path, |         path = form.path, | ||||||
|         newFileName = None, |         newFileName = None, | ||||||
|         oldFileName = Some(form.fileName), |         oldFileName = Some(form.fileName), | ||||||
| @@ -491,12 +542,61 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|         loginAccount = context.loginAccount.get, |         loginAccount = context.loginAccount.get, | ||||||
|         settings = context.settings |         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 => |   get("/:owner/:repository/raw/*")(referrersOnly { repository => | ||||||
|     val (id, path) = repository.splitPath(multiParams("splat").head) |     val (id, path) = repository.splitPath(multiParams("splat").head) | ||||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => |     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 blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository => | ||||||
|     val (id, path) = repository.splitPath(multiParams("splat").head) |     val (id, path) = repository.splitPath(multiParams("splat").head) | ||||||
|     val raw = params.get("raw").getOrElse("false").toBoolean |     val raw = params.get("raw").getOrElse("false").toBoolean | ||||||
|  |     val highlighterTheme = getSyntaxHighlighterTheme() | ||||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { |     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { | ||||||
|       git => |       git => | ||||||
|         val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) |         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), |                 hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount), | ||||||
|                 isBlame = request.paths(2) == "blame", |                 isBlame = request.paths(2) == "blame", | ||||||
|                 isLfsFile = isLfsFile(git, objectId), |                 isLfsFile = isLfsFile(git, objectId), | ||||||
|                 tabSize = info.tabSize |                 tabSize = info.tabSize, | ||||||
|  |                 highlighterTheme = highlighterTheme | ||||||
|               ) |               ) | ||||||
|             } |             } | ||||||
|         } getOrElse NotFound() |         } 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 = { |   private def isLfsFile(git: Git, objectId: ObjectId): Boolean = { | ||||||
|     JGitUtil.getObjectLoaderFromId(git, objectId)(JGitUtil.isLfsPointer).getOrElse(false) |     JGitUtil.getObjectLoaderFromId(git, objectId)(JGitUtil.isLfsPointer).getOrElse(false) | ||||||
|   } |   } | ||||||
| @@ -601,6 +714,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|                 new JGitUtil.CommitInfo(revCommit), |                 new JGitUtil.CommitInfo(revCommit), | ||||||
|                 JGitUtil.getBranchesOfCommit(git, revCommit.getName), |                 JGitUtil.getBranchesOfCommit(git, revCommit.getName), | ||||||
|                 JGitUtil.getTagsOfCommit(git, revCommit.getName), |                 JGitUtil.getTagsOfCommit(git, revCommit.getName), | ||||||
|  |                 getCommitStatusWithSummary(repository.owner, repository.name, revCommit.getName), | ||||||
|                 getCommitComments(repository.owner, repository.name, id, true), |                 getCommitComments(repository.owner, repository.name, id, true), | ||||||
|                 repository, |                 repository, | ||||||
|                 diffs, |                 diffs, | ||||||
| @@ -759,19 +873,20 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|             defaultBranch = repository.repository.defaultBranch, |             defaultBranch = repository.repository.defaultBranch, | ||||||
|             origin = repository.repository.originUserName.isEmpty |             origin = repository.repository.originUserName.isEmpty | ||||||
|           ) |           ) | ||||||
|           .sortBy(br => (br.mergeInfo.isEmpty, br.commitTime)) |           .sortBy(branch => (branch.mergeInfo.isEmpty, branch.commitTime)) | ||||||
|           .map( |           .map( | ||||||
|             br => |             branch => | ||||||
|               ( |               ( | ||||||
|                 br, |                 branch, | ||||||
|                 getPullRequestByRequestCommit( |                 getPullRequestByRequestCommit( | ||||||
|                   repository.owner, |                   repository.owner, | ||||||
|                   repository.name, |                   repository.name, | ||||||
|                   repository.repository.defaultBranch, |                   repository.repository.defaultBranch, | ||||||
|                   br.name, |                   branch.name, | ||||||
|                   br.commitId |                   branch.commitId | ||||||
|                 ), |                 ), | ||||||
|                 protectedBranches.contains(br.name) |                 protectedBranches.contains(branch.name), | ||||||
|  |                 getCommitStatusWithSummary(repository.owner, repository.name, branch.commitId) | ||||||
|             ) |             ) | ||||||
|           ) |           ) | ||||||
|           .reverse |           .reverse | ||||||
| @@ -832,7 +947,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     if (repository.repository.defaultBranch != branchName) { |     if (repository.repository.defaultBranch != branchName) { | ||||||
|       Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => |       Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||||
|         git.branchDelete().setForce(true).setBranchNames(branchName).call() |         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") |     redirect(s"/${repository.owner}/${repository.name}/branches") | ||||||
| @@ -905,10 +1021,6 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|     lazy val isValid: Boolean = fileIds.nonEmpty |     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. |    * Provides HTML of the file list. | ||||||
|    * |    * | ||||||
| @@ -930,12 +1042,19 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|                 if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) |                 if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path) | ||||||
|               val commitCount = JGitUtil.getCommitCount(git, lastModifiedCommit.getName) |               val commitCount = JGitUtil.getCommitCount(git, lastModifiedCommit.getName) | ||||||
|               // get files |               // 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 |               val parentPath = if (path == ".") Nil else path.split("/").toList | ||||||
|               // process README.md or README.markdown |               // process README | ||||||
|               val readme = files |               val readme = files // files should be sorted alphabetically. | ||||||
|                 .find { file => |                 .find { file => | ||||||
|                   !file.isDirectory && readmeFiles.contains(file.name.toLowerCase) |                   !file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase) | ||||||
|                 } |                 } | ||||||
|                 .map { file => |                 .map { file => | ||||||
|                   val path = (file.name :: parentPath.reverse).reverse |                   val path = (file.name :: parentPath.reverse).reverse | ||||||
| @@ -951,6 +1070,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { | |||||||
|                 repository, |                 repository, | ||||||
|                 if (path == ".") Nil else path.split("/").toList, // current path |                 if (path == ".") Nil else path.split("/").toList, // current path | ||||||
|                 new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit |                 new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit | ||||||
|  |                 getCommitStatusWithSummary(repository.owner, repository.name, lastModifiedCommit.getName), | ||||||
|                 commitCount, |                 commitCount, | ||||||
|                 files, |                 files, | ||||||
|                 readme, |                 readme, | ||||||
|   | |||||||
| @@ -48,7 +48,6 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase { | |||||||
|     )(RepositoryOperation.apply), |     )(RepositoryOperation.apply), | ||||||
|     "gravatar" -> trim(label("Gravatar", boolean())), |     "gravatar" -> trim(label("Gravatar", boolean())), | ||||||
|     "notification" -> trim(label("Notification", boolean())), |     "notification" -> trim(label("Notification", boolean())), | ||||||
|     "activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))), |  | ||||||
|     "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), |     "limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())), | ||||||
|     "ssh" -> mapping( |     "ssh" -> mapping( | ||||||
|       "enabled" -> trim(label("SSH access", boolean())), |       "enabled" -> trim(label("SSH access", boolean())), | ||||||
| @@ -109,7 +108,10 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase { | |||||||
|       "timeout" -> trim(label("Timeout", long(required))), |       "timeout" -> trim(label("Timeout", long(required))), | ||||||
|       "largeMaxFileSize" -> trim(label("Max file size for large file", long(required))), |       "largeMaxFileSize" -> trim(label("Max file size for large file", long(required))), | ||||||
|       "largeTimeout" -> trim(label("Timeout 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 => |   )(SystemSettings.apply).verifying { settings => | ||||||
|     Vector( |     Vector( | ||||||
|       if (settings.ssh.enabled && settings.baseUrl.isEmpty) { |       if (settings.ssh.enabled && settings.baseUrl.isEmpty) { | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package gitbucket.core.controller | package gitbucket.core.controller | ||||||
|  |  | ||||||
| import gitbucket.core.model.WebHook | import gitbucket.core.model.WebHook | ||||||
|  | import gitbucket.core.model.activity.{CreateWikiPageInfo, DeleteWikiInfo, EditWikiPageInfo} | ||||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||||
| import gitbucket.core.service.WebHookService.WebHookGollumPayload | import gitbucket.core.service.WebHookService.WebHookGollumPayload | ||||||
| import gitbucket.core.wiki.html | import gitbucket.core.wiki.html | ||||||
| @@ -13,6 +14,7 @@ import gitbucket.core.util.Directory._ | |||||||
| import org.scalatra.forms._ | import org.scalatra.forms._ | ||||||
| import org.eclipse.jgit.api.Git | import org.eclipse.jgit.api.Git | ||||||
| import org.scalatra.i18n.Messages | import org.scalatra.i18n.Messages | ||||||
|  |  | ||||||
| import scala.util.Using | import scala.util.Using | ||||||
|  |  | ||||||
| class WikiController | class WikiController | ||||||
| @@ -24,6 +26,7 @@ class WikiController | |||||||
|     with WebHookService |     with WebHookService | ||||||
|     with ReadableUsersAuthenticator |     with ReadableUsersAuthenticator | ||||||
|     with ReferrerAuthenticator |     with ReferrerAuthenticator | ||||||
|  |     with RequestCache | ||||||
|  |  | ||||||
| trait WikiControllerBase extends ControllerBase { | trait WikiControllerBase extends ControllerBase { | ||||||
|   self: WikiService |   self: WikiService | ||||||
| @@ -184,13 +187,9 @@ trait WikiControllerBase extends ControllerBase { | |||||||
|           ).foreach { |           ).foreach { | ||||||
|             commitId => |             commitId => | ||||||
|               updateLastActivityDate(repository.owner, repository.name) |               updateLastActivityDate(repository.owner, repository.name) | ||||||
|               recordEditWikiPageActivity( |               val wikiEditInfo = | ||||||
|                 repository.owner, |                 EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId) | ||||||
|                 repository.name, |               recordActivity(wikiEditInfo) | ||||||
|                 loginAccount.userName, |  | ||||||
|                 form.pageName, |  | ||||||
|                 commitId |  | ||||||
|               ) |  | ||||||
|               callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) { |               callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) { | ||||||
|                 getAccountByUserName(repository.owner).map { repositoryUser => |                 getAccountByUserName(repository.owner).map { repositoryUser => | ||||||
|                   WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount) |                   WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount) | ||||||
| @@ -228,7 +227,9 @@ trait WikiControllerBase extends ControllerBase { | |||||||
|           ).foreach { |           ).foreach { | ||||||
|             commitId => |             commitId => | ||||||
|               updateLastActivityDate(repository.owner, repository.name) |               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) { |               callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) { | ||||||
|                 getAccountByUserName(repository.owner).map { repositoryUser => |                 getAccountByUserName(repository.owner).map { repositoryUser => | ||||||
|                   WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount) |                   WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount) | ||||||
| @@ -250,14 +251,13 @@ trait WikiControllerBase extends ControllerBase { | |||||||
|       val pageName = StringUtil.urlDecode(params("page")) |       val pageName = StringUtil.urlDecode(params("page")) | ||||||
|  |  | ||||||
|       defining(context.loginAccount.get) { loginAccount => |       defining(context.loginAccount.get) { loginAccount => | ||||||
|         deleteWikiPage( |         val deleteWikiInfo = DeleteWikiInfo( | ||||||
|           repository.owner, |           repository.owner, | ||||||
|           repository.name, |           repository.name, | ||||||
|           pageName, |           loginAccount.userName, | ||||||
|           loginAccount.fullName, |           pageName | ||||||
|           loginAccount.mailAddress, |  | ||||||
|           s"Destroyed ${pageName}" |  | ||||||
|         ) |         ) | ||||||
|  |         recordActivity(deleteWikiInfo) | ||||||
|         updateLastActivityDate(repository.owner, repository.name) |         updateLastActivityDate(repository.owner, repository.name) | ||||||
|  |  | ||||||
|         redirect(s"/${repository.owner}/${repository.name}/wiki") |         redirect(s"/${repository.owner}/${repository.name}/wiki") | ||||||
|   | |||||||
| @@ -1,10 +1,13 @@ | |||||||
| package gitbucket.core.controller.api | 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.controller.ControllerBase | ||||||
| import gitbucket.core.util.Directory.getRepositoryDir | import gitbucket.core.util.Directory.getRepositoryDir | ||||||
| import gitbucket.core.util.ReferrerAuthenticator | import gitbucket.core.util.ReferrerAuthenticator | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| import org.eclipse.jgit.api.Git | 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 org.slf4j.LoggerFactory | ||||||
|  |  | ||||||
| import scala.jdk.CollectionConverters._ | import scala.jdk.CollectionConverters._ | ||||||
| @@ -17,7 +20,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase { | |||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * i. Get a reference |    * 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 => |   get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository => | ||||||
|     getRef() |     getRef() | ||||||
| @@ -55,21 +58,79 @@ trait ApiGitReferenceControllerBase extends ControllerBase { | |||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * ii. Get all references |    * 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 |    * 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 |    * 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 |    * 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.service._ | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName} | import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName} | ||||||
|  | import org.scalatra.{ActionResult, NoContent} | ||||||
|  |  | ||||||
| trait ApiIssueCommentControllerBase extends ControllerBase { | trait ApiIssueCommentControllerBase extends ControllerBase { | ||||||
|   self: AccountService |   self: AccountService | ||||||
| @@ -14,8 +15,8 @@ trait ApiIssueCommentControllerBase extends ControllerBase { | |||||||
|     with ReadableUsersAuthenticator |     with ReadableUsersAuthenticator | ||||||
|     with ReferrerAuthenticator => |     with ReferrerAuthenticator => | ||||||
|   /* |   /* | ||||||
|    * i. List comments on an issue |    * i. List issue comments for a repository | ||||||
|    * https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue |    * 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 => |   get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository => | ||||||
|     (for { |     (for { | ||||||
| @@ -30,18 +31,90 @@ trait ApiIssueCommentControllerBase extends ControllerBase { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * ii. List comments in a repository |    * ii. Get an issue comment | ||||||
|    * https://developer.github.com/v3/issues/comments/#list-comments-in-a-repository |    * 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 |    * vi. Create an issue comment | ||||||
|    * https://developer.github.com/v3/issues/comments/#get-a-single-comment |    * https://docs.github.com/en/rest/reference/issues#create-an-issue-comment | ||||||
|    */ |  | ||||||
|  |  | ||||||
|   /* |  | ||||||
|    * iv. Create a comment |  | ||||||
|    * https://developer.github.com/v3/issues/comments/#create-a-comment |  | ||||||
|    */ |    */ | ||||||
|   post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository => |   post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository => | ||||||
|     (for { |     (for { | ||||||
| @@ -64,16 +137,6 @@ trait ApiIssueCommentControllerBase extends ControllerBase { | |||||||
|     }) getOrElse NotFound() |     }) 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 = |   private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = | ||||||
|     hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName |     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.JGitUtil.CommitInfo | ||||||
| import gitbucket.core.util._ | import gitbucket.core.util._ | ||||||
| import org.eclipse.jgit.api.Git | import org.eclipse.jgit.api.Git | ||||||
| import org.scalatra.NoContent | import org.scalatra.{Conflict, MethodNotAllowed, NoContent, Ok} | ||||||
| import scala.util.Using | import scala.util.Using | ||||||
|  |  | ||||||
| import scala.jdk.CollectionConverters._ | import scala.jdk.CollectionConverters._ | ||||||
| @@ -161,8 +161,28 @@ trait ApiPullRequestControllerBase extends ControllerBase { | |||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * v. Update a pull request |    * 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 |    * 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) { |       if (checkConflict(repository.owner, repository.name, pullReq.branch, issueId).isDefined) { | ||||||
|         NoContent |         NoContent | ||||||
|       } else { |       } else { | ||||||
|         NotFound |         NotFound() | ||||||
|       } |       } | ||||||
|     }).getOrElse(NotFound) |     }).getOrElse(NotFound()) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * ix. Merge a pull request (Merge Button) |    * 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 |    * x. Labels, assignees, and milestones | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ import gitbucket.core.util.Directory._ | |||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| import gitbucket.core.util.JGitUtil.getBranches | import gitbucket.core.util.JGitUtil.getBranches | ||||||
| import org.eclipse.jgit.api.Git | import org.eclipse.jgit.api.Git | ||||||
|  | import org.scalatra.NoContent | ||||||
|  |  | ||||||
| import scala.util.Using | import scala.util.Using | ||||||
|  |  | ||||||
| trait ApiRepositoryBranchControllerBase extends ControllerBase { | trait ApiRepositoryBranchControllerBase extends ControllerBase { | ||||||
| @@ -22,7 +24,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase { | |||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * i. List branches |    * 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 => |   get("/api/v3/repos/:owner/:repository/branches")(referrersOnly { repository => | ||||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => |     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => | ||||||
| @@ -41,8 +43,8 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * ii. Get branch |    * ii. Get a branch | ||||||
|    * https://developer.github.com/v3/repos/branches/#get-branch |    * https://docs.github.com/en/rest/reference/repos#get-a-branch | ||||||
|    */ |    */ | ||||||
|   get("/api/v3/repos/:owner/:repository/branches/*")(referrersOnly { repository => |   get("/api/v3/repos/:owner/:repository/branches/*")(referrersOnly { repository => | ||||||
|     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { |     Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { | ||||||
| @@ -65,147 +67,206 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase { | |||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * iii. Get branch protection |    * 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 |    * 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 |    * v. Delete branch protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-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 |    * vii. Set admin branch protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#get-required-status-checks-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#set-admin-branch-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * vii. Update required status checks of protected branch |    * viii. Delete admin branch protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#update-required-status-checks-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#delete-admin-branch-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * viii. Remove required status checks of protected branch |    * ix. Get pull request review protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-status-checks-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#get-pull-request-review-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * ix. List required status checks contexts of protected branch |    * x. Update pull request review protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#list-required-status-checks-contexts-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#update-pull-request-review-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * x. Replace required status checks contexts of protected branch |    * xi. Delete pull request review protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#replace-required-status-checks-contexts-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#delete-pull-request-review-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xi. Add required status checks contexts of protected branch |    * xii. Get commit signature protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-status-checks-contexts-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#get-commit-signature-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xii. Remove required status checks contexts of protected branch |    * xiii. Create commit signature protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-status-checks-contexts-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#create-commit-signature-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xiii. Get pull request review enforcement of protected branch |    * xiv. Delete commit signature protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#get-pull-request-review-enforcement-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#delete-commit-signature-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xiv. Update pull request review enforcement of protected branch |    * xv. Get status checks protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#update-pull-request-review-enforcement-of-protected-branch |    * 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 |    * xvii. Remove status check protection | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-pull-request-review-enforcement-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#remove-status-check-protection | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xvi. Get required signatures of protected branch |    * xviii. Get all status check contexts | ||||||
|    * https://developer.github.com/v3/repos/branches/#get-required-signatures-of-protected-branch |    * 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 |    * xx. Set status check contexts | ||||||
|    * https://developer.github.com/v3/repos/branches/#add-required-signatures-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#set-status-check-contexts | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xviii. Remove required signatures of protected branch |    * xxi. Remove status check contexts | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-required-signatures-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#remove-status-check-contexts | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xix. Get admin enforcement of protected branch |    * xxii. Get access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#get-admin-enforcement-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#get-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xx. Add admin enforcement of protected branch |    * xxiii. Delete access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#add-admin-enforcement-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#delete-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxi. Remove admin enforcement of protected branch |    * xxiv. Get apps with access to the protected branch | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-admin-enforcement-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#get-apps-with-access-to-the-protected-branch | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxii. Get restrictions of protected branch |    * xxv. Add app access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#get-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#add-app-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxiii. Remove restrictions of protected branch |    * xxvi. Set app access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#set-app-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxiv. List team restrictions of protected branch |    * xxvii. Remove app access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#list-team-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#remove-app-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxv. Replace team restrictions of protected branch |    * xxviii. Get teams with access to the protected branch | ||||||
|    * https://developer.github.com/v3/repos/branches/#replace-team-restrictions-of-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 |    * xxix. Add team access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#add-team-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#add-team-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxvii. Remove team restrictions of protected branch |    * xxx. Set team access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-team-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#set-team-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxviii. List user restrictions of protected branch |    * xxxi. Remove team access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#list-user-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#remove-team-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxix. Replace user restrictions of protected branch |    * xxxii. Get users with access to the protected branch | ||||||
|    * https://developer.github.com/v3/repos/branches/#replace-user-restrictions-of-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 |    * xxxiii. Add user access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#add-user-restrictions-of-protected-branch |    * https://docs.github.com/en/rest/reference/repos#add-user-access-restrictions | ||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xxxi. Remove user restrictions of protected branch |    * xxxiv. Set user access restrictions | ||||||
|    * https://developer.github.com/v3/repos/branches/#remove-user-restrictions-of-protected-branch |    * 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 | 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.controller.ControllerBase | ||||||
| import gitbucket.core.service.{AccountService, RepositoryService} | import gitbucket.core.service.{AccountService, RepositoryService} | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| @@ -10,8 +10,8 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { | |||||||
|   self: RepositoryService with AccountService with ReferrerAuthenticator with OwnerAuthenticator => |   self: RepositoryService with AccountService with ReferrerAuthenticator with OwnerAuthenticator => | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * i. List collaborators |    * i. List repository collaborators | ||||||
|    * https://developer.github.com/v3/repos/collaborators/#list-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 => |   get("/api/v3/repos/:owner/:repository/collaborators")(referrersOnly { repository => | ||||||
|     // TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members. |     // 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)) |       getCollaboratorUserNames(params("owner"), params("repository")).map(u => ApiUser(getAccountByUserName(u).get)) | ||||||
|     ) |     ) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * ii. Check if a user is a collaborator |    * 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 |    * iii. Get repository permissions for a user | ||||||
|    * https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level |    * 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 |    * iv. Add a repository collaborator | ||||||
|    * https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator |    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#add-a-repository-collaborator | ||||||
|    * requested #1586 |    * requested #1586 | ||||||
|    */ |    */ | ||||||
|   put("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => |   put("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => | ||||||
| @@ -44,8 +65,8 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase { | |||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * v. Remove user as a collaborator |    * v. Remove a repository collaborator | ||||||
|    * https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator |    * https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#remove-a-repository-collaborator | ||||||
|    * requested #1586 |    * requested #1586 | ||||||
|    */ |    */ | ||||||
|   delete("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => |   delete("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository => | ||||||
|   | |||||||
| @@ -1,19 +1,20 @@ | |||||||
| package gitbucket.core.controller.api | 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.controller.ControllerBase | ||||||
| import gitbucket.core.model.Account | 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.Directory.getRepositoryDir | ||||||
| import gitbucket.core.util.Implicits._ | 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 gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName} | ||||||
| import org.eclipse.jgit.api.Git | import org.eclipse.jgit.api.Git | ||||||
| import org.eclipse.jgit.revwalk.RevWalk | import org.eclipse.jgit.revwalk.RevWalk | ||||||
|  |  | ||||||
| import scala.jdk.CollectionConverters._ | import scala.jdk.CollectionConverters._ | ||||||
| import scala.util.Using | import scala.util.Using | ||||||
|  |  | ||||||
| trait ApiRepositoryCommitControllerBase extends ControllerBase { | trait ApiRepositoryCommitControllerBase extends ControllerBase { | ||||||
|   self: AccountService with CommitsService with ReferrerAuthenticator => |   self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator => | ||||||
|   /* |   /* | ||||||
|    * i. List commits on a repository |    * i. List commits on a repository | ||||||
|    * https://developer.github.com/v3/repos/commits/#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 owner = repository.owner | ||||||
|     val name = repository.name |     val name = repository.name | ||||||
|     // TODO: The following parameters need to be implemented. [:path, :author, :since, :until] |     // 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))) { |     Using.resource(Git.open(getRepositoryDir(owner, name))) { | ||||||
|       git => |       git => | ||||||
|         val repo = git.getRepository |         val repo = git.getRepository | ||||||
| @@ -110,4 +111,22 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase { | |||||||
|    * v. Commit signature verification |    * v. Commit signature verification | ||||||
|    * https://developer.github.com/v3/repos/commits/#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 | package gitbucket.core.controller.api | ||||||
| import gitbucket.core.api.{ApiContents, ApiError, CreateAFile, JsonFormat} | import gitbucket.core.api.{ApiContents, ApiError, CreateAFile, JsonFormat} | ||||||
| import gitbucket.core.controller.ControllerBase | import gitbucket.core.controller.ControllerBase | ||||||
|  | import gitbucket.core.plugin.PluginRegistry | ||||||
| import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService} | import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService} | ||||||
| import gitbucket.core.util.Directory.getRepositoryDir | import gitbucket.core.util.Directory.getRepositoryDir | ||||||
| import gitbucket.core.util.JGitUtil.{FileInfo, getContentFromId, getFileList} | 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.view.helpers.{isRenderable, renderMarkup} | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| import org.eclipse.jgit.api.Git | import org.eclipse.jgit.api.Git | ||||||
|  |  | ||||||
| import scala.util.Using | import scala.util.Using | ||||||
|  |  | ||||||
| trait ApiRepositoryContentsControllerBase extends ControllerBase { | trait ApiRepositoryContentsControllerBase extends ControllerBase { | ||||||
|   self: ReferrerAuthenticator with WritableUsersAuthenticator with RepositoryCommitFileService => |   self: ReferrerAuthenticator with WritableUsersAuthenticator with RepositoryCommitFileService => | ||||||
|  |  | ||||||
|   /* |   /** | ||||||
|    * i. Get the README |    * i. Get a repository README | ||||||
|    * https://developer.github.com/v3/repos/contents/#get-the-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 |    * ii. Get contents | ||||||
| @@ -34,21 +50,32 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase { | |||||||
|     getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch)) |     getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch)) | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|   private def getContents(repository: RepositoryService.RepositoryInfo, path: String, refStr: String) = { |   private def getContents( | ||||||
|     def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = { |     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 { |       val (dirName, fileName) = pathStr.lastIndexOf('/') match { | ||||||
|         case -1 => |         case -1 => | ||||||
|           (".", pathStr) |           (".", pathStr) | ||||||
|         case n => |         case n => | ||||||
|           (pathStr.take(n), pathStr.drop(n + 1)) |           (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 => |     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 |       if (fileList.isEmpty) { // file or NotFound | ||||||
|         getFileInfo(git, refStr, path) |         getFileInfo(git, refStr, path, ignoreCase) | ||||||
|           .flatMap { f => |           .flatMap { f => | ||||||
|             val largeFile = params.get("large_file").exists(s => s.equals("true")) |             val largeFile = params.get("large_file").exists(s => s.equals("true")) | ||||||
|             val content = getContentFromId(git, f.id, largeFile) |             val content = getContentFromId(git, f.id, largeFile) | ||||||
|   | |||||||
| @@ -54,11 +54,15 @@ trait ApiRepositoryControllerBase extends ControllerBase { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* |   /** | ||||||
|    * iv. List all public repositories |    * iv. List all public repositories | ||||||
|    * https://developer.github.com/v3/repos/#list-all-public-repositories |    * https://developer.github.com/v3/repos/#list-public-repositories | ||||||
|    * Not implemented |  | ||||||
|    */ |    */ | ||||||
|  |   get("/api/v3/repositories") { | ||||||
|  |     JsonFormat(getPublicRepositories().map { r => | ||||||
|  |       ApiRepository(r, getAccountByUserName(r.owner).get) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * v. Create |    * v. Create | ||||||
| @@ -174,9 +178,14 @@ trait ApiRepositoryControllerBase extends ControllerBase { | |||||||
|    */ |    */ | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|    * xiii. List tags |    * xiii. List repository tags | ||||||
|    * https://developer.github.com/v3/repos/#list-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 |    * xiv. Delete a repository | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase { | |||||||
|       ref <- params.get("ref") |       ref <- params.get("ref") | ||||||
|       sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) |       sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) | ||||||
|     } yield { |     } yield { | ||||||
|       JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map { |       JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map { | ||||||
|         case (status, creator) => |         case (status, creator) => | ||||||
|           ApiCommitStatus(status, ApiUser(creator)) |           ApiCommitStatus(status, ApiUser(creator)) | ||||||
|       }) |       }) | ||||||
| @@ -73,7 +73,7 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase { | |||||||
|       owner <- getAccountByUserName(repository.owner) |       owner <- getAccountByUserName(repository.owner) | ||||||
|       sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) |       sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref) | ||||||
|     } yield { |     } yield { | ||||||
|       val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha) |       val statuses = getCommitStatusesWithCreator(repository.owner, repository.name, sha) | ||||||
|       JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) |       JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner))) | ||||||
|     }) getOrElse NotFound() |     }) 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 userName = column[String]("USER_NAME") | ||||||
|     val tokenHash = column[String]("TOKEN_HASH") |     val tokenHash = column[String]("TOKEN_HASH") | ||||||
|     val note = column[String]("NOTE") |     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( | case class AccessToken( | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ trait AccountComponent { self: Profile => | |||||||
|         groupAccount, |         groupAccount, | ||||||
|         removed, |         removed, | ||||||
|         description.? |         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 userName = column[String]("USER_NAME", O PrimaryKey) | ||||||
|     val extraMailAddress = column[String]("EXTRA_MAIL_ADDRESS", O PrimaryKey) |     val extraMailAddress = column[String]("EXTRA_MAIL_ADDRESS", O PrimaryKey) | ||||||
|     def * = |     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 issuer = column[String]("ISSUER") | ||||||
|     val subject = column[String]("SUBJECT") |     val subject = column[String]("SUBJECT") | ||||||
|     val userName = column[String]("USER_NAME") |     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] = |     def byPrimaryKey(issuer: String, subject: String): Rep[Boolean] = | ||||||
|       (this.issuer === issuer.bind) && (this.subject === subject.bind) |       (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 url = column[String]("URL") | ||||||
|     val token = column[Option[String]]("TOKEN") |     val token = column[Option[String]]("TOKEN") | ||||||
|     val ctype = column[WebHookContentType]("CTYPE") |     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) |     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 url = column[String]("URL") | ||||||
|     val event = column[WebHook.Event]("EVENT") |     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) |     def byAccountWebHook(userName: String, url: String) = (this.userName === userName.bind) && (this.url === url.bind) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,9 @@ | |||||||
| package gitbucket.core.model | 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 => | trait ActivityComponent extends TemplateComponent { self: Profile => | ||||||
|   import profile.api._ |   import profile.api._ | ||||||
|   import self._ |   import self._ | ||||||
| @@ -7,14 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile => | |||||||
|   lazy val Activities = TableQuery[Activities] |   lazy val Activities = TableQuery[Activities] | ||||||
|  |  | ||||||
|   class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { |   class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate { | ||||||
|     val activityId = column[Int]("ACTIVITY_ID", O AutoInc) |     def * = ??? | ||||||
|     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) |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -26,5 +23,5 @@ case class Activity( | |||||||
|   message: String, |   message: String, | ||||||
|   additionalInfo: Option[String], |   additionalInfo: Option[String], | ||||||
|   activityDate: java.util.Date, |   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 { |   class Collaborators(tag: Tag) extends Table[Collaborator](tag, "COLLABORATOR") with BasicTemplate { | ||||||
|     val collaboratorName = column[String]("COLLABORATOR_NAME") |     val collaboratorName = column[String]("COLLABORATOR_NAME") | ||||||
|     val role = column[String]("ROLE") |     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) = |     def byPrimaryKey(owner: String, repository: String, collaborator: String) = | ||||||
|       byRepository(owner, repository) && (collaboratorName === collaborator.bind) |       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 registeredDate = column[java.util.Date]("REGISTERED_DATE") | ||||||
|     val updatedDate = column[java.util.Date]("UPDATED_DATE") |     val updatedDate = column[java.util.Date]("UPDATED_DATE") | ||||||
|     def * = |     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 |     def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind | ||||||
|   } |   } | ||||||
| @@ -74,7 +75,7 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile => | |||||||
|         originalCommitId, |         originalCommitId, | ||||||
|         originalOldLine, |         originalOldLine, | ||||||
|         originalNewLine |         originalNewLine | ||||||
|       ) <> (CommitComment.tupled, CommitComment.unapply) |       ).<>(CommitComment.tupled, CommitComment.unapply) | ||||||
|  |  | ||||||
|     def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind |     def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile => | |||||||
|         creator, |         creator, | ||||||
|         registeredDate, |         registeredDate, | ||||||
|         updatedDate |         updatedDate | ||||||
|       ) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply) |       ).<>((CommitStatus.apply _).tupled, CommitStatus.unapply) | ||||||
|     def byPrimaryKey(id: Int) = commitStatusId === id.bind |     def byPrimaryKey(id: Int) = commitStatusId === id.bind | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ trait DeployKeyComponent extends TemplateComponent { self: Profile => | |||||||
|     val publicKey = column[String]("PUBLIC_KEY") |     val publicKey = column[String]("PUBLIC_KEY") | ||||||
|     val allowWrite = column[Boolean]("ALLOW_WRITE") |     val allowWrite = column[Boolean]("ALLOW_WRITE") | ||||||
|     def * = |     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) = |     def byPrimaryKey(userName: String, repositoryName: String, deployKeyId: Int) = | ||||||
|       (this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.deployKeyId === deployKeyId.bind) |       (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 gpgKeyId = column[Long]("GPG_KEY_ID") | ||||||
|     val title = column[String]("TITLE") |     val title = column[String]("TITLE") | ||||||
|     val publicKey = column[String]("PUBLIC_KEY") |     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) = |     def byPrimaryKey(userName: String, keyId: Int) = | ||||||
|       (this.userName === userName.bind) && (this.keyId === keyId.bind) |       (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 groupName = column[String]("GROUP_NAME", O PrimaryKey) | ||||||
|     val userName = column[String]("USER_NAME", O PrimaryKey) |     val userName = column[String]("USER_NAME", O PrimaryKey) | ||||||
|     val isManager = column[Boolean]("MANAGER") |     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, |         registeredDate, | ||||||
|         updatedDate, |         updatedDate, | ||||||
|         pullRequest |         pullRequest | ||||||
|       ) <> (Issue.tupled, Issue.unapply) |       ).<>(Issue.tupled, Issue.unapply) | ||||||
|  |  | ||||||
|     def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) |     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] |   lazy val IssueLabels = TableQuery[IssueLabels] | ||||||
|  |  | ||||||
|   class IssueLabels(tag: Tag) extends Table[IssueLabel](tag, "ISSUE_LABEL") with IssueTemplate with LabelTemplate { |   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) = |     def byPrimaryKey(owner: String, repository: String, issueId: Int, labelId: Int) = | ||||||
|       byIssue(owner, repository, issueId) && (this.labelId === labelId.bind) |       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 labelId = column[Int]("LABEL_ID", O AutoInc) | ||||||
|     override val labelName = column[String]("LABEL_NAME") |     override val labelName = column[String]("LABEL_NAME") | ||||||
|     val color = column[String]("COLOR") |     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(owner: String, repository: String, labelId: Int) = byLabel(owner, repository, labelId) | ||||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], labelId: Rep[Int]) = |     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 dueDate = column[Option[java.util.Date]]("DUE_DATE") | ||||||
|     val closedDate = column[Option[java.util.Date]]("CLOSED_DATE") |     val closedDate = column[Option[java.util.Date]]("CLOSED_DATE") | ||||||
|     def * = |     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(owner: String, repository: String, milestoneId: Int) = byMilestone(owner, repository, milestoneId) | ||||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], milestoneId: Rep[Int]) = |     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 isDefault = column[Boolean]("IS_DEFAULT") | ||||||
|     val color = column[String]("COLOR") |     val color = column[String]("COLOR") | ||||||
|     def * = |     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(owner: String, repository: String, priorityId: Int) = byPriority(owner, repository, priorityId) | ||||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = |     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = | ||||||
|   | |||||||
| @@ -70,5 +70,6 @@ trait CoreProfile | |||||||
|     with ReleaseTagComponent |     with ReleaseTagComponent | ||||||
|     with ReleaseAssetComponent |     with ReleaseAssetComponent | ||||||
|     with AccountExtraMailAddressComponent |     with AccountExtraMailAddressComponent | ||||||
|  |     with AccountPreferenceComponent | ||||||
|  |  | ||||||
| object Profile extends CoreProfile | object Profile extends CoreProfile | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile => | |||||||
|   lazy val ProtectedBranches = TableQuery[ProtectedBranches] |   lazy val ProtectedBranches = TableQuery[ProtectedBranches] | ||||||
|   class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate { |   class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate { | ||||||
|     val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN") |     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) = |     def byPrimaryKey(userName: String, repositoryName: String, branch: String) = | ||||||
|       byBranch(userName, repositoryName, branch) |       byBranch(userName, repositoryName, branch) | ||||||
|     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) = |     def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], branch: Rep[String]) = | ||||||
| @@ -20,7 +20,7 @@ trait ProtectedBranchComponent extends TemplateComponent { self: Profile => | |||||||
|       with BranchTemplate { |       with BranchTemplate { | ||||||
|     val context = column[String]("CONTEXT") |     val context = column[String]("CONTEXT") | ||||||
|     def * = |     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, |         commitIdFrom, | ||||||
|         commitIdTo, |         commitIdTo, | ||||||
|         isDraft |         isDraft | ||||||
|       ) <> (PullRequest.tupled, PullRequest.unapply) |       ).<>(PullRequest.tupled, PullRequest.unapply) | ||||||
|  |  | ||||||
|     def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = |     def byPrimaryKey(userName: String, repositoryName: String, issueId: Int) = | ||||||
|       byIssue(userName, repositoryName, issueId) |       byIssue(userName, repositoryName, issueId) | ||||||
|   | |||||||
| @@ -21,7 +21,8 @@ trait ReleaseAssetComponent extends TemplateComponent { | |||||||
|     val updatedDate = column[Date]("UPDATED_DATE") |     val updatedDate = column[Date]("UPDATED_DATE") | ||||||
|  |  | ||||||
|     def * = |     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) = |     def byPrimaryKey(owner: String, repository: String, tag: String, fileName: String) = | ||||||
|       byTag(owner, repository, tag) && (this.fileName === fileName.bind) |       byTag(owner, repository, tag) && (this.fileName === fileName.bind) | ||||||
|     def byTag(owner: String, repository: String, tag: String) = |     def byTag(owner: String, repository: String, tag: String) = | ||||||
|   | |||||||
| @@ -17,7 +17,8 @@ trait ReleaseTagComponent extends TemplateComponent { | |||||||
|     val updatedDate = column[java.util.Date]("UPDATED_DATE") |     val updatedDate = column[java.util.Date]("UPDATED_DATE") | ||||||
|  |  | ||||||
|     def * = |     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 byPrimaryKey(owner: String, repository: String, tag: String) = byTag(owner, repository, tag) | ||||||
|     def byTag(owner: String, repository: String, tag: String) = |     def byTag(owner: String, repository: String, tag: String) = | ||||||
|       byRepository(owner, repository) && (this.tag === tag.bind) |       byRepository(owner, repository) && (this.tag === tag.bind) | ||||||
|   | |||||||
| @@ -42,7 +42,8 @@ trait RepositoryComponent extends TemplateComponent { self: Profile => | |||||||
|           parentRepositoryName.? |           parentRepositoryName.? | ||||||
|         ), |         ), | ||||||
|         (issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork, mergeOptions, defaultMergeOption) |         (issuesOption, externalIssuesUrl.?, wikiOption, externalWikiUrl.?, allowFork, mergeOptions, defaultMergeOption) | ||||||
|       ).shaped <> ({ |       ).shaped.<>( | ||||||
|  |         { | ||||||
|           case (repository, options) => |           case (repository, options) => | ||||||
|             Repository( |             Repository( | ||||||
|               repository._1, |               repository._1, | ||||||
| @@ -81,7 +82,8 @@ trait RepositoryComponent extends TemplateComponent { self: Profile => | |||||||
|               ) |               ) | ||||||
|             ) |             ) | ||||||
|           ) |           ) | ||||||
|       }) |         } | ||||||
|  |       ) | ||||||
|  |  | ||||||
|     def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) |     def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -9,20 +9,26 @@ trait RepositoryWebHookComponent extends TemplateComponent { self: Profile => | |||||||
|   lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks] |   lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks] | ||||||
|  |  | ||||||
|   class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate { |   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 url = column[String]("URL") | ||||||
|     val token = column[Option[String]]("TOKEN") |     val token = column[Option[String]]("TOKEN") | ||||||
|     val ctype = column[WebHookContentType]("CTYPE") |     val ctype = column[WebHookContentType]("CTYPE") | ||||||
|     def * = |     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) |       byRepository(owner, repository) && (this.url === url.bind) | ||||||
|  |  | ||||||
|  |     def byId(id: Int) = | ||||||
|  |       (this.hookId === id.bind) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| case class RepositoryWebHook( | case class RepositoryWebHook( | ||||||
|   userName: String, |   userName: String, | ||||||
|   repositoryName: String, |   repositoryName: String, | ||||||
|  |   hookId: Int = 0, | ||||||
|   url: String, |   url: String, | ||||||
|   ctype: WebHookContentType, |   ctype: WebHookContentType, | ||||||
|   token: Option[String] |   token: Option[String] | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ trait RepositoryWebHookEventComponent extends TemplateComponent { self: Profile | |||||||
|     val url = column[String]("URL") |     val url = column[String]("URL") | ||||||
|     val event = column[WebHook.Event]("EVENT") |     val event = column[WebHook.Event]("EVENT") | ||||||
|     def * = |     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) = |     def byRepositoryWebHook(owner: String, repository: String, url: String) = | ||||||
|       byRepository(owner, repository) && (this.url === url.bind) |       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 sshKeyId = column[Int]("SSH_KEY_ID", O AutoInc) | ||||||
|     val title = column[String]("TITLE") |     val title = column[String]("TITLE") | ||||||
|     val publicKey = column[String]("PUBLIC_KEY") |     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) = |     def byPrimaryKey(userName: String, sshKeyId: Int) = | ||||||
|       (this.userName === userName.bind) && (this.sshKeyId === sshKeyId.bind) |       (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, |     implicit session: Session, | ||||||
|     context: Context |     context: Context | ||||||
|   ): Unit = () |   ): 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 closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () | ||||||
|   def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () |   def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = () | ||||||
|   def assigned( |   def assigned( | ||||||
|   | |||||||
| @@ -57,6 +57,7 @@ class PluginRegistry { | |||||||
|   private val textDecorators = new ConcurrentLinkedQueue[TextDecorator] |   private val textDecorators = new ConcurrentLinkedQueue[TextDecorator] | ||||||
|   private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider] |   private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider] | ||||||
|   suggestionProviders.add(new UserNameSuggestionProvider()) |   suggestionProviders.add(new UserNameSuggestionProvider()) | ||||||
|  |   suggestionProviders.add(new IssueSuggestionProvider()) | ||||||
|   private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]() |   private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]() | ||||||
|  |  | ||||||
|   def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo) |   def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo) | ||||||
|   | |||||||
| @@ -6,11 +6,25 @@ import profile.api._ | |||||||
|  |  | ||||||
| trait ReceiveHook { | 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 |     implicit session: Session | ||||||
|   ): Option[String] = None |   ): 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 |     implicit session: Session | ||||||
|   ): Unit = () |   ): 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 |    * 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. |    * 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 id: String = "user" | ||||||
|   override val prefix: String = "@" |   override val prefix: String = "@" | ||||||
|   override val context: Seq[String] = Seq("issues") |   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; });""" |     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 | package gitbucket.core.service | ||||||
|  |  | ||||||
| import org.slf4j.LoggerFactory | 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._ | ||||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | import gitbucket.core.model.Profile.profile.blockingApi._ | ||||||
| import gitbucket.core.model.Profile.dateColumnType | import gitbucket.core.model.Profile.dateColumnType | ||||||
| @@ -17,7 +17,9 @@ trait AccountService { | |||||||
|   def authenticate(settings: SystemSettings, userName: String, password: String)( |   def authenticate(settings: SystemSettings, userName: String, password: String)( | ||||||
|     implicit s: Session |     implicit s: Session | ||||||
|   ): Option[Account] = { |   ): Option[Account] = { | ||||||
|     val account = if (settings.ldapAuthentication) { |     val account = if (password.isEmpty) { | ||||||
|  |       None | ||||||
|  |     } else if (settings.ldapAuthentication) { | ||||||
|       ldapAuthentication(settings, userName, password) |       ldapAuthentication(settings, userName, password) | ||||||
|     } else { |     } else { | ||||||
|       defaultAuthentication(userName, password) |       defaultAuthentication(userName, password) | ||||||
| @@ -103,13 +105,13 @@ trait AccountService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] = |   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)( |   def getAccountByUserNameIgnoreCase(userName: String, includeRemoved: Boolean = false)( | ||||||
|     implicit s: Session |     implicit s: Session | ||||||
|   ): Option[Account] = |   ): Option[Account] = | ||||||
|     Accounts filter ( |     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 |     ) firstOption | ||||||
|  |  | ||||||
|   def getAccountsByUserNames(userNames: Set[String], knowns: Set[Account], includeRemoved: Boolean = false)( |   def getAccountsByUserNames(userNames: Set[String], knowns: Set[Account], includeRemoved: Boolean = false)( | ||||||
| @@ -121,7 +123,7 @@ trait AccountService { | |||||||
|       map |       map | ||||||
|     } else { |     } else { | ||||||
|       map ++ Accounts |       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 |         .list | ||||||
|         .map(a => a.userName -> a) |         .map(a => a.userName -> a) | ||||||
|         .toMap |         .toMap | ||||||
| @@ -138,15 +140,15 @@ trait AccountService { | |||||||
|             (x.map { e => |             (x.map { e => | ||||||
|                 e.extraMailAddress.toLowerCase === mailAddress.toLowerCase.bind |                 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 |       .map { case (a, e) => a } firstOption | ||||||
|  |  | ||||||
|   def getAllUsers(includeRemoved: Boolean = true, includeGroups: Boolean = true)(implicit s: Session): List[Account] = { |   def getAllUsers(includeRemoved: Boolean = true, includeGroups: Boolean = true)(implicit s: Session): List[Account] = { | ||||||
|     Accounts filter { t => |     Accounts filter { t => | ||||||
|       (1.bind === 1.bind) && |       (1.bind === 1.bind) | ||||||
|       (t.groupAccount === false.bind, !includeGroups) && |         .&&(t.groupAccount === false.bind, !includeGroups) | ||||||
|       (t.removed === false.bind, !includeRemoved) |         .&&(t.removed === false.bind, !includeRemoved) | ||||||
|     } sortBy (_.userName) list |     } sortBy (_.userName) list | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -308,6 +310,33 @@ trait AccountService { | |||||||
|       Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list.distinct |       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 | object AccountService extends AccountService | ||||||
|   | |||||||
| @@ -3,389 +3,103 @@ package gitbucket.core.service | |||||||
| import gitbucket.core.model.Activity | import gitbucket.core.model.Activity | ||||||
| import gitbucket.core.util.JGitUtil | import gitbucket.core.util.JGitUtil | ||||||
| import gitbucket.core.model.Profile._ | 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 { | trait ActivityService { | ||||||
|  |   self: RequestCache => | ||||||
|  |  | ||||||
|   def deleteOldActivities(limit: Int)(implicit s: Session): Int = { |   private implicit val formats = Serialization.formats(NoTypeHints) | ||||||
|     Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id => |  | ||||||
|       Activities.filter(_.activityId <= id.bind).delete |   private def writeLog(activity: Activity): Unit = { | ||||||
|     } getOrElse 0 |     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] = |   def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = { | ||||||
|     Activities |     if (!ActivityLog.exists()) { | ||||||
|       .join(Repositories) |       List.empty | ||||||
|       .on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName)) |  | ||||||
|       .filter { |  | ||||||
|         case (t1, t2) => |  | ||||||
|           if (isPublic) { |  | ||||||
|             (t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind) |  | ||||||
|     } else { |     } 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, |   def getRecentPublicActivities()(implicit context: Context): List[Activity] = { | ||||||
|     repositoryName: String, |     if (!ActivityLog.exists()) { | ||||||
|     activityUserName: String, |       List.empty | ||||||
|     tagName: String, |     } else { | ||||||
|     commits: List[JGitUtil.CommitInfo] |       val list = new ListBuffer[Activity] | ||||||
|   )(implicit s: Session): Unit = |       Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader => | ||||||
|     Activities insert Activity( |         var json: String = null | ||||||
|       userName, |         while (list.length < 50 && { json = reader.readLine(); json } != null) { | ||||||
|       repositoryName, |           val activity = read[Activity](json) | ||||||
|       activityUserName, |           if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName) | ||||||
|       "create_tag", |                 .map(_.isPrivate) | ||||||
|       s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]", |                 .getOrElse(true)) { | ||||||
|       None, |             list += activity | ||||||
|       currentDate |           } | ||||||
|     ) |         } | ||||||
|  |       } | ||||||
|   def recordDeleteTagActivity( |       list.toList | ||||||
|     userName: String, |     } | ||||||
|     repositoryName: String, |   } | ||||||
|     activityUserName: String, |  | ||||||
|     tagName: String, |   def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = { | ||||||
|     commits: List[JGitUtil.CommitInfo] |     if (!ActivityLog.exists()) { | ||||||
|   )(implicit s: Session): Unit = |       List.empty | ||||||
|     Activities insert Activity( |     } else { | ||||||
|       userName, |       val list = new ListBuffer[Activity] | ||||||
|       repositoryName, |       Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader => | ||||||
|       activityUserName, |         var json: String = null | ||||||
|       "delete_tag", |         while (list.length < 50 && { json = reader.readLine(); json } != null) { | ||||||
|       s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]", |           val activity = read[Activity](json) | ||||||
|       None, |           if (owners.contains(activity.userName)) { | ||||||
|       currentDate |             list += activity | ||||||
|     ) |           } else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName) | ||||||
|  |                        .map(_.isPrivate) | ||||||
|   def recordCreateBranchActivity( |                        .getOrElse(true)) { | ||||||
|     userName: String, |             list += activity | ||||||
|     repositoryName: String, |           } | ||||||
|     activityUserName: String, |         } | ||||||
|     branchName: String |       } | ||||||
|   )(implicit s: Session): Unit = |       list.toList | ||||||
|     Activities insert Activity( |     } | ||||||
|       userName, |   } | ||||||
|       repositoryName, |  | ||||||
|       activityUserName, |   def recordActivity[T <: { def toActivity: Activity }](info: T): Unit = { | ||||||
|       "create_branch", |     import scala.language.reflectiveCalls | ||||||
|       s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]", |     writeLog(info.toActivity) | ||||||
|       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 |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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] = |   def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session): Option[CommitStatus] = | ||||||
|     CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption |     CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption | ||||||
|  |  | ||||||
| @@ -55,10 +67,12 @@ trait CommitStatusService { | |||||||
|   ): Option[CommitStatus] = |   ): Option[CommitStatus] = | ||||||
|     CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind).firstOption |     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] = |   def getCommitStatuses(userName: String, repositoryName: String, sha: String)( | ||||||
|     byCommitStatues(userName, repositoryName, sha).list |     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 |     implicit s: Session | ||||||
|   ): List[String] = |   ): List[String] = | ||||||
|     CommitStatuses |     CommitStatuses | ||||||
| @@ -68,15 +82,15 @@ trait CommitStatusService { | |||||||
|       .map(_._1) |       .map(_._1) | ||||||
|       .list |       .list | ||||||
|  |  | ||||||
|   def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)( |   def getCommitStatusesWithCreator(userName: String, repositoryName: String, sha: String)( | ||||||
|     implicit s: Session |     implicit s: Session | ||||||
|   ): List[(CommitStatus, Account)] = |   ): List[(CommitStatus, Account)] = | ||||||
|     byCommitStatues(userName, repositoryName, sha) |     byCommitStatus(userName, repositoryName, sha) | ||||||
|       .join(Accounts) |       .join(Accounts) | ||||||
|       .filter { case (t, a) => t.creator === a.userName } |       .filter { case (t, a) => t.creator === a.userName } | ||||||
|       .list |       .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) |     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._ | ||||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | import gitbucket.core.model.Profile.profile.blockingApi._ | ||||||
| import gitbucket.core.model.Profile.dateColumnType | import gitbucket.core.model.Profile.dateColumnType | ||||||
|  | import gitbucket.core.model.activity.{CommitCommentInfo, PullRequestCommentInfo} | ||||||
| import gitbucket.core.plugin.PluginRegistry | import gitbucket.core.plugin.PluginRegistry | ||||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||||
| import gitbucket.core.util.Directory._ | import gitbucket.core.util.Directory._ | ||||||
| @@ -80,13 +81,9 @@ trait CommitsService { | |||||||
|       case Some(issueId) => |       case Some(issueId) => | ||||||
|         getPullRequest(repository.owner, repository.name, issueId).foreach { |         getPullRequest(repository.owner, repository.name, issueId).foreach { | ||||||
|           case (issue, pullRequest) => |           case (issue, pullRequest) => | ||||||
|             recordCommentPullRequestActivity( |             val pullRequestCommentInfo = | ||||||
|               repository.owner, |               PullRequestCommentInfo(repository.owner, repository.name, loginAccount.userName, content, issueId) | ||||||
|               repository.name, |             recordActivity(pullRequestCommentInfo) | ||||||
|               loginAccount.userName, |  | ||||||
|               issueId, |  | ||||||
|               content |  | ||||||
|             ) |  | ||||||
|             PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, content, issue, repository)) |             PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, content, issue, repository)) | ||||||
|             callPullRequestReviewCommentWebHook( |             callPullRequestReviewCommentWebHook( | ||||||
|               "create", |               "create", | ||||||
| @@ -99,13 +96,9 @@ trait CommitsService { | |||||||
|             ) |             ) | ||||||
|         } |         } | ||||||
|       case None => |       case None => | ||||||
|         recordCommentCommitActivity( |         val commitCommentInfo = | ||||||
|           repository.owner, |           CommitCommentInfo(repository.owner, repository.name, loginAccount.userName, content, commitId) | ||||||
|           repository.name, |         recordActivity(commitCommentInfo) | ||||||
|           loginAccount.userName, |  | ||||||
|           commitId, |  | ||||||
|           content |  | ||||||
|         ) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     commentId |     commentId | ||||||
|   | |||||||
| @@ -1,9 +1,18 @@ | |||||||
| package gitbucket.core.service | package gitbucket.core.service | ||||||
|  |  | ||||||
| import gitbucket.core.controller.Context | 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.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.SyntaxSugars._ | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
|  |  | ||||||
| @@ -29,25 +38,31 @@ trait HandleCommentService { | |||||||
|         case (owner, name) => |         case (owner, name) => | ||||||
|           val userName = loginAccount.userName |           val userName = loginAccount.userName | ||||||
|  |  | ||||||
|           val (action, actionActivity) = actionOpt |           actionOpt.collect { | ||||||
|             .collect { |             case "close" if !issue.closed => | ||||||
|               case "close" if (!issue.closed) => |               updateClosed(owner, name, issue.issueId, true) | ||||||
|                 true -> |             case "reopen" if issue.closed => | ||||||
|                   (Some("close") -> Some( |               updateClosed(owner, name, issue.issueId, false) | ||||||
|                     if (issue.isPullRequest) recordClosePullRequestActivity _ |  | ||||||
|                     else recordCloseIssueActivity _ |  | ||||||
|                   )) |  | ||||||
|               case "reopen" if (issue.closed) => |  | ||||||
|                 false -> |  | ||||||
|                   (Some("reopen") -> Some( |  | ||||||
|                     if (issue.isPullRequest) recordReopenPullRequestActivity _ |  | ||||||
|                     else recordReopenIssueActivity _ |  | ||||||
|                   )) |  | ||||||
|           } |           } | ||||||
|             .map { |  | ||||||
|               case (closed, t) => |           val (action, _) = actionOpt | ||||||
|                 updateClosed(owner, name, issue.issueId, closed) |             .collect { | ||||||
|                 t |               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) |             .getOrElse(None -> None) | ||||||
|  |  | ||||||
| @@ -68,8 +83,12 @@ trait HandleCommentService { | |||||||
|               ) |               ) | ||||||
|  |  | ||||||
|               // record comment activity |               // record comment activity | ||||||
|               if (issue.isPullRequest) recordCommentPullRequestActivity(owner, name, userName, issue.issueId, content) |               val commentInfo = if (issue.isPullRequest) { | ||||||
|               else recordCommentIssueActivity(owner, name, userName, issue.issueId, content) |                 PullRequestCommentInfo(owner, name, userName, content, issue.issueId) | ||||||
|  |               } else { | ||||||
|  |                 IssueCommentInfo(owner, name, userName, content, issue.issueId) | ||||||
|  |               } | ||||||
|  |               recordActivity(commentInfo) | ||||||
|  |  | ||||||
|               // extract references and create refer comment |               // extract references and create refer comment | ||||||
|               createReferComment(owner, name, issue, content, loginAccount) |               createReferComment(owner, name, issue, content, loginAccount) | ||||||
| @@ -77,10 +96,6 @@ trait HandleCommentService { | |||||||
|               id |               id | ||||||
|           } |           } | ||||||
|  |  | ||||||
|           actionActivity.foreach { f => |  | ||||||
|             f(owner, name, userName, issue.issueId, issue.title) |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           // call web hooks |           // call web hooks | ||||||
|           action match { |           action match { | ||||||
|             case None => |             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.controller.Context | ||||||
| import gitbucket.core.model.{Account, Issue} | import gitbucket.core.model.{Account, Issue} | ||||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | import gitbucket.core.model.Profile.profile.blockingApi._ | ||||||
|  | import gitbucket.core.model.activity.CreateIssueInfo | ||||||
| import gitbucket.core.plugin.PluginRegistry | import gitbucket.core.plugin.PluginRegistry | ||||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||||
| import gitbucket.core.util.Implicits._ | import gitbucket.core.util.Implicits._ | ||||||
| @@ -51,7 +52,8 @@ trait IssueCreationService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // record activity |     // record activity | ||||||
|     recordCreateIssueActivity(owner, name, userName, issueId, title) |     val createIssueInfo = CreateIssueInfo(owner, name, userName, issueId, title) | ||||||
|  |     recordActivity(createIssueInfo) | ||||||
|  |  | ||||||
|     // extract references and create refer comment |     // extract references and create refer comment | ||||||
|     createReferComment(owner, name, issue, title + " " + body.getOrElse(""), loginAccount) |     createReferComment(owner, name, issue, title + " " + body.getOrElse(""), loginAccount) | ||||||
| @@ -72,6 +74,13 @@ trait IssueCreationService { | |||||||
|     hasDeveloperRole(repository.owner, repository.name, context.loginAccount) |     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. |    * Tests whether an logged-in user can post issues. | ||||||
|    */ |    */ | ||||||
|   | |||||||
| @@ -31,6 +31,9 @@ trait IssuesService { | |||||||
|       Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption |       Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption | ||||||
|     else None |     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) = |   def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) = | ||||||
|     IssueComments filter (_.byIssue(owner, repository, issueId)) sortBy (_.commentId asc) list |     IssueComments filter (_.byIssue(owner, repository, issueId)) sortBy (_.commentId asc) list | ||||||
|  |  | ||||||
| @@ -68,6 +71,20 @@ trait IssuesService { | |||||||
|     else None |     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] = { |   def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session): List[Label] = { | ||||||
|     IssueLabels |     IssueLabels | ||||||
|       .join(Labels) |       .join(Labels) | ||||||
| @@ -90,14 +107,14 @@ trait IssuesService { | |||||||
|    * Returns the count of the search result against  issues. |    * Returns the count of the search result against  issues. | ||||||
|    * |    * | ||||||
|    * @param condition the search condition |    * @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 |    * @param repos Tuple of the repository owner and the repository name | ||||||
|    * @return the count of the search result |    * @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 |     implicit s: Session | ||||||
|   ): Int = { |   ): 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] |     filterUser: Map[String, String] | ||||||
|   )(implicit s: Session): Map[String, Int] = { |   )(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) |       .join(IssueLabels) | ||||||
|       .on { |       .on { | ||||||
|         case t1 ~ t2 => |         case t1 ~ t2 => | ||||||
| @@ -153,7 +170,7 @@ trait IssuesService { | |||||||
|     filterUser: Map[String, String] |     filterUser: Map[String, String] | ||||||
|   )(implicit s: Session): Map[String, Int] = { |   )(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) |       .join(Priorities) | ||||||
|       .on { |       .on { | ||||||
|         case t1 ~ t2 => |         case t1 ~ t2 => | ||||||
| @@ -171,42 +188,11 @@ trait IssuesService { | |||||||
|       .toMap |       .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. |    * Returns the search result against issues. | ||||||
|    * |    * | ||||||
|    * @param condition the search condition |    * @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 offset the offset for pagination | ||||||
|    * @param limit the limit for pagination |    * @param limit the limit for pagination | ||||||
|    * @param repos Tuple of the repository owner and the repository name |    * @param repos Tuple of the repository owner and the repository name | ||||||
| @@ -214,13 +200,13 @@ trait IssuesService { | |||||||
|    */ |    */ | ||||||
|   def searchIssue( |   def searchIssue( | ||||||
|     condition: IssueSearchCondition, |     condition: IssueSearchCondition, | ||||||
|     pullRequest: Boolean, |     searchOption: IssueSearchOption, | ||||||
|     offset: Int, |     offset: Int, | ||||||
|     limit: Int, |     limit: Int, | ||||||
|     repos: (String, String)* |     repos: (String, String)* | ||||||
|   )(implicit s: Session): List[IssueInfo] = { |   )(implicit s: Session): List[IssueInfo] = { | ||||||
|     // get issues and comment count and labels |     // 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) |       .joinLeft(IssueLabels) | ||||||
|       .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } |       .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } | ||||||
|       .joinLeft(Labels) |       .joinLeft(Labels) | ||||||
| @@ -229,9 +215,11 @@ trait IssuesService { | |||||||
|       .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } |       .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } | ||||||
|       .joinLeft(Priorities) |       .joinLeft(Priorities) | ||||||
|       .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) } |       .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 { |       .map { | ||||||
|         case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => |         case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => | ||||||
|           ( |           ( | ||||||
|             t1, |             t1, | ||||||
|             t2.commentCount, |             t2.commentCount, | ||||||
| @@ -239,7 +227,8 @@ trait IssuesService { | |||||||
|             t4.map(_.labelName), |             t4.map(_.labelName), | ||||||
|             t4.map(_.color), |             t4.map(_.color), | ||||||
|             t5.map(_.title), |             t5.map(_.title), | ||||||
|             t6.map(_.priorityName) |             t6.map(_.priorityName), | ||||||
|  |             t7.map(_.commitIdTo) | ||||||
|           ) |           ) | ||||||
|       } |       } | ||||||
|       .list |       .list | ||||||
| @@ -249,7 +238,7 @@ trait IssuesService { | |||||||
|  |  | ||||||
|     result.map { issues => |     result.map { issues => | ||||||
|       issues.head match { |       issues.head match { | ||||||
|         case (issue, commentCount, _, _, _, milestone, priority) => |         case (issue, commentCount, _, _, _, milestone, priority, commitId) => | ||||||
|           IssueInfo( |           IssueInfo( | ||||||
|             issue, |             issue, | ||||||
|             issues.flatMap { t => |             issues.flatMap { t => | ||||||
| @@ -258,7 +247,7 @@ trait IssuesService { | |||||||
|             milestone, |             milestone, | ||||||
|             priority, |             priority, | ||||||
|             commentCount, |             commentCount, | ||||||
|             getCommitStatues(issue.userName, issue.repositoryName, issue.issueId) |             commitId | ||||||
|           ) |           ) | ||||||
|       } |       } | ||||||
|     } toList |     } toList | ||||||
| @@ -271,7 +260,7 @@ trait IssuesService { | |||||||
|     implicit s: Session |     implicit s: Session | ||||||
|   ): List[(Issue, Account, Option[Account])] = { |   ): List[(Issue, Account, Option[Account])] = { | ||||||
|     // get issues and comment count and labels |     // get issues and comment count and labels | ||||||
|     searchIssueQueryBase(condition, false, offset, limit, repos) |     searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos) | ||||||
|       .join(Accounts) |       .join(Accounts) | ||||||
|       .on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName } |       .on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName } | ||||||
|       .joinLeft(Accounts) |       .joinLeft(Accounts) | ||||||
| @@ -288,7 +277,7 @@ trait IssuesService { | |||||||
|     implicit s: Session |     implicit s: Session | ||||||
|   ): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = { |   ): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = { | ||||||
|     // get issues and comment count and labels |     // get issues and comment count and labels | ||||||
|     searchIssueQueryBase(condition, true, offset, limit, repos) |     searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos) | ||||||
|       .join(PullRequests) |       .join(PullRequests) | ||||||
|       .on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } |       .on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) } | ||||||
|       .join(Repositories) |       .join(Repositories) | ||||||
| @@ -306,12 +295,12 @@ trait IssuesService { | |||||||
|  |  | ||||||
|   private def searchIssueQueryBase( |   private def searchIssueQueryBase( | ||||||
|     condition: IssueSearchCondition, |     condition: IssueSearchCondition, | ||||||
|     pullRequest: Boolean, |     searchOption: IssueSearchOption, | ||||||
|     offset: Int, |     offset: Int, | ||||||
|     limit: Int, |     limit: Int, | ||||||
|     repos: Seq[(String, String)] |     repos: Seq[(String, String)] | ||||||
|   )(implicit s: Session) = |   )(implicit s: Session) = | ||||||
|     searchIssueQuery(repos, condition, pullRequest) |     searchIssueQuery(repos, condition, searchOption) | ||||||
|       .join(IssueOutline) |       .join(IssueOutline) | ||||||
|       .on { (t1, t2) => |       .on { (t1, t2) => | ||||||
|         t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) |         t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) | ||||||
| @@ -349,7 +338,11 @@ trait IssuesService { | |||||||
|   /** |   /** | ||||||
|    * Assembles query for conditional issue searching. |    * 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 |     implicit s: Session | ||||||
|   ) = |   ) = | ||||||
|     Issues filter { t1 => |     Issues filter { t1 => | ||||||
| @@ -358,45 +351,64 @@ trait IssuesService { | |||||||
|        } else { |        } else { | ||||||
|          ((t1.userName ++ "/" ++ t1.repositoryName) inSetBind (repos.map { case (owner, repo) => s"$owner/$repo" })) |          ((t1.userName ++ "/" ++ t1.repositoryName) inSetBind (repos.map { case (owner, repo) => s"$owner/$repo" })) | ||||||
|        }) && |        }) && | ||||||
|       (t1.closed === (condition.state == "closed").bind) && |       (t1.closed === (condition.state == "closed").bind) | ||||||
|       (t1.milestoneId.? isEmpty, condition.milestone == Some(None)) && |         .&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) | ||||||
|       (t1.priorityId.? isEmpty, condition.priority == Some(None)) && |         .&&(t1.priorityId.? isEmpty, condition.priority == Some(None)) | ||||||
|       (t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) && |         .&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) | ||||||
|       (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && |         .&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && | ||||||
|       (t1.pullRequest === pullRequest.bind) && |       (searchOption match { | ||||||
|  |         case IssueSearchOption.Issues       => t1.pullRequest === false | ||||||
|  |         case IssueSearchOption.PullRequests => t1.pullRequest === true | ||||||
|  |         case IssueSearchOption.Both         => t1.pullRequest === false || t1.pullRequest === true | ||||||
|  |       }) | ||||||
|       // Milestone filter |       // Milestone filter | ||||||
|       (Milestones filter { t2 => |         .&&( | ||||||
|  |           Milestones filter { t2 => | ||||||
|             (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && |             (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && | ||||||
|             (t2.title === condition.milestone.get.get.bind) |             (t2.title === condition.milestone.get.get.bind) | ||||||
|       } exists, condition.milestone.flatten.isDefined) && |           } exists, | ||||||
|  |           condition.milestone.flatten.isDefined | ||||||
|  |         ) | ||||||
|         // Priority filter |         // Priority filter | ||||||
|       (Priorities filter { t2 => |         .&&( | ||||||
|  |           Priorities filter { t2 => | ||||||
|             (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.priorityId)) && |             (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.priorityId)) && | ||||||
|             (t2.priorityName === condition.priority.get.get.bind) |             (t2.priorityName === condition.priority.get.get.bind) | ||||||
|       } exists, condition.priority.flatten.isDefined) && |           } exists, | ||||||
|  |           condition.priority.flatten.isDefined | ||||||
|  |         ) | ||||||
|         // Assignee filter |         // 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 |         // Label filter | ||||||
|       (IssueLabels filter { t2 => |         .&&( | ||||||
|  |           IssueLabels filter { t2 => | ||||||
|             (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && |             (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && | ||||||
|             (t2.labelId in |             (t2.labelId in | ||||||
|               (Labels filter { t3 => |               (Labels filter { t3 => | ||||||
|                 (t3.byRepository(t1.userName, t1.repositoryName)) && |                 (t3.byRepository(t1.userName, t1.repositoryName)) && | ||||||
|                 (t3.labelName inSetBind condition.labels) |                 (t3.labelName inSetBind condition.labels) | ||||||
|               } map (_.labelId))) |               } map (_.labelId))) | ||||||
|       } exists, condition.labels.nonEmpty) && |           } exists, | ||||||
|  |           condition.labels.nonEmpty | ||||||
|  |         ) | ||||||
|         // Visibility filter |         // Visibility filter | ||||||
|       (Repositories filter { t2 => |         .&&( | ||||||
|  |           Repositories filter { t2 => | ||||||
|             (t2.byRepository(t1.userName, t1.repositoryName)) && |             (t2.byRepository(t1.userName, t1.repositoryName)) && | ||||||
|             (t2.isPrivate === (condition.visibility == Some("private")).bind) |             (t2.isPrivate === (condition.visibility == Some("private")).bind) | ||||||
|       } exists, condition.visibility.nonEmpty) && |           } exists, | ||||||
|  |           condition.visibility.nonEmpty | ||||||
|  |         ) | ||||||
|         // Organization (group) filter |         // Organization (group) filter | ||||||
|       (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && |         .&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) | ||||||
|         // Mentioned filter |         // 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 => |             (IssueComments filter { t2 => | ||||||
|               (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind) |               (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind) | ||||||
|       } exists), condition.mentioned.isDefined) |             } exists), | ||||||
|  |           condition.mentioned.isDefined | ||||||
|  |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   def insertIssue( |   def insertIssue( | ||||||
| @@ -635,7 +647,10 @@ trait IssuesService { | |||||||
|     IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate) |     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) |     Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate) | ||||||
|     IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match { |     IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match { | ||||||
|       case Some(c) if c.action == "reopen_comment" => |       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") |         IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close") | ||||||
|       case Some(_) => |       case Some(_) => | ||||||
|         IssueComments.filter(_.byPrimaryKey(commentId)).delete |         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) => |         case (t1, t2) => | ||||||
|           keywords |           keywords | ||||||
|             .map { keyword => |             .map { keyword => | ||||||
|               (t1.title.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) || |               (t1.title.toLowerCase.like(s"%${likeEncode(keyword)}%", '^')) || | ||||||
|               (t1.content.toLowerCase like (s"%${likeEncode(keyword)}%", '^')) |               (t1.content.toLowerCase.like(s"%${likeEncode(keyword)}%", '^')) | ||||||
|             } |             } | ||||||
|             .reduceLeft(_ && _) |             .reduceLeft(_ && _) | ||||||
|       } |       } | ||||||
| @@ -710,7 +735,7 @@ trait IssuesService { | |||||||
|           t2.pullRequest === pullRequest.bind && |           t2.pullRequest === pullRequest.bind && | ||||||
|             keywords |             keywords | ||||||
|               .map { query => |               .map { query => | ||||||
|                 t1.content.toLowerCase like (s"%${likeEncode(query)}%", '^') |                 t1.content.toLowerCase.like(s"%${likeEncode(query)}%", '^') | ||||||
|               } |               } | ||||||
|               .reduceLeft(_ && _) |               .reduceLeft(_ && _) | ||||||
|       } |       } | ||||||
| @@ -910,27 +935,46 @@ object IssuesService { | |||||||
|         param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) |         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) = { |     def page(request: HttpServletRequest) = { | ||||||
|       PaginationHelper.page(param(request, "page")) |       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( |   case class IssueInfo( | ||||||
|     issue: Issue, |     issue: Issue, | ||||||
|     labels: List[Label], |     labels: List[Label], | ||||||
|     milestone: Option[String], |     milestone: Option[String], | ||||||
|     priority: Option[String], |     priority: Option[String], | ||||||
|     commentCount: Int, |     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.api.JsonFormat | ||||||
| import gitbucket.core.controller.Context | import gitbucket.core.controller.Context | ||||||
| import gitbucket.core.model.{Account, PullRequest, WebHook} | import gitbucket.core.model.{Account, Issue, PullRequest, WebHook} | ||||||
| import gitbucket.core.plugin.PluginRegistry | import gitbucket.core.plugin.{PluginRegistry, ReceiveHook} | ||||||
| import gitbucket.core.service.RepositoryService.RepositoryInfo | import gitbucket.core.service.RepositoryService.RepositoryInfo | ||||||
| import gitbucket.core.util.Directory._ | import gitbucket.core.util.Directory._ | ||||||
| import gitbucket.core.util.{JGitUtil, LockUtil} | import gitbucket.core.util.{JGitUtil, LockUtil} | ||||||
| import gitbucket.core.model.Profile.profile.blockingApi._ | 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.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.merge.{MergeStrategy, Merger, RecursiveMerger} | ||||||
| import org.eclipse.jgit.api.Git | 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.errors.NoMergeBaseException | ||||||
| import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository} | import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository} | ||||||
| import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} | import org.eclipse.jgit.revwalk.{RevCommit, RevWalk} | ||||||
| @@ -25,7 +28,8 @@ trait MergeService { | |||||||
|     with IssuesService |     with IssuesService | ||||||
|     with RepositoryService |     with RepositoryService | ||||||
|     with PullRequestService |     with PullRequestService | ||||||
|     with WebHookPullRequestService => |     with WebHookPullRequestService | ||||||
|  |     with WebHookService => | ||||||
|  |  | ||||||
|   import MergeService._ |   import MergeService._ | ||||||
|  |  | ||||||
| @@ -35,7 +39,7 @@ trait MergeService { | |||||||
|    */ |    */ | ||||||
|   def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = { |   def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = { | ||||||
|     Using.resource(Git.open(getRepositoryDir(userName, repositoryName))) { git => |     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 |     issueId: Int | ||||||
|   ): Option[Option[String]] = { |   ): Option[Option[String]] = { | ||||||
|     Using.resource(Git.open(getRepositoryDir(userName, repositoryName))) { git => |     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 */ |   /** merge the pull request with a merge commit */ | ||||||
|   def mergePullRequest( |   def mergeWithMergeCommit( | ||||||
|     git: Git, |     git: Git, | ||||||
|  |     repository: RepositoryInfo, | ||||||
|     branch: String, |     branch: String, | ||||||
|     issueId: Int, |     issueId: Int, | ||||||
|     message: String, |     message: String, | ||||||
|     committer: PersonIdent |     loginAccount: Account, | ||||||
|   ): ObjectId = { |     settings: SystemSettings | ||||||
|     new MergeCacheInfo(git, branch, issueId).merge(message, committer) |   )(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 */ |   /** rebase to the head of the pull request branch */ | ||||||
|   def rebasePullRequest( |   def mergeWithRebase( | ||||||
|     git: Git, |     git: Git, | ||||||
|  |     repository: RepositoryInfo, | ||||||
|     branch: String, |     branch: String, | ||||||
|     issueId: Int, |     issueId: Int, | ||||||
|     commits: Seq[RevCommit], |     commits: Seq[RevCommit], | ||||||
|     committer: PersonIdent |     loginAccount: Account, | ||||||
|   ): ObjectId = { |     settings: SystemSettings | ||||||
|     new MergeCacheInfo(git, branch, issueId).rebase(committer, commits) |   )(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 */ |   /** squash commits in the pull request and append it */ | ||||||
|   def squashPullRequest( |   def mergeWithSquash( | ||||||
|     git: Git, |     git: Git, | ||||||
|  |     repository: RepositoryInfo, | ||||||
|     branch: String, |     branch: String, | ||||||
|     issueId: Int, |     issueId: Int, | ||||||
|     message: String, |     message: String, | ||||||
|     committer: PersonIdent |     loginAccount: Account, | ||||||
|   ): ObjectId = { |     settings: SystemSettings | ||||||
|     new MergeCacheInfo(git, branch, issueId).squash(message, committer) |   )(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 */ |   /** fetch remote branch to my repository refs/pull/{issueId}/head */ | ||||||
| @@ -168,7 +229,7 @@ trait MergeService { | |||||||
|     remoteBranch: String, |     remoteBranch: String, | ||||||
|     loginAccount: Account, |     loginAccount: Account, | ||||||
|     message: String, |     message: String, | ||||||
|     pullreq: Option[PullRequest], |     pullRequest: Option[PullRequest], | ||||||
|     settings: SystemSettings |     settings: SystemSettings | ||||||
|   )(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = { |   )(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = { | ||||||
|     val localUserName = localRepository.owner |     val localUserName = localRepository.owner | ||||||
| @@ -200,13 +261,14 @@ trait MergeService { | |||||||
|           } |           } | ||||||
|  |  | ||||||
|           // record activity |           // record activity | ||||||
|           recordPushActivity( |           val pushInfo = PushInfo( | ||||||
|             localUserName, |             localUserName, | ||||||
|             localRepositoryName, |             localRepositoryName, | ||||||
|             loginAccount.userName, |             loginAccount.userName, | ||||||
|             localBranch, |             localBranch, | ||||||
|             commits |             commits | ||||||
|           ) |           ) | ||||||
|  |           recordActivity(pushInfo) | ||||||
|  |  | ||||||
|           // close issue by commit message |           // close issue by commit message | ||||||
|           if (localBranch == localRepository.repository.defaultBranch) { |           if (localBranch == localRepository.repository.defaultBranch) { | ||||||
| @@ -215,6 +277,14 @@ trait MergeService { | |||||||
|                 .foreach { issueId => |                 .foreach { issueId => | ||||||
|                   getIssue(localRepository.owner, localRepository.name, issueId.toString).foreach { issue => |                   getIssue(localRepository.owner, localRepository.name, issueId.toString).foreach { issue => | ||||||
|                     callIssuesWebHook("closed", localRepository, issue, loginAccount, settings) |                     callIssuesWebHook("closed", localRepository, issue, loginAccount, settings) | ||||||
|  |                     val closeIssueInfo = CloseIssueInfo( | ||||||
|  |                       localRepository.owner, | ||||||
|  |                       localRepository.name, | ||||||
|  |                       localUserName, | ||||||
|  |                       issue.issueId, | ||||||
|  |                       issue.title | ||||||
|  |                     ) | ||||||
|  |                     recordActivity(closeIssueInfo) | ||||||
|                     PluginRegistry().getIssueHooks |                     PluginRegistry().getIssueHooks | ||||||
|                       .foreach( |                       .foreach( | ||||||
|                         _.closedByCommitComment(issue, localRepository, commit.fullMessage, loginAccount) |                         _.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) { |             callWebHookOf(localRepository.owner, localRepository.name, WebHook.Push, settings) { | ||||||
|               for { |               for { | ||||||
|                 ownerAccount <- getAccountByUserName(localRepository.owner) |                 ownerAccount <- getAccountByUserName(localRepository.owner) | ||||||
| @@ -232,7 +302,7 @@ trait MergeService { | |||||||
|                 WebHookService.WebHookPushPayload( |                 WebHookService.WebHookPushPayload( | ||||||
|                   git, |                   git, | ||||||
|                   loginAccount, |                   loginAccount, | ||||||
|                   pullreq.requestBranch, |                   pullRequest.requestBranch, | ||||||
|                   localRepository, |                   localRepository, | ||||||
|                   commits, |                   commits, | ||||||
|                   ownerAccount, |                   ownerAccount, | ||||||
| @@ -247,6 +317,10 @@ trait MergeService { | |||||||
|     }.toOption |     }.toOption | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected def getReceiveHooks(): Seq[ReceiveHook] = { | ||||||
|  |     PluginRegistry().getReceiveHooks | ||||||
|  |   } | ||||||
|  |  | ||||||
|   def mergePullRequest( |   def mergePullRequest( | ||||||
|     repository: RepositoryInfo, |     repository: RepositoryInfo, | ||||||
|     issueId: Int, |     issueId: Int, | ||||||
| @@ -261,73 +335,53 @@ trait MergeService { | |||||||
|         LockUtil.lock(s"${repository.owner}/${repository.name}") { |         LockUtil.lock(s"${repository.owner}/${repository.name}") { | ||||||
|           getPullRequest(repository.owner, repository.name, issueId) |           getPullRequest(repository.owner, repository.name, issueId) | ||||||
|             .map { |             .map { | ||||||
|               case (issue, pullreq) => |               case (issue, pullRequest) => | ||||||
|                 Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => |                 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. |                       // mark issue as merged and close. | ||||||
|                       val commentId = |                       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") |                       createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close") | ||||||
|                       updateClosed(repository.owner, repository.name, issueId, true) |                       updateClosed(repository.owner, repository.name, issueId, true) | ||||||
|  |  | ||||||
|                       // record activity |                       // 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 |                       // close issue by content of pull request | ||||||
|                       val defaultBranch = getRepository(repository.owner, repository.name).get.repository.defaultBranch |                       val defaultBranch = getRepository(repository.owner, repository.name).get.repository.defaultBranch | ||||||
|                       if (pullreq.branch == defaultBranch) { |                       if (pullRequest.branch == defaultBranch) { | ||||||
|                         commits.flatten.foreach { commit => |                         commits.flatten.foreach { commit => | ||||||
|                           closeIssuesFromMessage( |                           closeIssuesFromMessage( | ||||||
|                             commit.fullMessage, |                             commit.fullMessage, | ||||||
| @@ -337,6 +391,14 @@ trait MergeService { | |||||||
|                           ).foreach { issueId => |                           ).foreach { issueId => | ||||||
|                             getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => |                             getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||||
|                               callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) |                               callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) | ||||||
|  |                               val closeIssueInfo = CloseIssueInfo( | ||||||
|  |                                 repository.owner, | ||||||
|  |                                 repository.name, | ||||||
|  |                                 loginAccount.userName, | ||||||
|  |                                 issue.issueId, | ||||||
|  |                                 issue.title | ||||||
|  |                               ) | ||||||
|  |                               recordActivity(closeIssueInfo) | ||||||
|                               PluginRegistry().getIssueHooks |                               PluginRegistry().getIssueHooks | ||||||
|                                 .foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount)) |                                 .foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount)) | ||||||
|                             } |                             } | ||||||
| @@ -351,6 +413,14 @@ trait MergeService { | |||||||
|                         ).foreach { issueId => |                         ).foreach { issueId => | ||||||
|                           getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => |                           getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||||
|                             callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) |                             callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) | ||||||
|  |                             val closeIssueInfo = CloseIssueInfo( | ||||||
|  |                               repository.owner, | ||||||
|  |                               repository.name, | ||||||
|  |                               loginAccount.userName, | ||||||
|  |                               issue.issueId, | ||||||
|  |                               issue.title | ||||||
|  |                             ) | ||||||
|  |                             recordActivity(closeIssueInfo) | ||||||
|                             PluginRegistry().getIssueHooks |                             PluginRegistry().getIssueHooks | ||||||
|                               .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) |                               .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) | ||||||
|                           } |                           } | ||||||
| @@ -359,6 +429,14 @@ trait MergeService { | |||||||
|                           .foreach { issueId => |                           .foreach { issueId => | ||||||
|                             getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => |                             getIssue(repository.owner, repository.name, issueId.toString).foreach { issue => | ||||||
|                               callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) |                               callIssuesWebHook("closed", repository, issue, loginAccount, context.settings) | ||||||
|  |                               val closeIssueInfo = CloseIssueInfo( | ||||||
|  |                                 repository.owner, | ||||||
|  |                                 repository.name, | ||||||
|  |                                 loginAccount.userName, | ||||||
|  |                                 issue.issueId, | ||||||
|  |                                 issue.title | ||||||
|  |                               ) | ||||||
|  |                               recordActivity(closeIssueInfo) | ||||||
|                               PluginRegistry().getIssueHooks |                               PluginRegistry().getIssueHooks | ||||||
|                                 .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) |                                 .foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount)) | ||||||
|                             } |                             } | ||||||
| @@ -370,7 +448,7 @@ trait MergeService { | |||||||
|                       updatePullRequests( |                       updatePullRequests( | ||||||
|                         repository.owner, |                         repository.owner, | ||||||
|                         repository.name, |                         repository.name, | ||||||
|                         pullreq.branch, |                         pullRequest.branch, | ||||||
|                         loginAccount, |                         loginAccount, | ||||||
|                         "closed", |                         "closed", | ||||||
|                         settings |                         settings | ||||||
| @@ -394,6 +472,68 @@ trait MergeService { | |||||||
|       } else Left("Strategy not allowed") |       } else Left("Strategy not allowed") | ||||||
|     } else Left("Draft pull requests cannot be merged") |     } 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 { | object MergeService { | ||||||
| @@ -440,18 +580,22 @@ object MergeService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   class MergeCacheInfo(git: Git, branch: String, issueId: Int) { |   class MergeCacheInfo( | ||||||
|  |     git: Git, | ||||||
|     private val repository = git.getRepository |     userName: String, | ||||||
|  |     repositoryName: String, | ||||||
|  |     branch: String, | ||||||
|  |     issueId: Int, | ||||||
|  |     receiveHooks: Seq[ReceiveHook] | ||||||
|  |   ) { | ||||||
|     private val mergedBranchName = s"refs/pull/${issueId}/merge" |     private val mergedBranchName = s"refs/pull/${issueId}/merge" | ||||||
|     private val conflictedBranchName = s"refs/pull/${issueId}/conflict" |     private val conflictedBranchName = s"refs/pull/${issueId}/conflict" | ||||||
|  |  | ||||||
|     lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}") |     lazy val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}") | ||||||
|     lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head") |     lazy val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head") | ||||||
|  |  | ||||||
|     def checkConflictCache(): Option[Option[String]] = { |     def checkConflictCache(): Option[Option[String]] = { | ||||||
|       Option(repository.resolve(mergedBranchName)) |       Option(git.getRepository.resolve(mergedBranchName)) | ||||||
|         .flatMap { merged => |         .flatMap { merged => | ||||||
|           if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) { |           if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) { | ||||||
|             // merged branch exists |             // merged branch exists | ||||||
| @@ -460,7 +604,7 @@ object MergeService { | |||||||
|             None |             None | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         .orElse(Option(repository.resolve(conflictedBranchName)).flatMap { conflicted => |         .orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted => | ||||||
|           val commit = parseCommit(conflicted) |           val commit = parseCommit(conflicted) | ||||||
|           if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) { |           if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) { | ||||||
|             // conflict branch exists |             // conflict branch exists | ||||||
| @@ -472,23 +616,23 @@ object MergeService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     def checkConflict(): Option[String] = { |     def checkConflict(): Option[String] = { | ||||||
|       checkConflictCache.getOrElse(checkConflictForce) |       checkConflictCache().getOrElse(checkConflictForce()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     def checkConflictForce(): Option[String] = { |     def checkConflictForce(): Option[String] = { | ||||||
|       val merger = MergeStrategy.RECURSIVE.newMerger(repository, true) |       val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true) | ||||||
|       val conflicted = try { |       val conflicted = try { | ||||||
|         !merger.merge(mergeBaseTip, mergeTip) |         !merger.merge(mergeBaseTip, mergeTip) | ||||||
|       } catch { |       } catch { | ||||||
|         case e: NoMergeBaseException => true |         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 |       val committer = mergeTipCommit.getCommitterIdent | ||||||
|  |  | ||||||
|       def _updateBranch(treeId: ObjectId, message: String, branchName: String): Unit = { |       def _updateBranch(treeId: ObjectId, message: String, branchName: String): Unit = { | ||||||
|         // creates merge commit |         // creates merge commit | ||||||
|         val mergeCommitId = createMergeCommit(treeId, committer, message) |         val mergeCommitId = createMergeCommit(treeId, committer, message) | ||||||
|         Util.updateRefs(repository, branchName, mergeCommitId, true, committer) |         Util.updateRefs(git.getRepository, branchName, mergeCommitId, true, committer) | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!conflicted) { |       if (!conflicted) { | ||||||
| @@ -504,26 +648,48 @@ object MergeService { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     // update branch from cache |     // update branch from cache | ||||||
|     def merge(message: String, committer: PersonIdent): ObjectId = { |     def merge(message: String, committer: PersonIdent)(implicit s: Session): ObjectId = { | ||||||
|       if (checkConflict().isDefined) { |       if (checkConflict().isDefined) { | ||||||
|         throw new RuntimeException("This pull request can't merge automatically.") |         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}") |         throw new RuntimeException(s"Not found branch ${mergedBranchName}") | ||||||
|       }) |       }) | ||||||
|       // creates merge commit |       // creates merge commit | ||||||
|       val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message) |       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) { |       if (checkConflict().isDefined) { | ||||||
|         throw new RuntimeException("This pull request can't merge automatically.") |         throw new RuntimeException("This pull request can't merge automatically.") | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       def _cloneCommit(commit: RevCommit, parentId: ObjectId, baseId: ObjectId): CommitBuilder = { |       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) |         merger.merge(commit.toObjectId, baseId) | ||||||
|  |  | ||||||
|         val newCommit = new CommitBuilder() |         val newCommit = new CommitBuilder() | ||||||
| @@ -535,10 +701,10 @@ object MergeService { | |||||||
|         newCommit |         newCommit | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       val mergeBaseTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeBaseTip)) |       val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip)) | ||||||
|       var previousId = mergeBaseTipCommit.getId |       var previousId = mergeBaseTipCommit.getId | ||||||
|  |  | ||||||
|       Using.resource(repository.newObjectInserter) { inserter => |       Using.resource(git.getRepository.newObjectInserter) { inserter => | ||||||
|         commits.foreach { commit => |         commits.foreach { commit => | ||||||
|           val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId) |           val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId) | ||||||
|           previousId = inserter.insert(nextCommit) |           previousId = inserter.insert(nextCommit) | ||||||
| @@ -546,17 +712,40 @@ object MergeService { | |||||||
|         inserter.flush() |         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) { |       if (checkConflict().isDefined) { | ||||||
|         throw new RuntimeException("This pull request can't merge automatically.") |         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 = |       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 |       // Create squash commit | ||||||
|       val mergeCommit = new CommitBuilder() |       val mergeCommit = new CommitBuilder() | ||||||
| @@ -567,30 +756,52 @@ object MergeService { | |||||||
|       mergeCommit.setMessage(message) |       mergeCommit.setMessage(message) | ||||||
|  |  | ||||||
|       // insertObject and got squash commit Object Id |       // 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) |         val newCommitId = inserter.insert(mergeCommit) | ||||||
|         inserter.flush() |         inserter.flush() | ||||||
|         newCommitId |         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 |       // rebase to squash commit | ||||||
|       Util.updateRefs( |       val objectId = Util.updateRefs( | ||||||
|         repository, |         git.getRepository, | ||||||
|         s"refs/heads/${branch}", |         s"refs/heads/${branch}", | ||||||
|         repository.resolve(mergedBranchName), |         git.getRepository.resolve(mergedBranchName), | ||||||
|         false, |         false, | ||||||
|         committer, |         committer, | ||||||
|         Some("squashed") |         Some("squashed") | ||||||
|       ) |       ) | ||||||
|  |  | ||||||
|  |       // call post-commit hook | ||||||
|  |       receiveHooks.foreach { hook => | ||||||
|  |         hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true) | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       objectId | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // return treeId |     // return treeId | ||||||
|     private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) = |     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