mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-10-30 10:06:07 +01:00
Compare commits
85 Commits
disable-gi
...
4.35.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a3c8e0712 | ||
|
|
d3a29b3ecb | ||
|
|
7a50a15748 | ||
|
|
9a1b55b992 | ||
|
|
828b798c0e | ||
|
|
8d8845536d | ||
|
|
f20497e769 | ||
|
|
6053d9826e | ||
|
|
85263474a7 | ||
|
|
c02a722799 | ||
|
|
ce4faceccc | ||
|
|
04c8f8b864 | ||
|
|
1b32e13113 | ||
|
|
401728d47f | ||
|
|
31ace89f43 | ||
|
|
995cb86e90 | ||
|
|
e27623ca29 | ||
|
|
ea4da561c5 | ||
|
|
2e8f3efafd | ||
|
|
f25cf5781c | ||
|
|
d97f7c6025 | ||
|
|
e91d903650 | ||
|
|
4f93f06de5 | ||
|
|
08ed3c4171 | ||
|
|
5193d82980 | ||
|
|
eab82bf1be | ||
|
|
311d758910 | ||
|
|
097a2d32b8 | ||
|
|
a34766ccfd | ||
|
|
147eef9ee5 | ||
|
|
49118662b2 | ||
|
|
5989f2e2cb | ||
|
|
127f034bba | ||
|
|
e1e00c4b94 | ||
|
|
eb64cdd9cd | ||
|
|
1bfa8dffb8 | ||
|
|
33361b8015 | ||
|
|
b5ee074075 | ||
|
|
cbddc34bfa | ||
|
|
61504ae9e3 | ||
|
|
3049f6010c | ||
|
|
d4e01d631f | ||
|
|
a46aa2c61c | ||
|
|
ad147e8dd5 | ||
|
|
3555519392 | ||
|
|
f6a5def638 | ||
|
|
0590cb7048 | ||
|
|
b35d0792aa | ||
|
|
0d20bc0173 | ||
|
|
851141c2f4 | ||
|
|
31a104a697 | ||
|
|
bfc44cff98 | ||
|
|
0da781c33d | ||
|
|
8dbcbb5485 | ||
|
|
7544f64c65 | ||
|
|
73d05aefad | ||
|
|
4d70b056ad | ||
|
|
b81ce41d51 | ||
|
|
a143683d7f | ||
|
|
5ba38057dc | ||
|
|
07eb2bc41e | ||
|
|
5e4d041295 | ||
|
|
4d7fc061a4 | ||
|
|
8db98d7b16 | ||
|
|
a6063c8aa9 | ||
|
|
2cc1336e82 | ||
|
|
308bda2050 | ||
|
|
36989c38d4 | ||
|
|
29357ae170 | ||
|
|
36643bcdd0 | ||
|
|
72e40a0b12 | ||
|
|
2194ff7625 | ||
|
|
b247864bfe | ||
|
|
b1c3ae4974 | ||
|
|
81c0e2037f | ||
|
|
6224ec2a7b | ||
|
|
9ff4507fe2 | ||
|
|
67667dbff1 | ||
|
|
1a4961c3e1 | ||
|
|
561220237f | ||
|
|
f45b85aa71 | ||
|
|
83b3a7983e | ||
|
|
63d4c5054e | ||
|
|
c8f6017be9 | ||
|
|
f9fcb54861 |
@@ -5,6 +5,10 @@ trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.java]
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
6
.github/dependabot.yml
vendored
Normal file
6
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -1,19 +1,25 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java: [8, 11]
|
||||
java: [8, 11]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-sbt-libs
|
||||
with:
|
||||
path: |
|
||||
~/.ivy2/cache
|
||||
~/.sbt
|
||||
~/.cache/coursier/v1
|
||||
key: build-${{ env.cache-name }}-${{ hashFiles('build.sbt') }}
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -10,6 +10,7 @@ lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
.bsp/
|
||||
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
@@ -28,4 +29,8 @@ project/plugins/project/
|
||||
# Metals specific
|
||||
.metals
|
||||
.bloop
|
||||
project/metals.sbt
|
||||
**/metals.sbt
|
||||
|
||||
# Visual Studio Code specific
|
||||
.vscode
|
||||
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,15 +1,38 @@
|
||||
# Changelog
|
||||
All changes to the project will be documented in this file.
|
||||
|
||||
### 4.33.0 - 31 Dec 2019
|
||||
### 4.35.0 - 25 Dec 2020
|
||||
- Editor and source viewer color theme
|
||||
- Auto completion for issues and pull requests
|
||||
- Upload image from clipboard
|
||||
- Close multiple issues by commit comment
|
||||
- Create pull request from online editor
|
||||
- Milestone overview
|
||||
- Commit status at various places
|
||||
- WebAPI coverage improvements
|
||||
|
||||
## 4.34.0 - 26 Jul 2020
|
||||
- Enhancement admin settings UI
|
||||
- File upload settings
|
||||
- Restrict repository operations
|
||||
- User-defined CSS
|
||||
- Limit the repository list in the sidebar
|
||||
- Improve MariaDB support
|
||||
- Improve activity logging
|
||||
- CLI option to persist session on disk in the standalone mode
|
||||
- Web API updates
|
||||
- Add [list commits API](https://developer.github.com/v3/repos/commits/#list-commits)
|
||||
- Bundled plugins updates
|
||||
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin) 4.18.0 -> 4.19.0
|
||||
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin) 1.8.0 -> 1.9.0
|
||||
|
||||
## 4.33.0 - 31 Dec 2019
|
||||
- All CLI options are configurable by environment variables
|
||||
- Folding pull request files
|
||||
- WebHook security options
|
||||
- Add assignee and assignees properties to some Web APIs' response
|
||||
|
||||
### 4.32.0 - 7 Aug 2019
|
||||
|
||||
## 4.32.0 - 7 Aug 2019
|
||||
- Bump to Scala 2.13.0 and Scalatra 2.7.0
|
||||
- Draft pull request
|
||||
- Drop network installation of plugins
|
||||
@@ -17,20 +40,20 @@ All changes to the project will be documented in this file.
|
||||
- Apply default priority to pull requests
|
||||
- Focus title after clicking issue / pull request edit button
|
||||
|
||||
### 4.31.2 - 7 Apr 2019
|
||||
## 4.31.2 - 7 Apr 2019
|
||||
- Bug and security fix
|
||||
|
||||
### 4.31.1 - 17 Mar 2019
|
||||
## 4.31.1 - 17 Mar 2019
|
||||
- Bug fix
|
||||
|
||||
### 4.31.0 - 17 Mar 2019
|
||||
## 4.31.0 - 17 Mar 2019
|
||||
- Docker support in CI plugin
|
||||
- Verify GPG key signed commit
|
||||
- OAuth2 Token (sent as a parameter) authentication support and new APIs in Web API
|
||||
- OGP (Open Graph protocol) support
|
||||
- Username completion with avatars
|
||||
|
||||
### 4.30.1 - 22 Dec 2018
|
||||
## 4.30.1 - 22 Dec 2018
|
||||
- Bug fix for several WebHooks and Web API
|
||||
|
||||
## 4.30.0 - 15 Dec 2018
|
||||
@@ -117,7 +140,7 @@ All changes to the project will be documented in this file.
|
||||
- Submodule links to web page
|
||||
- Clarify close/reopen button
|
||||
|
||||
# 4.20.0 - 23 Dec 2017
|
||||
## 4.20.0 - 23 Dec 2017
|
||||
- Squash and rebase merge strategy for pull requests
|
||||
- Quick pull request creation
|
||||
- Download patch from the diff view
|
||||
|
||||
16
README.md
16
README.md
@@ -55,13 +55,17 @@ Support
|
||||
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
|
||||
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
|
||||
|
||||
What's New in 4.33.x
|
||||
What's New in 4.34.x
|
||||
-------------
|
||||
### 4.33.0 - 31 Dec 2019
|
||||
### 4.35.0 - 25 Dec 2020
|
||||
|
||||
- All CLI options are configurable by environment variables
|
||||
- Folding pull request files
|
||||
- WebHook security options
|
||||
- Add assignee and assignees properties to some Web APIs' response
|
||||
- Editor and source viewer color theme
|
||||
- Auto completion for issues and pull requests
|
||||
- Upload image from clipboard
|
||||
- Close multiple issues by commit comment
|
||||
- Create pull request from online editor
|
||||
- Milestone overview
|
||||
- Commit status at various places
|
||||
- WebAPI coverage improvements
|
||||
|
||||
See the [change log](CHANGELOG.md) for all of the updates.
|
||||
|
||||
29
build.sbt
29
build.sbt
@@ -3,10 +3,10 @@ import com.typesafe.sbt.pgp.PgpKeys._
|
||||
|
||||
val Organization = "io.github.gitbucket"
|
||||
val Name = "gitbucket"
|
||||
val GitBucketVersion = "4.33.0"
|
||||
val ScalatraVersion = "2.7.0-RC1"
|
||||
val JettyVersion = "9.4.30.v20200611"
|
||||
val JgitVersion = "5.8.0.202006091008-r"
|
||||
val GitBucketVersion = "4.35.0"
|
||||
val ScalatraVersion = "2.7.0"
|
||||
val JettyVersion = "9.4.32.v20200930"
|
||||
val JgitVersion = "5.9.0.202009080501-r"
|
||||
|
||||
lazy val root = (project in file("."))
|
||||
.enablePlugins(SbtTwirl, ScalatraPlugin)
|
||||
@@ -36,30 +36,28 @@ libraryDependencies ++= Seq(
|
||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
|
||||
"org.scalatra" %% "scalatra-forms" % ScalatraVersion,
|
||||
"org.json4s" %% "json4s-jackson" % "3.6.9",
|
||||
"commons-io" % "commons-io" % "2.7",
|
||||
"org.json4s" %% "json4s-jackson" % "3.6.10",
|
||||
"commons-io" % "commons-io" % "2.8.0",
|
||||
"io.github.gitbucket" % "solidbase" % "1.0.3",
|
||||
"io.github.gitbucket" % "markedj" % "1.0.16",
|
||||
"org.apache.commons" % "commons-compress" % "1.20",
|
||||
"org.apache.commons" % "commons-email" % "1.5",
|
||||
"commons-net" % "commons-net" % "3.6",
|
||||
"commons-net" % "commons-net" % "3.7",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.5.12",
|
||||
"org.apache.sshd" % "apache-sshd" % "2.1.0" exclude ("org.slf4j", "slf4j-jdk14") exclude ("org.apache.sshd", "sshd-mina") exclude ("org.apache.sshd", "sshd-netty"),
|
||||
"org.apache.tika" % "tika-core" % "1.24.1",
|
||||
"com.github.takezoe" %% "blocking-slick-32" % "0.0.12",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"com.h2database" % "h2" % "1.4.199",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.6.0",
|
||||
"org.mariadb.jdbc" % "mariadb-java-client" % "2.7.0",
|
||||
"org.postgresql" % "postgresql" % "42.2.6",
|
||||
"ch.qos.logback" % "logback-classic" % "1.2.3",
|
||||
"com.zaxxer" % "HikariCP" % "3.4.5",
|
||||
"com.typesafe" % "config" % "1.4.0",
|
||||
"com.typesafe.akka" %% "akka-actor" % "2.5.27",
|
||||
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.1.0",
|
||||
"com.github.bkromhout" % "java-diff-utils" % "2.1.1",
|
||||
"org.cache2k" % "cache2k-all" % "1.2.4.Final",
|
||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.8.1-akka-2.5.x" exclude ("com.mchange", "c3p0") exclude ("com.zaxxer", "HikariCP-java6"),
|
||||
"net.coobird" % "thumbnailator" % "0.4.11",
|
||||
"net.coobird" % "thumbnailator" % "0.4.12",
|
||||
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
|
||||
"com.nimbusds" % "oauth2-oidc-sdk" % "5.64.4",
|
||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||
@@ -72,7 +70,8 @@ libraryDependencies ++= Seq(
|
||||
"org.testcontainers" % "postgresql" % "1.14.3" % "test",
|
||||
"net.i2p.crypto" % "eddsa" % "0.3.0",
|
||||
"is.tagomor.woothee" % "woothee-java" % "1.11.0",
|
||||
"org.ec4j.core" % "ec4j-core" % "0.0.3"
|
||||
"org.ec4j.core" % "ec4j-core" % "0.0.3",
|
||||
"org.kohsuke" % "github-api" % "1.116" % "test"
|
||||
)
|
||||
|
||||
// Compiler settings
|
||||
@@ -122,6 +121,12 @@ libraryDependencies ++= Seq(
|
||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||
)
|
||||
|
||||
// Run package task before test to generate target/webapp for integration test
|
||||
test in Test := {
|
||||
_root_.sbt.Keys.`package`.value
|
||||
(test in Test).value
|
||||
}
|
||||
|
||||
val executableKey = TaskKey[File]("executable")
|
||||
executableKey := {
|
||||
import java.util.jar.Attributes.{Name => AttrName}
|
||||
|
||||
@@ -1 +1 @@
|
||||
sbt.version=1.3.12
|
||||
sbt.version=1.4.4
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
notifications:1.8.0
|
||||
gist:4.18.0
|
||||
notifications:1.9.0
|
||||
gist:4.20.0
|
||||
emoji:4.6.0
|
||||
pages:1.8.0
|
||||
pages:1.9.0
|
||||
|
||||
4
src/main/resources/update/gitbucket-core_4.34.xml
Normal file
4
src/main/resources/update/gitbucket-core_4.34.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<dropTable tableName="ACTIVITY" />
|
||||
</changeSet>
|
||||
21
src/main/resources/update/gitbucket-core_4.35.xml
Normal file
21
src/main/resources/update/gitbucket-core_4.35.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<changeSet>
|
||||
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT"/>
|
||||
<dropForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK"/>
|
||||
<dropPrimaryKey tableName="WEB_HOOK" constraintName="IDX_WEB_HOOK_PK"/>
|
||||
<addColumn tableName="WEB_HOOK">
|
||||
<column name="HOOK_ID" type="int" nullable="false" unique="true"/>
|
||||
</addColumn>
|
||||
<addPrimaryKey constraintName="IDX_WEB_HOOK_PK" tableName="WEB_HOOK" columnNames="USER_NAME, REPOSITORY_NAME, URL, HOOK_ID"/>
|
||||
<addUniqueConstraint constraintName="IDX_WEB_HOOK_1" tableName="WEB_HOOK" columnNames="USER_NAME, REPOSITORY_NAME, URL"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_FK0" baseTableName="WEB_HOOK" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_WEB_HOOK_EVENT_FK0" baseTableName="WEB_HOOK_EVENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, URL" referencedTableName="WEB_HOOK" referencedColumnNames="USER_NAME, REPOSITORY_NAME, URL" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
<addAutoIncrement columnName="HOOK_ID" columnDataType="int" tableName="WEB_HOOK"/>
|
||||
|
||||
<createTable tableName="ACCOUNT_PREFERENCE">
|
||||
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||
<column name="HIGHLIGHTER_THEME" type="varchar(100)" nullable="false" defaultValue="prettify"/>
|
||||
</createTable>
|
||||
<addPrimaryKey constraintName="IDX_ACCOUNT_PREFERENCE_PK" tableName="ACCOUNT_PREFERENCE" columnNames="USER_NAME"/>
|
||||
<addForeignKeyConstraint constraintName="IDX_ACCOUNT_PREFERENCE_FK0" baseTableName="ACCOUNT_PREFERENCE" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||
</changeSet>
|
||||
@@ -1,7 +1,22 @@
|
||||
package gitbucket.core
|
||||
|
||||
import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration}
|
||||
import io.github.gitbucket.solidbase.model.{Version, Module}
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.sql.Connection
|
||||
import java.util
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.util.Directory.ActivityLog
|
||||
import gitbucket.core.util.JDBCUtil
|
||||
import io.github.gitbucket.solidbase.Solidbase
|
||||
import io.github.gitbucket.solidbase.migration.{LiquibaseMigration, Migration, SqlMigration}
|
||||
import io.github.gitbucket.solidbase.model.{Module, Version}
|
||||
import org.json4s.NoTypeHints
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.json4s.jackson.Serialization.write
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
object GitBucketCoreModule
|
||||
extends Module(
|
||||
@@ -65,5 +80,39 @@ object GitBucketCoreModule
|
||||
new Version("4.31.1"),
|
||||
new Version("4.31.2"),
|
||||
new Version("4.32.0", new LiquibaseMigration("update/gitbucket-core_4.32.xml")),
|
||||
new Version("4.33.0")
|
||||
new Version("4.33.0"),
|
||||
new Version(
|
||||
"4.34.0",
|
||||
new Migration() {
|
||||
override def migrate(moduleId: String, version: String, context: util.Map[String, AnyRef]): Unit = {
|
||||
implicit val formats = Serialization.formats(NoTypeHints)
|
||||
import JDBCUtil._
|
||||
|
||||
val conn = context.get(Solidbase.CONNECTION).asInstanceOf[Connection]
|
||||
val list = conn.select("SELECT * FROM ACTIVITY ORDER BY ACTIVITY_ID") {
|
||||
rs =>
|
||||
Activity(
|
||||
activityId = UUID.randomUUID().toString,
|
||||
userName = rs.getString("USER_NAME"),
|
||||
repositoryName = rs.getString("REPOSITORY_NAME"),
|
||||
activityUserName = rs.getString("ACTIVITY_USER_NAME"),
|
||||
activityType = rs.getString("ACTIVITY_TYPE"),
|
||||
message = rs.getString("MESSAGE"),
|
||||
additionalInfo = {
|
||||
val additionalInfo = rs.getString("ADDITIONAL_INFO")
|
||||
if (rs.wasNull()) None else Some(additionalInfo)
|
||||
},
|
||||
activityDate = rs.getTimestamp("ACTIVITY_DATE")
|
||||
)
|
||||
}
|
||||
Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
|
||||
list.foreach { activity =>
|
||||
out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
new LiquibaseMigration("update/gitbucket-core_4.34.xml")
|
||||
),
|
||||
new Version("4.35.0", new LiquibaseMigration("update/gitbucket-core_4.35.xml")),
|
||||
)
|
||||
|
||||
@@ -22,3 +22,12 @@ case class ApiBranchForList(
|
||||
name: String,
|
||||
commit: ApiBranchCommit
|
||||
)
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/repos#list-branches-for-head-commit
|
||||
*/
|
||||
case class ApiBranchForHeadCommit(
|
||||
name: String,
|
||||
commit: ApiBranchCommit,
|
||||
`protected`: Boolean
|
||||
)
|
||||
|
||||
@@ -4,7 +4,11 @@ import gitbucket.core.service.ProtectedBranchService
|
||||
import org.json4s._
|
||||
|
||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
||||
case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]) {
|
||||
case class ApiBranchProtection(
|
||||
url: Option[ApiPath], // for output
|
||||
enabled: Boolean,
|
||||
required_status_checks: Option[ApiBranchProtection.Status]
|
||||
) {
|
||||
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
|
||||
}
|
||||
|
||||
@@ -15,13 +19,36 @@ object ApiBranchProtection {
|
||||
|
||||
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection =
|
||||
ApiBranchProtection(
|
||||
url = Some(
|
||||
ApiPath(
|
||||
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection"
|
||||
)
|
||||
),
|
||||
enabled = info.enabled,
|
||||
required_status_checks = Some(
|
||||
Status(EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), info.contexts)
|
||||
Status(
|
||||
Some(
|
||||
ApiPath(
|
||||
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks"
|
||||
)
|
||||
),
|
||||
EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators),
|
||||
info.contexts,
|
||||
Some(
|
||||
ApiPath(
|
||||
s"/api/v3/repos/${info.owner}/${info.repository}/branches/${info.branch}/protection/required_status_checks/contexts"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val statusNone = Status(Off, Seq.empty)
|
||||
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
|
||||
val statusNone = Status(None, Off, Seq.empty, None)
|
||||
case class Status(
|
||||
url: Option[ApiPath], // for output
|
||||
enforcement_level: EnforcementLevel,
|
||||
contexts: Seq[String],
|
||||
contexts_url: Option[ApiPath] // for output
|
||||
)
|
||||
sealed class EnforcementLevel(val name: String)
|
||||
case object Off extends EnforcementLevel("off")
|
||||
case object NonAdmins extends EnforcementLevel("non_admins")
|
||||
|
||||
49
src/main/scala/gitbucket/core/api/ApiMilestone.scala
Normal file
49
src/main/scala/gitbucket/core/api/ApiMilestone.scala
Normal file
@@ -0,0 +1,49 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.{Milestone, Repository}
|
||||
import gitbucket.core.util.RepositoryName
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/issues#milestones
|
||||
*/
|
||||
case class ApiMilestone(
|
||||
url: ApiPath,
|
||||
html_url: ApiPath,
|
||||
// label_url: ApiPath,
|
||||
id: Int,
|
||||
number: Int,
|
||||
state: String,
|
||||
title: String,
|
||||
description: String,
|
||||
// creator: ApiUser, // MILESTONE table does not have created user column
|
||||
open_issues: Int,
|
||||
closed_issues: Int,
|
||||
// created_at: Option[Date],
|
||||
// updated_at: Option[Date],
|
||||
closed_at: Option[Date],
|
||||
due_on: Option[Date]
|
||||
)
|
||||
|
||||
object ApiMilestone {
|
||||
def apply(
|
||||
repository: Repository,
|
||||
milestone: Milestone,
|
||||
open_issue_count: Int = 0,
|
||||
closed_issue_count: Int = 0
|
||||
): ApiMilestone =
|
||||
ApiMilestone(
|
||||
url = ApiPath(s"/api/v3/repos/${RepositoryName(repository).fullName}/milestones/${milestone.milestoneId}"),
|
||||
html_url = ApiPath(s"/${RepositoryName(repository).fullName}/milestone/${milestone.milestoneId}"),
|
||||
// label_url = ApiPath(s"/api/v3/repos/${RepositoryName(repository).fullName}/milestones/${milestone_number}/labels"),
|
||||
id = milestone.milestoneId,
|
||||
number = milestone.milestoneId, // use milestoneId as number
|
||||
state = if (milestone.closedDate.isDefined) "closed" else "open",
|
||||
title = milestone.title,
|
||||
description = milestone.description.getOrElse(""),
|
||||
open_issues = open_issue_count,
|
||||
closed_issues = closed_issue_count,
|
||||
closed_at = milestone.closedDate,
|
||||
due_on = milestone.dueDate
|
||||
)
|
||||
}
|
||||
@@ -12,13 +12,13 @@ case class ApiRepository(
|
||||
forks: Int,
|
||||
`private`: Boolean,
|
||||
default_branch: String,
|
||||
owner: ApiUser
|
||||
owner: ApiUser,
|
||||
has_issues: Boolean
|
||||
) {
|
||||
val id = 0 // dummy id
|
||||
val forks_count = forks
|
||||
val watchers_count = watchers
|
||||
val url = ApiPath(s"/api/v3/repos/${full_name}")
|
||||
val http_url = ApiPath(s"/git/${full_name}.git")
|
||||
val clone_url = ApiPath(s"/git/${full_name}.git")
|
||||
val html_url = ApiPath(s"/${full_name}")
|
||||
val ssh_url = Some(SshPath(s":${full_name}.git"))
|
||||
@@ -39,11 +39,16 @@ object ApiRepository {
|
||||
forks = forkedCount,
|
||||
`private` = repository.isPrivate,
|
||||
default_branch = repository.defaultBranch,
|
||||
owner = owner
|
||||
owner = owner,
|
||||
has_issues = if (repository.options.issuesOption == "DISABLE") false else true
|
||||
)
|
||||
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||
ApiRepository(repositoryInfo.repository, owner, forkedCount = repositoryInfo.forkedCount)
|
||||
ApiRepository(
|
||||
repositoryInfo.repository,
|
||||
owner,
|
||||
forkedCount = repositoryInfo.forkedCount
|
||||
)
|
||||
|
||||
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
|
||||
this(repositoryInfo, ApiUser(owner))
|
||||
@@ -57,6 +62,7 @@ object ApiRepository {
|
||||
forks = 0,
|
||||
`private` = false,
|
||||
default_branch = "master",
|
||||
owner = owner
|
||||
owner = owner,
|
||||
has_issues = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
case class ApiRepositoryCollaborator(
|
||||
permission: String,
|
||||
user: ApiUser
|
||||
)
|
||||
29
src/main/scala/gitbucket/core/api/ApiTag.scala
Normal file
29
src/main/scala/gitbucket/core/api/ApiTag.scala
Normal file
@@ -0,0 +1,29 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
case class ApiTagCommit(
|
||||
sha: String,
|
||||
url: ApiPath
|
||||
)
|
||||
|
||||
case class ApiTag(
|
||||
name: String,
|
||||
commit: ApiTagCommit,
|
||||
zipball_url: ApiPath,
|
||||
tarball_url: ApiPath
|
||||
)
|
||||
|
||||
object ApiTag {
|
||||
def apply(
|
||||
tagName: String,
|
||||
repositoryName: RepositoryName,
|
||||
commitId: String
|
||||
): ApiTag =
|
||||
ApiTag(
|
||||
name = tagName,
|
||||
commit = ApiTagCommit(sha = commitId, url = ApiPath(s"/${repositoryName.fullName}/commits/${commitId}")),
|
||||
zipball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.zip"),
|
||||
tarball_url = ApiPath(s"/${repositoryName.fullName}/archive/${tagName}.tar.gz")
|
||||
)
|
||||
}
|
||||
46
src/main/scala/gitbucket/core/api/ApiWebhook.scala
Normal file
46
src/main/scala/gitbucket/core/api/ApiWebhook.scala
Normal file
@@ -0,0 +1,46 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import gitbucket.core.model.Profile.{RepositoryWebHookEvents, RepositoryWebHooks}
|
||||
import gitbucket.core.model.{RepositoryWebHook, WebHook}
|
||||
import gitbucket.core.util.RepositoryName
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/repos#webhooks
|
||||
*/
|
||||
case class ApiWebhookConfig(
|
||||
content_type: String,
|
||||
// insecure_ssl: String,
|
||||
url: String
|
||||
)
|
||||
|
||||
case class ApiWebhook(
|
||||
`type`: String,
|
||||
id: Int,
|
||||
name: String,
|
||||
active: Boolean,
|
||||
events: List[String],
|
||||
config: ApiWebhookConfig,
|
||||
// updated_at: Option[Date],
|
||||
// created_at: Option[Date],
|
||||
url: ApiPath,
|
||||
// test_url: ApiPath,
|
||||
// ping_url: ApiPath,
|
||||
// last_response: ...
|
||||
)
|
||||
|
||||
object ApiWebhook {
|
||||
def apply(
|
||||
_type: String,
|
||||
hook: RepositoryWebHook,
|
||||
hookEvents: Set[WebHook.Event]
|
||||
): ApiWebhook =
|
||||
ApiWebhook(
|
||||
`type` = _type,
|
||||
id = hook.hookId,
|
||||
name = "web", // dummy
|
||||
active = true, // dummy
|
||||
events = hookEvents.toList.map(_.name),
|
||||
config = ApiWebhookConfig(hook.ctype.code, hook.url),
|
||||
url = ApiPath(s"/api/v3/${hook.userName}/${hook.repositoryName}/hooks/${hook.hookId}")
|
||||
)
|
||||
}
|
||||
14
src/main/scala/gitbucket/core/api/CreateAMilestone.scala
Normal file
14
src/main/scala/gitbucket/core/api/CreateAMilestone.scala
Normal file
@@ -0,0 +1,14 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
import java.util.Date
|
||||
|
||||
case class CreateAMilestone(
|
||||
title: String,
|
||||
state: String = "open",
|
||||
description: Option[String],
|
||||
due_on: Option[Date]
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
title.length <= 100 && title.matches("[a-zA-Z0-9\\-\\+_.]+")
|
||||
}
|
||||
}
|
||||
@@ -15,3 +15,11 @@ case class CreateAPullRequestAlt(
|
||||
base: String,
|
||||
maintainer_can_modify: Option[Boolean]
|
||||
)
|
||||
|
||||
case class UpdateAPullRequest(
|
||||
title: Option[String],
|
||||
body: Option[String],
|
||||
state: Option[String],
|
||||
base: Option[String],
|
||||
maintainer_can_modify: Option[Boolean],
|
||||
)
|
||||
|
||||
7
src/main/scala/gitbucket/core/api/CreateARef.scala
Normal file
7
src/main/scala/gitbucket/core/api/CreateARef.scala
Normal file
@@ -0,0 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
|
||||
* api form
|
||||
*/
|
||||
case class CreateARef(ref: String, sha: String)
|
||||
@@ -0,0 +1,35 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
case class CreateARepositoryWebhookConfig(
|
||||
url: String,
|
||||
content_type: String = "form",
|
||||
insecure_ssl: String = "0",
|
||||
secret: Option[String]
|
||||
)
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook
|
||||
*/
|
||||
case class CreateARepositoryWebhook(
|
||||
name: String = "web",
|
||||
config: CreateARepositoryWebhookConfig,
|
||||
events: List[String] = List("push"),
|
||||
active: Boolean = true
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
config.content_type == "form" || config.content_type == "json"
|
||||
}
|
||||
}
|
||||
|
||||
case class UpdateARepositoryWebhook(
|
||||
name: String = "web",
|
||||
config: CreateARepositoryWebhookConfig,
|
||||
events: List[String] = List("push"),
|
||||
add_events: List[String] = List(),
|
||||
remove_events: List[String] = List(),
|
||||
active: Boolean = true
|
||||
) {
|
||||
def isValid: Boolean = {
|
||||
config.content_type == "form" || config.content_type == "json"
|
||||
}
|
||||
}
|
||||
23
src/main/scala/gitbucket/core/api/MergeAPullRequest.scala
Normal file
23
src/main/scala/gitbucket/core/api/MergeAPullRequest.scala
Normal file
@@ -0,0 +1,23 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request
|
||||
*/
|
||||
case class MergeAPullRequest(
|
||||
commit_title: Option[String],
|
||||
commit_message: Option[String],
|
||||
/* TODO: Not Implemented
|
||||
sha: Option[String],*/
|
||||
merge_method: Option[String]
|
||||
)
|
||||
|
||||
case class SuccessToMergePrResponse(
|
||||
sha: String,
|
||||
merged: Boolean,
|
||||
message: String
|
||||
)
|
||||
|
||||
case class FailToMergePrResponse(
|
||||
documentation_url: String,
|
||||
message: String
|
||||
)
|
||||
7
src/main/scala/gitbucket/core/api/UpdateARef.scala
Normal file
7
src/main/scala/gitbucket/core/api/UpdateARef.scala
Normal file
@@ -0,0 +1,7 @@
|
||||
package gitbucket.core.api
|
||||
|
||||
/**
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
|
||||
* api form
|
||||
*/
|
||||
case class UpdateARef(sha: String, force: Boolean)
|
||||
@@ -35,6 +35,7 @@ class AccountController
|
||||
with WebHookService
|
||||
with PrioritiesService
|
||||
with RepositoryCreationService
|
||||
with RequestCache
|
||||
|
||||
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
self: AccountService
|
||||
@@ -81,6 +82,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
|
||||
case class PersonalTokenForm(note: String)
|
||||
|
||||
case class SyntaxHighlighterThemeForm(theme: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"userName" -> trim(label("User name", text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
|
||||
"password" -> trim(label("Password", text(required, maxlength(20)))),
|
||||
@@ -121,6 +124,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
"note" -> trim(label("Token", text(required, maxlength(100))))
|
||||
)(PersonalTokenForm.apply)
|
||||
|
||||
val syntaxHighlighterThemeForm = mapping(
|
||||
"highlighterTheme" -> trim(label("Theme", text(required)))
|
||||
)(SyntaxHighlighterThemeForm.apply)
|
||||
|
||||
case class NewGroupForm(
|
||||
groupName: String,
|
||||
description: Option[String],
|
||||
@@ -441,6 +448,29 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
redirect(s"/${userName}/_application")
|
||||
})
|
||||
|
||||
/**
|
||||
* Display the user preference settings page
|
||||
*/
|
||||
get("/:userName/_preferences")(oneselfOnly {
|
||||
val userName = params("userName")
|
||||
val currentTheme = getAccountPreference(userName) match {
|
||||
case Some(accountHighlighter) => accountHighlighter.highlighterTheme
|
||||
case _ => "github-v2"
|
||||
}
|
||||
getAccountByUserName(userName).map { x =>
|
||||
html.preferences(x, currentTheme)
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
/**
|
||||
* Update the syntax highlighter setting of user
|
||||
*/
|
||||
post("/:userName/_preferences/highlighter", syntaxHighlighterThemeForm)(oneselfOnly { form =>
|
||||
val userName = params("userName")
|
||||
addOrUpdateAccountPreference(userName, form.theme)
|
||||
redirect(s"/${userName}/_preferences")
|
||||
})
|
||||
|
||||
get("/:userName/_hooks")(managersOnly {
|
||||
val userName = params("userName")
|
||||
getAccountByUserName(userName).map { account =>
|
||||
@@ -521,7 +551,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
||||
val url = params("url")
|
||||
val token = Some(params("token"))
|
||||
val ctype = WebHookContentType.valueOf(params("ctype"))
|
||||
val dummyWebHookInfo = RepositoryWebHook(userName, "dummy", url, ctype, token)
|
||||
val dummyWebHookInfo =
|
||||
RepositoryWebHook(userName = userName, repositoryName = "dummy", url = url, ctype = ctype, token = token)
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(userName).get
|
||||
WebHookPushPayload.createDummyPayload(ownerAccount)
|
||||
|
||||
@@ -13,6 +13,7 @@ class ApiController
|
||||
with ApiIssueCommentControllerBase
|
||||
with ApiIssueControllerBase
|
||||
with ApiIssueLabelControllerBase
|
||||
with ApiIssueMilestoneControllerBase
|
||||
with ApiOrganizationControllerBase
|
||||
with ApiPullRequestControllerBase
|
||||
with ApiReleaseControllerBase
|
||||
@@ -22,6 +23,7 @@ class ApiController
|
||||
with ApiRepositoryContentsControllerBase
|
||||
with ApiRepositoryControllerBase
|
||||
with ApiRepositoryStatusControllerBase
|
||||
with ApiRepositoryWebhookControllerBase
|
||||
with ApiUserControllerBase
|
||||
with RepositoryService
|
||||
with AccountService
|
||||
@@ -52,6 +54,7 @@ class ApiController
|
||||
with ReferrerAuthenticator
|
||||
with ReadableUsersAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
with RequestCache
|
||||
|
||||
trait ApiControllerBase extends ControllerBase {
|
||||
|
||||
|
||||
@@ -21,10 +21,17 @@ class DashboardController
|
||||
with WebHookPullRequestService
|
||||
with WebHookPullRequestReviewCommentService
|
||||
with MilestonesService
|
||||
with CommitStatusService
|
||||
with UsersAuthenticator
|
||||
with RequestCache
|
||||
|
||||
trait DashboardControllerBase extends ControllerBase {
|
||||
self: IssuesService with PullRequestService with RepositoryService with AccountService with UsersAuthenticator =>
|
||||
self: IssuesService
|
||||
with PullRequestService
|
||||
with RepositoryService
|
||||
with AccountService
|
||||
with CommitStatusService
|
||||
with UsersAuthenticator =>
|
||||
|
||||
get("/dashboard/repos")(usersOnly {
|
||||
val repos = getVisibleRepositories(
|
||||
@@ -85,12 +92,13 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
||||
val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val issues = searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, userRepos: _*)
|
||||
|
||||
html.issues(
|
||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||
issues.map(issue => (issue, None)),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open"), false, userRepos: _*),
|
||||
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
||||
countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, userRepos: _*),
|
||||
countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, userRepos: _*),
|
||||
filter match {
|
||||
case "assigned" => condition.copy(assigned = Some(Some(userName)))
|
||||
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||
@@ -115,12 +123,24 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
|
||||
val allRepos = getAllRepositories(userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val issues = searchIssue(
|
||||
condition,
|
||||
IssueSearchOption.PullRequests,
|
||||
(page - 1) * PullRequestLimit,
|
||||
PullRequestLimit,
|
||||
allRepos: _*
|
||||
)
|
||||
val status = issues.map { issue =>
|
||||
issue.commitId.flatMap { commitId =>
|
||||
getCommitStatusWithSummary(issue.issue.userName, issue.issue.repositoryName, commitId)
|
||||
}
|
||||
}
|
||||
|
||||
html.pulls(
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||
issues.zip(status),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open"), true, allRepos: _*),
|
||||
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
||||
countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, allRepos: _*),
|
||||
countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, allRepos: _*),
|
||||
filter match {
|
||||
case "assigned" => condition.copy(assigned = Some(Some(userName)))
|
||||
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||
|
||||
@@ -29,6 +29,7 @@ class IndexController
|
||||
with AccessTokenService
|
||||
with AccountFederationService
|
||||
with OpenIDConnectService
|
||||
with RequestCache
|
||||
|
||||
trait IndexControllerBase extends ControllerBase {
|
||||
self: RepositoryService
|
||||
@@ -78,7 +79,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
}
|
||||
.getOrElse {
|
||||
gitbucket.core.html.index(
|
||||
getRecentActivities(),
|
||||
getRecentPublicActivities(),
|
||||
getVisibleRepositories(None, withoutPhysicalInfo = true),
|
||||
showBannerToCreatePersonalAccessToken = false
|
||||
)
|
||||
@@ -161,7 +162,7 @@ trait IndexControllerBase extends ControllerBase {
|
||||
|
||||
get("/activities.atom") {
|
||||
contentType = "application/atom+xml; type=feed"
|
||||
xml.feed(getRecentActivities())
|
||||
xml.feed(getRecentPublicActivities())
|
||||
}
|
||||
|
||||
post("/sidebar-collapse") {
|
||||
@@ -212,8 +213,10 @@ trait IndexControllerBase extends ControllerBase {
|
||||
}
|
||||
.map { t =>
|
||||
Map(
|
||||
"label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(t.userName)}</b> ${StringUtil
|
||||
.escapeHtml(t.fullName)}",
|
||||
"label" -> s"${avatar(t.userName, 16)}<b>@${StringUtil.escapeHtml(
|
||||
StringUtil.cutTail(t.userName, 25, "...")
|
||||
)}</b> ${StringUtil
|
||||
.escapeHtml(StringUtil.cutTail(t.fullName, 25, "..."))}",
|
||||
"value" -> t.userName
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class IssuesController
|
||||
with WebHookPullRequestReviewCommentService
|
||||
with CommitsService
|
||||
with PrioritiesService
|
||||
with RequestCache
|
||||
|
||||
trait IssuesControllerBase extends ControllerBase {
|
||||
self: IssuesService
|
||||
@@ -111,6 +112,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
getLabels(owner, name),
|
||||
isIssueEditable(repository),
|
||||
isIssueManageable(repository),
|
||||
isIssueCommentManageable(repository),
|
||||
repository
|
||||
)
|
||||
}
|
||||
@@ -237,8 +239,8 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
defining(repository.owner, repository.name) {
|
||||
case (owner, name) =>
|
||||
getComment(owner, name, params("id")).map { comment =>
|
||||
if (isEditableContent(owner, name, comment.commentedUserName)) {
|
||||
Ok(deleteComment(comment.issueId, comment.commentId))
|
||||
if (isDeletableComment(owner, name, comment.commentedUserName)) {
|
||||
Ok(deleteComment(repository.owner, repository.name, comment.issueId, comment.commentId))
|
||||
} else Unauthorized()
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
@@ -368,6 +370,9 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
case _ => BadRequest()
|
||||
}
|
||||
if (params("uri").nonEmpty) {
|
||||
redirect(params("uri"))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -376,6 +381,9 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
executeBatch(repository) { issueId =>
|
||||
getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse {
|
||||
registerIssueLabel(repository.owner, repository.name, issueId, labelId, true)
|
||||
if (params("uri").nonEmpty) {
|
||||
redirect(params("uri"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
@@ -386,6 +394,9 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
executeBatch(repository) {
|
||||
updateAssignedUserName(repository.owner, repository.name, _, value, true)
|
||||
}
|
||||
if (params("uri").nonEmpty) {
|
||||
redirect(params("uri"))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -416,6 +427,29 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/**
|
||||
* JSON API for issue and PR completion.
|
||||
*/
|
||||
ajaxGet("/:owner/:repository/_issue/proposals")(writableUsersOnly { repository =>
|
||||
contentType = formats("json")
|
||||
org.json4s.jackson.Serialization.write(
|
||||
Map(
|
||||
"options" -> (
|
||||
getOpenIssues(repository.owner, repository.name)
|
||||
.map { t =>
|
||||
Map(
|
||||
"label" -> s"""${if (t.isPullRequest) "<i class='octicon octicon-git-pull-request'></i>"
|
||||
else "<i class='octicon octicon-issue-opened'></i>"}<b> #${StringUtil
|
||||
.escapeHtml(t.issueId.toString)} ${StringUtil
|
||||
.escapeHtml(StringUtil.cutTail(t.title, 50, "..."))}</b>""",
|
||||
"value" -> t.issueId.toString
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
|
||||
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||
val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
|
||||
@@ -425,6 +459,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
params("from") match {
|
||||
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,20 +467,22 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
defining(repository.owner, repository.name) {
|
||||
case (owner, repoName) =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
// retrieve search condition
|
||||
val condition = IssueSearchCondition(request)
|
||||
// search issues
|
||||
val issues =
|
||||
searchIssue(condition, IssueSearchOption.Issues, (page - 1) * IssueLimit, IssueLimit, owner -> repoName)
|
||||
|
||||
html.list(
|
||||
"issues",
|
||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||
issues.map(issue => (issue, None)),
|
||||
page,
|
||||
getAssignableUserNames(owner, repoName),
|
||||
getMilestones(owner, repoName),
|
||||
getPriorities(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open"), false, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
|
||||
countIssue(condition.copy(state = "open"), IssueSearchOption.Issues, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), IssueSearchOption.Issues, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
isIssueEditable(repository),
|
||||
@@ -462,4 +499,13 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
): Boolean = {
|
||||
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an issue comment is deletable by a logged-in user.
|
||||
*/
|
||||
private def isDeletableComment(owner: String, repository: String, author: String)(
|
||||
implicit context: Context
|
||||
): Boolean = {
|
||||
hasOwnerRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.issues.milestones.html
|
||||
import gitbucket.core.service.{AccountService, MilestonesService, RepositoryService}
|
||||
import gitbucket.core.service.IssuesService.{IssueLimit, IssueSearchCondition}
|
||||
import gitbucket.core.service.{
|
||||
AccountService,
|
||||
CommitStatusService,
|
||||
IssueSearchOption,
|
||||
MilestonesService,
|
||||
RepositoryService
|
||||
}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.view.helpers.{getAssignableUserNames, getLabels, getPriorities, searchIssue}
|
||||
import org.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
@@ -13,11 +21,16 @@ class MilestonesController
|
||||
with MilestonesService
|
||||
with RepositoryService
|
||||
with AccountService
|
||||
with CommitStatusService
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
|
||||
trait MilestonesControllerBase extends ControllerBase {
|
||||
self: MilestonesService with RepositoryService with ReferrerAuthenticator with WritableUsersAuthenticator =>
|
||||
self: MilestonesService
|
||||
with RepositoryService
|
||||
with CommitStatusService
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator =>
|
||||
|
||||
case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.util.Date])
|
||||
|
||||
@@ -36,6 +49,41 @@ trait MilestonesControllerBase extends ControllerBase {
|
||||
)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/milestone/:id")(referrersOnly { repository =>
|
||||
val milestone = getMilestone(repository.owner, repository.name, params("id").toInt)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val condition = IssueSearchCondition(
|
||||
request,
|
||||
milestone.get.title
|
||||
)
|
||||
val issues = searchIssue(
|
||||
condition,
|
||||
IssueSearchOption.Both,
|
||||
(page - 1) * IssueLimit,
|
||||
IssueLimit,
|
||||
repository.owner -> repository.name
|
||||
)
|
||||
val status = issues.map { issue =>
|
||||
issue.commitId.flatMap { commitId =>
|
||||
getCommitStatusWithSummary(issue.issue.userName, issue.issue.repositoryName, commitId)
|
||||
}
|
||||
}
|
||||
|
||||
html.milestone(
|
||||
condition.state,
|
||||
issues.zip(status),
|
||||
page,
|
||||
getAssignableUserNames(repository.owner, repository.name),
|
||||
getPriorities(repository.owner, repository.name),
|
||||
getLabels(repository.owner, repository.name),
|
||||
condition,
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.filter(p => p._1.milestoneId == milestone.get.milestoneId),
|
||||
repository,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/milestones/new")(writableUsersOnly {
|
||||
html.edit(None, _)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.model.activity.DeleteBranchInfo
|
||||
import gitbucket.core.pulls.html
|
||||
import gitbucket.core.service.CommitStatusService
|
||||
import gitbucket.core.service.MergeService
|
||||
@@ -36,6 +37,7 @@ class PullRequestsController
|
||||
with MergeService
|
||||
with ProtectedBranchService
|
||||
with PrioritiesService
|
||||
with RequestCache
|
||||
|
||||
trait PullRequestsControllerBase extends ControllerBase {
|
||||
self: RepositoryService
|
||||
@@ -217,7 +219,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
|
||||
val mergeStatus = PullRequestService.MergeStatus(
|
||||
conflictMessage = conflictMessage,
|
||||
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
|
||||
commitStatuses = getCommitStatuses(owner, name, pullreq.commitIdTo),
|
||||
branchProtection = branchProtection,
|
||||
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
|
||||
needStatusCheck = context.loginAccount
|
||||
@@ -271,7 +273,8 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
val userName = context.loginAccount.get.userName
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(pullreq.requestBranch).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, pullreq.requestBranch)
|
||||
val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, pullreq.requestBranch)
|
||||
recordActivity(deleteBranchInfo)
|
||||
}
|
||||
createComment(
|
||||
baseRepository.owner,
|
||||
@@ -634,20 +637,33 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
defining(repository.owner, repository.name) {
|
||||
case (owner, repoName) =>
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
// retrieve search condition
|
||||
val condition = IssueSearchCondition(request)
|
||||
// search issues
|
||||
val issues = searchIssue(
|
||||
condition,
|
||||
IssueSearchOption.PullRequests,
|
||||
(page - 1) * PullRequestLimit,
|
||||
PullRequestLimit,
|
||||
owner -> repoName
|
||||
)
|
||||
// commit status
|
||||
val status = issues.map { issue =>
|
||||
issue.commitId.flatMap { commitId =>
|
||||
getCommitStatusWithSummary(owner, repoName, commitId)
|
||||
}
|
||||
}
|
||||
|
||||
gitbucket.core.issues.html.list(
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
issues.zip(status),
|
||||
page,
|
||||
getAssignableUserNames(owner, repoName),
|
||||
getMilestones(owner, repoName),
|
||||
getPriorities(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open"), true, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
|
||||
countIssue(condition.copy(state = "open"), IssueSearchOption.PullRequests, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), IssueSearchOption.PullRequests, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
isEditable(repository),
|
||||
|
||||
@@ -2,7 +2,15 @@ package gitbucket.core.controller
|
||||
|
||||
import java.io.File
|
||||
|
||||
import gitbucket.core.service.{AccountService, ActivityService, PaginationHelper, ReleaseService, RepositoryService}
|
||||
import gitbucket.core.model.activity.ReleaseInfo
|
||||
import gitbucket.core.service.{
|
||||
AccountService,
|
||||
ActivityService,
|
||||
PaginationHelper,
|
||||
ReleaseService,
|
||||
RepositoryService,
|
||||
RequestCache
|
||||
}
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -22,6 +30,7 @@ class ReleaseController
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with WritableUsersAuthenticator
|
||||
with RequestCache
|
||||
|
||||
trait ReleaseControllerBase extends ControllerBase {
|
||||
self: RepositoryService
|
||||
@@ -119,7 +128,8 @@ trait ReleaseControllerBase extends ControllerBase {
|
||||
createReleaseAsset(repository.owner, repository.name, tagName, fileId, fileName, size, loginAccount)
|
||||
}
|
||||
|
||||
recordReleaseActivity(repository.owner, repository.name, loginAccount.userName, form.name, tagName)
|
||||
val releaseInfo = ReleaseInfo(repository.owner, repository.name, loginAccount.userName, form.name, tagName)
|
||||
recordActivity(releaseInfo)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/releases/${tagName}")
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.model.WebHookContentType
|
||||
import gitbucket.core.model.activity.RenameRepositoryInfo
|
||||
import org.scalatra.forms._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.eclipse.jgit.api.Git
|
||||
@@ -30,8 +31,10 @@ class RepositorySettingsController
|
||||
with ProtectedBranchService
|
||||
with CommitStatusService
|
||||
with DeployKeyService
|
||||
with ActivityService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator
|
||||
with RequestCache
|
||||
|
||||
trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
self: RepositoryService
|
||||
@@ -40,6 +43,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
with ProtectedBranchService
|
||||
with CommitStatusService
|
||||
with DeployKeyService
|
||||
with ActivityService
|
||||
with OwnerAuthenticator
|
||||
with UsersAuthenticator =>
|
||||
|
||||
@@ -97,9 +101,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
"events" -> webhookEvents,
|
||||
"ctype" -> label("ctype", text()),
|
||||
"token" -> optional(trim(label("token", text(maxlength(100)))))
|
||||
)(
|
||||
(url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
|
||||
)
|
||||
)((url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token))
|
||||
|
||||
// for rename repository
|
||||
case class RenameRepositoryForm(repositoryName: String)
|
||||
@@ -176,14 +178,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
/** Branch protection for branch */
|
||||
get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository =>
|
||||
get("/:owner/:repository/settings/branches/*")(ownerOnly { repository =>
|
||||
import gitbucket.core.api._
|
||||
val branch = params("branch")
|
||||
val branch = params("splat")
|
||||
|
||||
if (!repository.branchList.contains(branch)) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
|
||||
} else {
|
||||
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
|
||||
val lastWeeks = getRecentStatuesContexts(
|
||||
val lastWeeks = getRecentStatusContexts(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
Date.from(LocalDateTime.now.minusWeeks(1).toInstant(ZoneOffset.UTC))
|
||||
@@ -225,7 +228,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook edit page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||
val webhook = RepositoryWebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None)
|
||||
val webhook = RepositoryWebHook(
|
||||
userName = repository.owner,
|
||||
repositoryName = repository.name,
|
||||
url = "",
|
||||
ctype = WebHookContentType.FORM,
|
||||
token = None
|
||||
)
|
||||
html.edithook(webhook, Set(WebHook.Push), repository, true)
|
||||
})
|
||||
|
||||
@@ -251,9 +260,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Send the test request to registered web hook URLs.
|
||||
*/
|
||||
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h =>
|
||||
Array(h.getName, h.getValue)
|
||||
}
|
||||
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] =
|
||||
h.map { h =>
|
||||
Array(h.getName, h.getValue)
|
||||
}
|
||||
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
|
||||
git =>
|
||||
@@ -267,7 +277,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
val url = params("url")
|
||||
val token = Some(params("token"))
|
||||
val ctype = WebHookContentType.valueOf(params("ctype"))
|
||||
val dummyWebHookInfo = RepositoryWebHook(repository.owner, repository.name, url, ctype, token)
|
||||
val dummyWebHookInfo = RepositoryWebHook(
|
||||
userName = repository.owner,
|
||||
repositoryName = repository.name,
|
||||
url = url,
|
||||
ctype = ctype,
|
||||
token = token
|
||||
)
|
||||
val dummyPayload = {
|
||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||
val commits =
|
||||
@@ -371,7 +387,16 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
post("/:owner/:repository/settings/rename", renameForm)(ownerOnly { (form, repository) =>
|
||||
if (context.settings.repositoryOperation.rename || context.loginAccount.get.isAdmin) {
|
||||
if (repository.name != form.repositoryName) {
|
||||
// Update database and move git repository
|
||||
renameRepository(repository.owner, repository.name, repository.owner, form.repositoryName)
|
||||
// Record activity log
|
||||
val renameInfo = RenameRepositoryInfo(
|
||||
repository.owner,
|
||||
form.repositoryName,
|
||||
context.loginAccount.get.userName,
|
||||
repository.name
|
||||
)
|
||||
recordActivity(renameInfo)
|
||||
}
|
||||
redirect(s"/${repository.owner}/${form.repositoryName}")
|
||||
} else Forbidden()
|
||||
@@ -384,7 +409,16 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
if (context.settings.repositoryOperation.transfer || context.loginAccount.get.isAdmin) {
|
||||
// Change repository owner
|
||||
if (repository.owner != form.newOwner) {
|
||||
// Update database and move git repository
|
||||
renameRepository(repository.owner, repository.name, form.newOwner, repository.name)
|
||||
// Record activity log
|
||||
val renameInfo = RenameRepositoryInfo(
|
||||
form.newOwner,
|
||||
repository.name,
|
||||
context.loginAccount.get.userName,
|
||||
repository.owner
|
||||
)
|
||||
recordActivity(renameInfo)
|
||||
}
|
||||
redirect(s"/${form.newOwner}/${repository.name}")
|
||||
} else Forbidden()
|
||||
@@ -435,32 +469,34 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Provides duplication check for web hook url.
|
||||
*/
|
||||
private def webHook(needExists: Boolean): Constraint = new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) {
|
||||
Some(if (needExists) {
|
||||
"URL had not been registered yet."
|
||||
private def webHook(needExists: Boolean): Constraint =
|
||||
new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
if (getWebHook(params("owner"), params("repository"), value).isDefined != needExists) {
|
||||
Some(if (needExists) {
|
||||
"URL had not been registered yet."
|
||||
} else {
|
||||
"URL had been registered already."
|
||||
})
|
||||
} else {
|
||||
"URL had been registered already."
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
private def webhookEvents = new ValueType[Set[WebHook.Event]] {
|
||||
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
|
||||
WebHook.Event.values.flatMap { t =>
|
||||
params.get(name + "." + t.name).map(_ => t)
|
||||
}.toSet
|
||||
None
|
||||
}
|
||||
}
|
||||
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
|
||||
if (convert(name, params, messages).isEmpty) {
|
||||
Seq(name -> messages("error.required").format(name))
|
||||
} else {
|
||||
Nil
|
||||
|
||||
private def webhookEvents =
|
||||
new ValueType[Set[WebHook.Event]] {
|
||||
def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Set[WebHook.Event] = {
|
||||
WebHook.Event.values.flatMap { t =>
|
||||
params.get(name + "." + t.name).map(_ => t)
|
||||
}.toSet
|
||||
}
|
||||
}
|
||||
def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] =
|
||||
if (convert(name, params, messages).isEmpty) {
|
||||
Seq(name -> messages("error.required").format(name))
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Provides Constraint to validate the collaborator name.
|
||||
@@ -480,70 +516,77 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Duplicate check for the rename repository name.
|
||||
*/
|
||||
private def renameRepositoryName: Constraint = new Constraint() {
|
||||
override def validate(
|
||||
name: String,
|
||||
value: String,
|
||||
params: Map[String, Seq[String]],
|
||||
messages: Messages
|
||||
): Option[String] = {
|
||||
for {
|
||||
repoName <- params.optionValue("repository") if repoName != value
|
||||
userName <- params.optionValue("owner")
|
||||
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
|
||||
} yield {
|
||||
"Repository already exists."
|
||||
private def renameRepositoryName: Constraint =
|
||||
new Constraint() {
|
||||
override def validate(
|
||||
name: String,
|
||||
value: String,
|
||||
params: Map[String, Seq[String]],
|
||||
messages: Messages
|
||||
): Option[String] = {
|
||||
for {
|
||||
repoName <- params.optionValue("repository") if repoName != value
|
||||
userName <- params.optionValue("owner")
|
||||
_ <- getRepositoryNamesOfUser(userName).find(_ == value)
|
||||
} yield {
|
||||
"Repository already exists."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private def featureOption: Constraint = new Constraint() {
|
||||
override def validate(
|
||||
name: String,
|
||||
value: String,
|
||||
params: Map[String, Seq[String]],
|
||||
messages: Messages
|
||||
): Option[String] =
|
||||
if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
|
||||
}
|
||||
private def featureOption: Constraint =
|
||||
new Constraint() {
|
||||
override def validate(
|
||||
name: String,
|
||||
value: String,
|
||||
params: Map[String, Seq[String]],
|
||||
messages: Messages
|
||||
): Option[String] =
|
||||
if (Seq("DISABLE", "PRIVATE", "PUBLIC", "ALL").contains(value)) None else Some("Option is invalid.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides Constraint to validate the repository transfer user.
|
||||
*/
|
||||
private def transferUser: Constraint = new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) =>
|
||||
if (x.userName == params("owner")) {
|
||||
Some("This is current repository owner.")
|
||||
} else {
|
||||
params.get("repository").flatMap { repositoryName =>
|
||||
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map { _ =>
|
||||
"User already has same repository."
|
||||
private def transferUser: Constraint =
|
||||
new Constraint() {
|
||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||
getAccountByUserName(value) match {
|
||||
case None => Some("User does not exist.")
|
||||
case Some(x) =>
|
||||
if (x.userName == params("owner")) {
|
||||
Some("This is current repository owner.")
|
||||
} else {
|
||||
params.get("repository").flatMap { repositoryName =>
|
||||
getRepositoryNamesOfUser(x.userName).find(_ == repositoryName).map { _ =>
|
||||
"User already has same repository."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def mergeOptions = new ValueType[Seq[String]] {
|
||||
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = {
|
||||
params.getOrElse("mergeOptions", Nil)
|
||||
}
|
||||
override def validate(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[(String, String)] = {
|
||||
val mergeOptions = params.getOrElse("mergeOptions", Nil)
|
||||
if (mergeOptions.isEmpty) {
|
||||
Seq("mergeOptions" -> "At least one option must be enabled.")
|
||||
} else if (!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))) {
|
||||
Seq("mergeOptions" -> "mergeOptions are invalid.")
|
||||
} else {
|
||||
Nil
|
||||
private def mergeOptions =
|
||||
new ValueType[Seq[String]] {
|
||||
override def convert(name: String, params: Map[String, Seq[String]], messages: Messages): Seq[String] = {
|
||||
params.getOrElse("mergeOptions", Nil)
|
||||
}
|
||||
override def validate(
|
||||
name: String,
|
||||
params: Map[String, Seq[String]],
|
||||
messages: Messages
|
||||
): Seq[(String, String)] = {
|
||||
val mergeOptions = params.getOrElse("mergeOptions", Nil)
|
||||
if (mergeOptions.isEmpty) {
|
||||
Seq("mergeOptions" -> "At least one option must be enabled.")
|
||||
} else if (!mergeOptions.forall(x => Seq("merge-commit", "squash", "rebase").contains(x))) {
|
||||
Seq("mergeOptions" -> "mergeOptions are invalid.")
|
||||
} else {
|
||||
Nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import java.io.{File, FileInputStream, FileOutputStream}
|
||||
|
||||
import scala.util.Using
|
||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.repo.html
|
||||
import gitbucket.core.helper
|
||||
import gitbucket.core.model.activity.DeleteBranchInfo
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.service.RepositoryCommitFileService.CommitFile
|
||||
import gitbucket.core.util._
|
||||
@@ -14,7 +14,8 @@ import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.model.{Account, CommitState, CommitStatus}
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
@@ -60,6 +61,7 @@ class RepositoryViewerController
|
||||
with WebHookPullRequestService
|
||||
with WebHookPullRequestReviewCommentService
|
||||
with ProtectedBranchService
|
||||
with RequestCache
|
||||
|
||||
/**
|
||||
* The repository viewer.
|
||||
@@ -88,7 +90,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
branch: String,
|
||||
path: String,
|
||||
uploadFiles: String,
|
||||
message: Option[String]
|
||||
message: Option[String],
|
||||
commit: String,
|
||||
newBranch: Boolean
|
||||
)
|
||||
|
||||
case class EditorForm(
|
||||
@@ -100,7 +104,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
lineSeparator: String,
|
||||
newFileName: String,
|
||||
oldFileName: Option[String],
|
||||
commit: String
|
||||
commit: String,
|
||||
newBranch: Boolean
|
||||
)
|
||||
|
||||
case class DeleteForm(
|
||||
@@ -108,7 +113,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
path: String,
|
||||
message: Option[String],
|
||||
fileName: String,
|
||||
commit: String
|
||||
commit: String,
|
||||
newBranch: Boolean
|
||||
)
|
||||
|
||||
case class CommentForm(
|
||||
@@ -131,6 +137,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
"path" -> trim(label("Path", text())),
|
||||
"uploadFiles" -> trim(label("Upload files", text(required))),
|
||||
"message" -> trim(label("Message", optional(text()))),
|
||||
"commit" -> trim(label("Commit", text(required, conflict))),
|
||||
"newBranch" -> trim(label("New Branch", boolean()))
|
||||
)(UploadForm.apply)
|
||||
|
||||
val editorForm = mapping(
|
||||
@@ -142,7 +150,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
"lineSeparator" -> trim(label("Line Separator", text(required))),
|
||||
"newFileName" -> trim(label("Filename", text(required))),
|
||||
"oldFileName" -> trim(label("Old filename", optional(text()))),
|
||||
"commit" -> trim(label("Commit", text(required, conflict)))
|
||||
"commit" -> trim(label("Commit", text(required, conflict))),
|
||||
"newBranch" -> trim(label("New Branch", boolean()))
|
||||
)(EditorForm.apply)
|
||||
|
||||
val deleteForm = mapping(
|
||||
@@ -150,7 +159,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
"path" -> trim(label("Path", text())),
|
||||
"message" -> trim(label("Message", optional(text()))),
|
||||
"fileName" -> trim(label("Filename", text(required))),
|
||||
"commit" -> trim(label("Commit", text(required, conflict)))
|
||||
"commit" -> trim(label("Commit", text(required, conflict))),
|
||||
"newBranch" -> trim(label("New Branch", boolean()))
|
||||
)(DeleteForm.apply)
|
||||
|
||||
val commentForm = mapping(
|
||||
@@ -252,23 +262,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val (branchName, path) = repository.splitPath(multiParams("splat").head)
|
||||
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
|
||||
|
||||
def getStatuses(sha: String): List[CommitStatus] = {
|
||||
getCommitStatues(repository.owner, repository.name, sha)
|
||||
}
|
||||
|
||||
def getSummary(statuses: List[CommitStatus]): (CommitState, String) = {
|
||||
val stateMap = statuses.groupBy(_.state)
|
||||
val state = CommitState.combine(stateMap.keySet)
|
||||
val summary = stateMap.map { case (keyState, states) => s"${states.size} ${keyState.name}" }.mkString(", ")
|
||||
state -> summary
|
||||
}
|
||||
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
|
||||
git =>
|
||||
def getTags(sha: String): List[String] = {
|
||||
JGitUtil.getTagsOnCommit(git, sha)
|
||||
}
|
||||
|
||||
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
|
||||
case Right((logs, hasNext)) =>
|
||||
html.commits(
|
||||
@@ -277,34 +272,33 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
repository,
|
||||
logs
|
||||
.map {
|
||||
c =>
|
||||
CommitInfo(
|
||||
id = c.id,
|
||||
shortMessage = c.shortMessage,
|
||||
fullMessage = c.fullMessage,
|
||||
parents = c.parents,
|
||||
authorTime = c.authorTime,
|
||||
authorName = c.authorName,
|
||||
authorEmailAddress = c.authorEmailAddress,
|
||||
commitTime = c.commitTime,
|
||||
committerName = c.committerName,
|
||||
committerEmailAddress = c.committerEmailAddress,
|
||||
commitSign = c.commitSign,
|
||||
verified = c.commitSign
|
||||
.flatMap { s =>
|
||||
GpgUtil.verifySign(s)
|
||||
}
|
||||
commit =>
|
||||
(
|
||||
CommitInfo(
|
||||
id = commit.id,
|
||||
shortMessage = commit.shortMessage,
|
||||
fullMessage = commit.fullMessage,
|
||||
parents = commit.parents,
|
||||
authorTime = commit.authorTime,
|
||||
authorName = commit.authorName,
|
||||
authorEmailAddress = commit.authorEmailAddress,
|
||||
commitTime = commit.commitTime,
|
||||
committerName = commit.committerName,
|
||||
committerEmailAddress = commit.committerEmailAddress,
|
||||
commitSign = commit.commitSign,
|
||||
verified = commit.commitSign.flatMap(GpgUtil.verifySign)
|
||||
),
|
||||
JGitUtil.getTagsOnCommit(git, commit.id),
|
||||
getCommitStatusWithSummary(repository.owner, repository.name, commit.id)
|
||||
)
|
||||
}
|
||||
.splitWith { (commit1, commit2) =>
|
||||
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
.splitWith {
|
||||
case ((commit1, _, _), (commit2, _, _)) =>
|
||||
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
|
||||
},
|
||||
page,
|
||||
hasNext,
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
|
||||
getStatuses,
|
||||
getSummary,
|
||||
getTags
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
)
|
||||
case Left(_) => NotFound()
|
||||
}
|
||||
@@ -335,7 +329,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val (branch, path) = repository.splitPath(multiParams("splat").head)
|
||||
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
.needStatusCheck(context.loginAccount.get.userName)
|
||||
html.upload(branch, repository, if (path.length == 0) Nil else path.split("/").toList, protectedBranch)
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
|
||||
html.upload(
|
||||
branch,
|
||||
repository,
|
||||
if (path.length == 0) Nil else path.split("/").toList,
|
||||
protectedBranch,
|
||||
revCommit.name
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
|
||||
@@ -348,36 +351,47 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
file.copy(name = if (form.path.length == 0) file.name else s"${form.path}/${file.name}")
|
||||
}
|
||||
|
||||
commitFiles(
|
||||
repository = repository,
|
||||
branch = form.branch,
|
||||
path = form.path,
|
||||
files = files.toIndexedSeq,
|
||||
message = form.message.getOrElse("Add files via upload"),
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
) {
|
||||
case (git, headTip, builder, inserter) =>
|
||||
JGitUtil.processTree(git, headTip) { (path, tree) =>
|
||||
if (!newFiles.exists(_.name.contains(path))) {
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
|
||||
newFiles.foreach { file =>
|
||||
val bytes =
|
||||
FileUtils.readFileToByteArray(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(file.id)))
|
||||
builder.add(
|
||||
JGitUtil.createDirCacheEntry(file.name, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes))
|
||||
)
|
||||
builder.finish()
|
||||
}
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
if (form.path.length == 0) {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}")
|
||||
} else {
|
||||
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}/${form.path}")
|
||||
def _commit(branchName: String): ObjectId = {
|
||||
commitFiles(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
path = form.path,
|
||||
files = files.toIndexedSeq,
|
||||
message = form.message.getOrElse("Add files via upload"),
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
) {
|
||||
case (git, headTip, builder, inserter) =>
|
||||
JGitUtil.processTree(git, headTip) { (path, tree) =>
|
||||
if (!newFiles.exists(_.name.contains(path))) {
|
||||
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||
}
|
||||
}
|
||||
|
||||
newFiles.foreach { file =>
|
||||
val bytes =
|
||||
FileUtils.readFileToByteArray(new File(getTemporaryDir(session.getId), FileUtil.checkFilename(file.id)))
|
||||
builder.add(
|
||||
JGitUtil.createDirCacheEntry(file.name, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes))
|
||||
)
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -432,70 +446,156 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
post("/:owner/:repository/create", editorForm)(writableUsersOnly { (form, repository) =>
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = form.branch,
|
||||
path = form.path,
|
||||
newFileName = Some(form.newFileName),
|
||||
oldFileName = None,
|
||||
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||
charset = form.charset,
|
||||
message = form.message.getOrElse(s"Create ${form.newFileName}"),
|
||||
commit = form.commit,
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
)
|
||||
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)}"}"
|
||||
)
|
||||
}
|
||||
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${form.branch}/${if (form.path.length == 0) urlEncode(form.newFileName)
|
||||
else s"${form.path}/${urlEncode(form.newFileName)}"}"
|
||||
)
|
||||
def _commit(branchName: String): ObjectId = {
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
path = form.path,
|
||||
newFileName = Some(form.newFileName),
|
||||
oldFileName = None,
|
||||
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||
charset = form.charset,
|
||||
message = form.message.getOrElse(s"Create ${form.newFileName}"),
|
||||
commit = form.commit,
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/update", editorForm)(writableUsersOnly { (form, repository) =>
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = form.branch,
|
||||
path = form.path,
|
||||
newFileName = Some(form.newFileName),
|
||||
oldFileName = form.oldFileName,
|
||||
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||
charset = form.charset,
|
||||
message = if (form.oldFileName.contains(form.newFileName)) {
|
||||
form.message.getOrElse(s"Update ${form.newFileName}")
|
||||
} else {
|
||||
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
|
||||
},
|
||||
commit = form.commit,
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
)
|
||||
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)}"}"
|
||||
)
|
||||
}
|
||||
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/blob/${urlEncode(form.branch)}/${if (form.path.length == 0) urlEncode(form.newFileName)
|
||||
else s"${form.path}/${urlEncode(form.newFileName)}"}"
|
||||
)
|
||||
def _commit(branchName: String): ObjectId = {
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
path = form.path,
|
||||
newFileName = Some(form.newFileName),
|
||||
oldFileName = form.oldFileName,
|
||||
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
|
||||
charset = form.charset,
|
||||
message = if (form.oldFileName.contains(form.newFileName)) {
|
||||
form.message.getOrElse(s"Update ${form.newFileName}")
|
||||
} else {
|
||||
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
|
||||
},
|
||||
commit = form.commit,
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
post("/:owner/:repository/remove", deleteForm)(writableUsersOnly { (form, repository) =>
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = form.branch,
|
||||
path = form.path,
|
||||
newFileName = None,
|
||||
oldFileName = Some(form.fileName),
|
||||
content = "",
|
||||
charset = "",
|
||||
message = form.message.getOrElse(s"Delete ${form.fileName}"),
|
||||
commit = form.commit,
|
||||
if (form.newBranch) {
|
||||
val newBranchName = createNewBranchForPullRequest(repository, form.branch)
|
||||
val objectId = _commit(newBranchName)
|
||||
val issueId =
|
||||
createIssueAndPullRequest(repository, form.branch, newBranchName, form.commit, objectId.name, form.message)
|
||||
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
|
||||
} else {
|
||||
_commit(form.branch)
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) ""
|
||||
else "/" + form.path}"
|
||||
)
|
||||
}
|
||||
|
||||
def _commit(branchName: String): ObjectId = {
|
||||
commitFile(
|
||||
repository = repository,
|
||||
branch = branchName,
|
||||
path = form.path,
|
||||
newFileName = None,
|
||||
oldFileName = Some(form.fileName),
|
||||
content = "",
|
||||
charset = "",
|
||||
message = form.message.getOrElse(s"Delete ${form.fileName}"),
|
||||
commit = form.commit,
|
||||
loginAccount = context.loginAccount.get,
|
||||
settings = context.settings
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
redirect(
|
||||
s"/${repository.owner}/${repository.name}/tree/${form.branch}${if (form.path.length == 0) "" else "/" + form.path}"
|
||||
)
|
||||
})
|
||||
issueId
|
||||
}
|
||||
|
||||
get("/:owner/:repository/raw/*")(referrersOnly { repository =>
|
||||
val (id, path) = repository.splitPath(multiParams("splat").head)
|
||||
@@ -514,6 +614,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
|
||||
val (id, path) = repository.splitPath(multiParams("splat").head)
|
||||
val raw = params.get("raw").getOrElse("false").toBoolean
|
||||
val highlighterTheme = getSyntaxHighlighterTheme()
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
|
||||
git =>
|
||||
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
|
||||
@@ -533,13 +634,25 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
hasWritePermission = hasDeveloperRole(repository.owner, repository.name, context.loginAccount),
|
||||
isBlame = request.paths(2) == "blame",
|
||||
isLfsFile = isLfsFile(git, objectId),
|
||||
tabSize = info.tabSize
|
||||
tabSize = info.tabSize,
|
||||
highlighterTheme = highlighterTheme
|
||||
)
|
||||
}
|
||||
} getOrElse NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
private def getSyntaxHighlighterTheme()(implicit context: Context): String = {
|
||||
context.loginAccount match {
|
||||
case Some(account) =>
|
||||
getAccountPreference(account.userName) match {
|
||||
case Some(x) => x.highlighterTheme
|
||||
case _ => "github-v2"
|
||||
}
|
||||
case _ => "github-v2"
|
||||
}
|
||||
}
|
||||
|
||||
private def isLfsFile(git: Git, objectId: ObjectId): Boolean = {
|
||||
JGitUtil.getObjectLoaderFromId(git, objectId)(JGitUtil.isLfsPointer).getOrElse(false)
|
||||
}
|
||||
@@ -601,6 +714,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
new JGitUtil.CommitInfo(revCommit),
|
||||
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
|
||||
JGitUtil.getTagsOfCommit(git, revCommit.getName),
|
||||
getCommitStatusWithSummary(repository.owner, repository.name, revCommit.getName),
|
||||
getCommitComments(repository.owner, repository.name, id, true),
|
||||
repository,
|
||||
diffs,
|
||||
@@ -759,19 +873,20 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
defaultBranch = repository.repository.defaultBranch,
|
||||
origin = repository.repository.originUserName.isEmpty
|
||||
)
|
||||
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
|
||||
.sortBy(branch => (branch.mergeInfo.isEmpty, branch.commitTime))
|
||||
.map(
|
||||
br =>
|
||||
branch =>
|
||||
(
|
||||
br,
|
||||
branch,
|
||||
getPullRequestByRequestCommit(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
repository.repository.defaultBranch,
|
||||
br.name,
|
||||
br.commitId
|
||||
branch.name,
|
||||
branch.commitId
|
||||
),
|
||||
protectedBranches.contains(br.name)
|
||||
protectedBranches.contains(branch.name),
|
||||
getCommitStatusWithSummary(repository.owner, repository.name, branch.commitId)
|
||||
)
|
||||
)
|
||||
.reverse
|
||||
@@ -832,7 +947,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
if (repository.repository.defaultBranch != branchName) {
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
git.branchDelete().setForce(true).setBranchNames(branchName).call()
|
||||
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
|
||||
val deleteBranchInfo = DeleteBranchInfo(repository.owner, repository.name, userName, branchName)
|
||||
recordActivity(deleteBranchInfo)
|
||||
}
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/branches")
|
||||
@@ -905,10 +1021,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
lazy val isValid: Boolean = fileIds.nonEmpty
|
||||
}
|
||||
|
||||
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
|
||||
s"readme.${extension}"
|
||||
} ++ Seq("readme.txt", "readme")
|
||||
|
||||
/**
|
||||
* Provides HTML of the file list.
|
||||
*
|
||||
@@ -930,12 +1042,19 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
if (path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
|
||||
val commitCount = JGitUtil.getCommitCount(git, lastModifiedCommit.getName)
|
||||
// get files
|
||||
val files = JGitUtil.getFileList(git, revision, path, context.settings.baseUrl, commitCount)
|
||||
val files = JGitUtil.getFileList(
|
||||
git,
|
||||
revision,
|
||||
path,
|
||||
context.settings.baseUrl,
|
||||
commitCount,
|
||||
context.settings.repositoryViewer.maxFiles
|
||||
)
|
||||
val parentPath = if (path == ".") Nil else path.split("/").toList
|
||||
// process README.md or README.markdown
|
||||
val readme = files
|
||||
// process README
|
||||
val readme = files // files should be sorted alphabetically.
|
||||
.find { file =>
|
||||
!file.isDirectory && readmeFiles.contains(file.name.toLowerCase)
|
||||
!file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase)
|
||||
}
|
||||
.map { file =>
|
||||
val path = (file.name :: parentPath.reverse).reverse
|
||||
@@ -951,6 +1070,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
||||
repository,
|
||||
if (path == ".") Nil else path.split("/").toList, // current path
|
||||
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||
getCommitStatusWithSummary(repository.owner, repository.name, lastModifiedCommit.getName),
|
||||
commitCount,
|
||||
files,
|
||||
readme,
|
||||
|
||||
@@ -48,7 +48,6 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
)(RepositoryOperation.apply),
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
|
||||
"limitVisibleRepositories" -> trim(label("limitVisibleRepositories", boolean())),
|
||||
"ssh" -> mapping(
|
||||
"enabled" -> trim(label("SSH access", boolean())),
|
||||
@@ -109,7 +108,10 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||
"timeout" -> trim(label("Timeout", long(required))),
|
||||
"largeMaxFileSize" -> trim(label("Max file size for large file", long(required))),
|
||||
"largeTimeout" -> trim(label("Timeout for large file", long(required)))
|
||||
)(Upload.apply)
|
||||
)(Upload.apply),
|
||||
"repositoryViewer" -> mapping(
|
||||
"maxFiles" -> trim(label("Max files", number(required)))
|
||||
)(RepositoryViewerSettings.apply)
|
||||
)(SystemSettings.apply).verifying { settings =>
|
||||
Vector(
|
||||
if (settings.ssh.enabled && settings.baseUrl.isEmpty) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.controller
|
||||
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.model.activity.{CreateWikiPageInfo, DeleteWikiInfo, EditWikiPageInfo}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.service.WebHookService.WebHookGollumPayload
|
||||
import gitbucket.core.wiki.html
|
||||
@@ -13,6 +14,7 @@ import gitbucket.core.util.Directory._
|
||||
import org.scalatra.forms._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.i18n.Messages
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
class WikiController
|
||||
@@ -24,6 +26,7 @@ class WikiController
|
||||
with WebHookService
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator
|
||||
with RequestCache
|
||||
|
||||
trait WikiControllerBase extends ControllerBase {
|
||||
self: WikiService
|
||||
@@ -184,13 +187,9 @@ trait WikiControllerBase extends ControllerBase {
|
||||
).foreach {
|
||||
commitId =>
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordEditWikiPageActivity(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
form.pageName,
|
||||
commitId
|
||||
)
|
||||
val wikiEditInfo =
|
||||
EditWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
|
||||
recordActivity(wikiEditInfo)
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
|
||||
getAccountByUserName(repository.owner).map { repositoryUser =>
|
||||
WebHookGollumPayload("edited", form.pageName, commitId, repository, repositoryUser, loginAccount)
|
||||
@@ -228,7 +227,9 @@ trait WikiControllerBase extends ControllerBase {
|
||||
).foreach {
|
||||
commitId =>
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
||||
val createWikiPageInfo =
|
||||
CreateWikiPageInfo(repository.owner, repository.name, loginAccount.userName, form.pageName)
|
||||
recordActivity(createWikiPageInfo)
|
||||
callWebHookOf(repository.owner, repository.name, WebHook.Gollum, context.settings) {
|
||||
getAccountByUserName(repository.owner).map { repositoryUser =>
|
||||
WebHookGollumPayload("created", form.pageName, commitId, repository, repositoryUser, loginAccount)
|
||||
@@ -250,14 +251,13 @@ trait WikiControllerBase extends ControllerBase {
|
||||
val pageName = StringUtil.urlDecode(params("page"))
|
||||
|
||||
defining(context.loginAccount.get) { loginAccount =>
|
||||
deleteWikiPage(
|
||||
val deleteWikiInfo = DeleteWikiInfo(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
pageName,
|
||||
loginAccount.fullName,
|
||||
loginAccount.mailAddress,
|
||||
s"Destroyed ${pageName}"
|
||||
loginAccount.userName,
|
||||
pageName
|
||||
)
|
||||
recordActivity(deleteWikiInfo)
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
|
||||
redirect(s"/${repository.owner}/${repository.name}/wiki")
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api.{ApiObject, ApiRef, JsonFormat}
|
||||
import gitbucket.core.api.{ApiObject, ApiRef, CreateARef, JsonFormat, UpdateARef}
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.util.Directory.getRepositoryDir
|
||||
import gitbucket.core.util.ReferrerAuthenticator
|
||||
import gitbucket.core.util.Implicits._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.eclipse.jgit.lib.RefUpdate.Result
|
||||
import org.scalatra.{BadRequest, NoContent, UnprocessableEntity}
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
@@ -17,7 +20,7 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||
|
||||
/*
|
||||
* i. Get a reference
|
||||
* https://developer.github.com/v3/git/refs/#get-a-reference
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#get-a-reference
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/git/ref/*")(referrersOnly { repository =>
|
||||
getRef()
|
||||
@@ -55,21 +58,79 @@ trait ApiGitReferenceControllerBase extends ControllerBase {
|
||||
|
||||
/*
|
||||
* ii. Get all references
|
||||
* https://developer.github.com/v3/git/refs/#get-all-references
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#list-matching-references
|
||||
*/
|
||||
|
||||
/*
|
||||
* iii. Create a reference
|
||||
* https://developer.github.com/v3/git/refs/#create-a-reference
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-reference
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/git/refs")(referrersOnly { _ =>
|
||||
extractFromJsonBody[CreateARef].map {
|
||||
data =>
|
||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
||||
val ref = git.getRepository.findRef(data.ref)
|
||||
if (ref == null) {
|
||||
val update = git.getRepository.updateRef(data.ref)
|
||||
update.setNewObjectId(ObjectId.fromString(data.sha))
|
||||
val result = update.update()
|
||||
result match {
|
||||
case Result.NEW => JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName)))
|
||||
case _ => UnprocessableEntity(result.name())
|
||||
}
|
||||
} else {
|
||||
UnprocessableEntity("Ref already exists.")
|
||||
}
|
||||
}
|
||||
} getOrElse BadRequest()
|
||||
})
|
||||
|
||||
/*
|
||||
* iv. Update a reference
|
||||
* https://developer.github.com/v3/git/refs/#update-a-reference
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ =>
|
||||
val refName = multiParams("splat").mkString("/")
|
||||
extractFromJsonBody[UpdateARef].map {
|
||||
data =>
|
||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
||||
val ref = git.getRepository.findRef(refName)
|
||||
if (ref == null) {
|
||||
UnprocessableEntity("Ref does not exist.")
|
||||
} else {
|
||||
val update = git.getRepository.updateRef(ref.getName)
|
||||
update.setNewObjectId(ObjectId.fromString(data.sha))
|
||||
update.setForceUpdate(data.force)
|
||||
val result = update.update()
|
||||
result match {
|
||||
case Result.FORCED | Result.FAST_FORWARD | Result.NO_CHANGE =>
|
||||
JsonFormat(ApiRef(update.getName, ApiObject(update.getNewObjectId.getName)))
|
||||
case _ => UnprocessableEntity(result.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
} getOrElse BadRequest()
|
||||
})
|
||||
|
||||
/*
|
||||
* v. Delete a reference
|
||||
* https://developer.github.com/v3/git/refs/#delete-a-reference
|
||||
*/
|
||||
* v. Delete a reference
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/git#delete-a-reference
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/git/refs/*")(referrersOnly { _ =>
|
||||
val refName = multiParams("splat").mkString("/")
|
||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
||||
val ref = git.getRepository.findRef(refName)
|
||||
if (ref == null) {
|
||||
UnprocessableEntity("Ref does not exist.")
|
||||
} else {
|
||||
val update = git.getRepository.updateRef(ref.getName)
|
||||
update.setForceUpdate(true)
|
||||
val result = update.delete()
|
||||
result match {
|
||||
case Result.FORCED => NoContent()
|
||||
case _ => UnprocessableEntity(result.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import gitbucket.core.controller.{Context, ControllerBase}
|
||||
import gitbucket.core.service._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.{ReadableUsersAuthenticator, ReferrerAuthenticator, RepositoryName}
|
||||
import org.scalatra.{ActionResult, NoContent}
|
||||
|
||||
trait ApiIssueCommentControllerBase extends ControllerBase {
|
||||
self: AccountService
|
||||
@@ -14,8 +15,8 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
|
||||
with ReadableUsersAuthenticator
|
||||
with ReferrerAuthenticator =>
|
||||
/*
|
||||
* i. List comments on an issue
|
||||
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||
* i. List issue comments for a repository
|
||||
* https://docs.github.com/en/rest/reference/issues#list-issue-comments-for-a-repository
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
||||
(for {
|
||||
@@ -30,18 +31,90 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
/*
|
||||
* ii. List comments in a repository
|
||||
* https://developer.github.com/v3/issues/comments/#list-comments-in-a-repository
|
||||
* ii. Get an issue comment
|
||||
* https://docs.github.com/en/rest/reference/issues#get-an-issue-comment
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/issues/comments/:id")(referrersOnly { repository =>
|
||||
val commentId = params("id").toInt
|
||||
getCommentForApi(repository.owner, repository.name, commentId) match {
|
||||
case Some((issueComment, user, issue)) =>
|
||||
JsonFormat(
|
||||
ApiComment(issueComment, RepositoryName(repository), issue.issueId, ApiUser(user), issue.isPullRequest)
|
||||
)
|
||||
case _ => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* iii. Update an issue comment
|
||||
* https://docs.github.com/en/rest/reference/issues#update-an-issue-comment
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/issues/comments/:id")(readableUsersOnly { repository =>
|
||||
val commentId = params("id")
|
||||
val result = for {
|
||||
issueComment <- getComment(repository.owner, repository.name, commentId)
|
||||
issue <- getIssue(repository.owner, repository.name, issueComment.issueId.toString)
|
||||
} yield {
|
||||
if (isEditable(repository.owner, repository.name, issueComment.commentedUserName)) {
|
||||
val body = extractFromJsonBody[CreateAComment].map(_.body)
|
||||
updateCommentByApi(repository, issue, issueComment.commentId.toString, body)
|
||||
getComment(repository.owner, repository.name, commentId) match {
|
||||
case Some(issueComment) =>
|
||||
JsonFormat(
|
||||
ApiComment(
|
||||
issueComment,
|
||||
RepositoryName(repository),
|
||||
issue.issueId,
|
||||
ApiUser(context.loginAccount.get),
|
||||
issue.isPullRequest
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
}
|
||||
} else {
|
||||
Unauthorized()
|
||||
}
|
||||
}
|
||||
result match {
|
||||
case Some(response) => response
|
||||
case None => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* iv. Delete a comment
|
||||
* https://docs.github.com/en/rest/reference/issues#delete-an-issue-comment
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repo/issues/comments/:id")(readableUsersOnly { repository =>
|
||||
val maybeDeleteResponse: Option[Either[ActionResult, Option[Int]]] =
|
||||
for {
|
||||
commentId <- params("id").toIntOpt
|
||||
comment <- getComment(repository.owner, repository.name, commentId.toString)
|
||||
issue <- getIssue(repository.owner, repository.name, comment.issueId.toString)
|
||||
} yield {
|
||||
if (isEditable(repository.owner, repository.name, comment.commentedUserName)) {
|
||||
val maybeDeletedComment = deleteCommentByApi(repository, comment, issue)
|
||||
Right(maybeDeletedComment.map(_.commentId))
|
||||
} else {
|
||||
Left(Unauthorized())
|
||||
}
|
||||
}
|
||||
maybeDeleteResponse
|
||||
.map {
|
||||
case Right(maybeDeletedCommentId) => maybeDeletedCommentId.getOrElse(NotFound())
|
||||
case Left(err) => err
|
||||
}
|
||||
.getOrElse(NotFound())
|
||||
})
|
||||
|
||||
/*
|
||||
* v. List issue comments
|
||||
* https://docs.github.com/en/rest/reference/issues#list-issue-comments
|
||||
*/
|
||||
|
||||
/*
|
||||
* iii. Get a single comment
|
||||
* https://developer.github.com/v3/issues/comments/#get-a-single-comment
|
||||
*/
|
||||
|
||||
/*
|
||||
* iv. Create a comment
|
||||
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||
* vi. Create an issue comment
|
||||
* https://docs.github.com/en/rest/reference/issues#create-an-issue-comment
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
||||
(for {
|
||||
@@ -64,16 +137,6 @@ trait ApiIssueCommentControllerBase extends ControllerBase {
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* v. Edit a comment
|
||||
* https://developer.github.com/v3/issues/comments/#edit-a-comment
|
||||
*/
|
||||
|
||||
/*
|
||||
* vi. Delete a comment
|
||||
* https://developer.github.com/v3/issues/comments/#delete-a-comment
|
||||
*/
|
||||
|
||||
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
|
||||
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.service.MilestonesService
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import org.scalatra.NoContent
|
||||
|
||||
trait ApiIssueMilestoneControllerBase extends ControllerBase {
|
||||
self: MilestonesService with WritableUsersAuthenticator with ReferrerAuthenticator =>
|
||||
|
||||
/*
|
||||
* i. List milestones
|
||||
* https://docs.github.com/en/rest/reference/issues#list-milestones
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/milestones")(referrersOnly { repository =>
|
||||
val state = params.getOrElse("state", "all")
|
||||
// TODO "sort", "direction" params should be implemented.
|
||||
val apiMilestones = (for (milestoneWithIssue <- getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.sortBy(p => p._1.milestoneId))
|
||||
yield {
|
||||
ApiMilestone(
|
||||
repository.repository,
|
||||
milestoneWithIssue._1,
|
||||
milestoneWithIssue._2,
|
||||
milestoneWithIssue._3
|
||||
)
|
||||
}).reverse
|
||||
state match {
|
||||
case "all" => JsonFormat(apiMilestones)
|
||||
case "open" | "closed" =>
|
||||
JsonFormat(
|
||||
apiMilestones.filter(p => p.state == state)
|
||||
)
|
||||
case _ => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* ii. Create a milestone
|
||||
* https://docs.github.com/en/rest/reference/issues#create-a-milestone
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/milestones")(writableUsersOnly { repository =>
|
||||
(for {
|
||||
data <- extractFromJsonBody[CreateAMilestone] if data.isValid
|
||||
milestoneId = createMilestone(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
data.title,
|
||||
data.description,
|
||||
data.due_on
|
||||
)
|
||||
apiMilestone <- getApiMilestone(repository, milestoneId)
|
||||
} yield {
|
||||
JsonFormat(apiMilestone)
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* iii. Get a milestone
|
||||
* https://docs.github.com/en/rest/reference/issues#get-a-milestone
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/milestones/:number")(referrersOnly { repository =>
|
||||
val milestoneId = params("number").toInt // use milestoneId as number
|
||||
(for (apiMilestone <- getApiMilestone(repository, milestoneId)) yield {
|
||||
JsonFormat(apiMilestone)
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* iv.Update a milestone
|
||||
* https://docs.github.com/en/rest/reference/issues#update-a-milestone
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/milestones/:number")(writableUsersOnly { repository =>
|
||||
val milestoneId = params("number").toInt
|
||||
(for {
|
||||
data <- extractFromJsonBody[CreateAMilestone] if data.isValid
|
||||
milestone <- getMilestone(repository.owner, repository.name, milestoneId)
|
||||
_ = (data.state, milestone.closedDate) match {
|
||||
case ("open", Some(_)) =>
|
||||
openMilestone(milestone)
|
||||
case ("closed", None) =>
|
||||
closeMilestone(milestone)
|
||||
case _ =>
|
||||
}
|
||||
milestone <- getMilestone(repository.owner, repository.name, milestoneId)
|
||||
_ = updateMilestone(milestone.copy(title = data.title, description = data.description, dueDate = data.due_on))
|
||||
apiMilestone <- getApiMilestone(repository, milestoneId)
|
||||
} yield {
|
||||
JsonFormat(apiMilestone)
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* v. Delete a milestone
|
||||
* https://docs.github.com/en/rest/reference/issues#delete-a-milestone
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/milestones/:number")(writableUsersOnly { repository =>
|
||||
val milestoneId = params("number").toInt // use milestoneId as number
|
||||
deleteMilestone(repository.owner, repository.name, milestoneId)
|
||||
NoContent()
|
||||
})
|
||||
|
||||
private def getApiMilestone(repository: RepositoryInfo, milestoneId: Int): Option[ApiMilestone] = {
|
||||
getMilestonesWithIssueCount(repository.owner, repository.name)
|
||||
.find(p => p._1.milestoneId == milestoneId)
|
||||
.map(
|
||||
milestoneWithIssue =>
|
||||
ApiMilestone(
|
||||
repository.repository,
|
||||
milestoneWithIssue._1,
|
||||
milestoneWithIssue._2,
|
||||
milestoneWithIssue._3
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.NoContent
|
||||
import org.scalatra.{Conflict, MethodNotAllowed, NoContent, Ok}
|
||||
import scala.util.Using
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
@@ -161,8 +161,28 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
|
||||
/*
|
||||
* v. Update a pull request
|
||||
* https://developer.github.com/v3/pulls/#update-a-pull-request
|
||||
* https://docs.github.com/en/rest/reference/pulls#update-a-pull-request
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
||||
(for {
|
||||
issueId <- params("id").toIntOpt
|
||||
account <- context.loginAccount
|
||||
settings = context.settings
|
||||
data <- extractFromJsonBody[UpdateAPullRequest]
|
||||
} yield {
|
||||
updatePullRequestsByApi(
|
||||
repository,
|
||||
issueId,
|
||||
account,
|
||||
settings,
|
||||
data.title,
|
||||
data.body,
|
||||
data.state,
|
||||
data.base
|
||||
)
|
||||
JsonFormat(getApiPullRequest(repository, issueId))
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* vi. List commits on a pull request
|
||||
@@ -217,8 +237,72 @@ trait ApiPullRequestControllerBase extends ControllerBase {
|
||||
|
||||
/*
|
||||
* ix. Merge a pull request (Merge Button)
|
||||
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
|
||||
* https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request
|
||||
*/
|
||||
put("/api/v3/repos/:owner/:repository/pulls/:id/merge")(referrersOnly { repository =>
|
||||
(for {
|
||||
//TODO: crash when body is empty
|
||||
//TODO: Implement sha parameter
|
||||
data <- extractFromJsonBody[MergeAPullRequest]
|
||||
issueId <- params("id").toIntOpt
|
||||
(issue, pullReq) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||
} yield {
|
||||
if (checkConflict(repository.owner, repository.name, pullReq.branch, issueId).isDefined) {
|
||||
Conflict(
|
||||
JsonFormat(
|
||||
FailToMergePrResponse(
|
||||
message = "Head branch was modified. Review and try the merge again.",
|
||||
documentation_url = "https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request",
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
if (issue.closed) {
|
||||
MethodNotAllowed(
|
||||
JsonFormat(
|
||||
FailToMergePrResponse(
|
||||
message = "Pull Request is not mergeable, Closed",
|
||||
documentation_url = "https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request",
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val strategy =
|
||||
if (data.merge_method.getOrElse("merge-commit") == "merge") "merge-commit"
|
||||
else data.merge_method.getOrElse("merge-commit")
|
||||
mergePullRequest(
|
||||
repository,
|
||||
issueId,
|
||||
context.loginAccount.get,
|
||||
data.commit_message.getOrElse(""), //TODO: Implement commit_title
|
||||
strategy,
|
||||
pullReq.isDraft,
|
||||
context.settings
|
||||
) match {
|
||||
case Right(objectId) =>
|
||||
Ok(
|
||||
JsonFormat(
|
||||
SuccessToMergePrResponse(
|
||||
sha = objectId.toString,
|
||||
merged = true,
|
||||
message = "Pull Request successfully merged"
|
||||
)
|
||||
)
|
||||
)
|
||||
case Left(message) =>
|
||||
MethodNotAllowed(
|
||||
JsonFormat(
|
||||
FailToMergePrResponse(
|
||||
message = "Pull Request is not mergeable",
|
||||
documentation_url = "https://docs.github.com/en/rest/reference/pulls#merge-a-pull-request",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
* x. Labels, assignees, and milestones
|
||||
|
||||
@@ -7,6 +7,8 @@ import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.JGitUtil.getBranches
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.scalatra.NoContent
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
@@ -22,7 +24,7 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
|
||||
/**
|
||||
* i. List branches
|
||||
* https://developer.github.com/v3/repos/branches/#list-branches
|
||||
* https://docs.github.com/en/rest/reference/repos#list-branches
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/branches")(referrersOnly { repository =>
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
@@ -41,8 +43,8 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
/**
|
||||
* ii. Get branch
|
||||
* https://developer.github.com/v3/repos/branches/#get-branch
|
||||
* ii. Get a branch
|
||||
* https://docs.github.com/en/rest/reference/repos#get-a-branch
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/branches/*")(referrersOnly { repository =>
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) {
|
||||
@@ -65,147 +67,206 @@ trait ApiRepositoryBranchControllerBase extends ControllerBase {
|
||||
|
||||
/*
|
||||
* iii. Get branch protection
|
||||
* https://developer.github.com/v3/repos/branches/#get-branch-protection
|
||||
* https://docs.github.com/en/rest/reference/repos#get-branch-protection
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/branches/:branch/protection")(referrersOnly { repository =>
|
||||
val branch = params("branch")
|
||||
if (repository.branchList.contains(branch)) {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
JsonFormat(
|
||||
ApiBranchProtection(protection)
|
||||
)
|
||||
} else { NotFound() }
|
||||
})
|
||||
|
||||
/*
|
||||
* iv. Update branch protection
|
||||
* https://developer.github.com/v3/repos/branches/#update-branch-protection
|
||||
* https://docs.github.com/en/rest/reference/repos#update-branch-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* v. Remove branch protection
|
||||
* https://developer.github.com/v3/repos/branches/#remove-branch-protection
|
||||
* v. Delete branch protection
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-branch-protection
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/branches/:branch/protection")(writableUsersOnly { repository =>
|
||||
val branch = params("branch")
|
||||
if (repository.branchList.contains(branch)) {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
if (protection.enabled) {
|
||||
disableBranchProtection(repository.owner, repository.name, branch)
|
||||
NoContent()
|
||||
} else NotFound()
|
||||
} else NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* vi. Get admin branch protection
|
||||
* https://docs.github.com/en/rest/reference/repos#get-admin-branch-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* vi. Get required status checks of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#get-required-status-checks-of-protected-branch
|
||||
* vii. Set admin branch protection
|
||||
* https://docs.github.com/en/rest/reference/repos#set-admin-branch-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* vii. Update required status checks of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#update-required-status-checks-of-protected-branch
|
||||
* viii. Delete admin branch protection
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-admin-branch-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* viii. Remove required status checks of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-required-status-checks-of-protected-branch
|
||||
* ix. Get pull request review protection
|
||||
* https://docs.github.com/en/rest/reference/repos#get-pull-request-review-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* ix. List required status checks contexts of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#list-required-status-checks-contexts-of-protected-branch
|
||||
* x. Update pull request review protection
|
||||
* https://docs.github.com/en/rest/reference/repos#update-pull-request-review-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* x. Replace required status checks contexts of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#replace-required-status-checks-contexts-of-protected-branch
|
||||
* xi. Delete pull request review protection
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-pull-request-review-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* xi. Add required status checks contexts of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-required-status-checks-contexts-of-protected-branch
|
||||
* xii. Get commit signature protection
|
||||
* https://docs.github.com/en/rest/reference/repos#get-commit-signature-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* xii. Remove required status checks contexts of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-required-status-checks-contexts-of-protected-branch
|
||||
* xiii. Create commit signature protection
|
||||
* https://docs.github.com/en/rest/reference/repos#create-commit-signature-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* xiii. Get pull request review enforcement of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#get-pull-request-review-enforcement-of-protected-branch
|
||||
* xiv. Delete commit signature protection
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-commit-signature-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* xiv. Update pull request review enforcement of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#update-pull-request-review-enforcement-of-protected-branch
|
||||
* xv. Get status checks protection
|
||||
* https://docs.github.com/en/rest/reference/repos#get-status-checks-protection
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/branches/:branch/protection/required_status_checks")(referrersOnly {
|
||||
repository =>
|
||||
val branch = params("branch")
|
||||
if (repository.branchList.contains(branch)) {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
JsonFormat(
|
||||
ApiBranchProtection(protection).required_status_checks
|
||||
)
|
||||
} else { NotFound() }
|
||||
})
|
||||
|
||||
/*
|
||||
* xvi. Update status check protection
|
||||
* https://docs.github.com/en/rest/reference/repos#update-status-check-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* xv. Remove pull request review enforcement of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-pull-request-review-enforcement-of-protected-branch
|
||||
* xvii. Remove status check protection
|
||||
* https://docs.github.com/en/rest/reference/repos#remove-status-check-protection
|
||||
*/
|
||||
|
||||
/*
|
||||
* xvi. Get required signatures of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#get-required-signatures-of-protected-branch
|
||||
* xviii. Get all status check contexts
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-all-status-check-contexts
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/branches/:branch/protection/required_status_checks/contexts")(referrersOnly {
|
||||
repository =>
|
||||
val branch = params("branch")
|
||||
if (repository.branchList.contains(branch)) {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
if (protection.enabled) {
|
||||
protection.contexts.toList
|
||||
} else NotFound()
|
||||
} else NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* xix. Add status check contexts
|
||||
* https://docs.github.com/en/rest/reference/repos#add-status-check-contexts
|
||||
*/
|
||||
|
||||
/*
|
||||
* xvii. Add required signatures of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#add-required-signatures-of-protected-branch
|
||||
* xx. Set status check contexts
|
||||
* https://docs.github.com/en/rest/reference/repos#set-status-check-contexts
|
||||
*/
|
||||
|
||||
/*
|
||||
* xviii. Remove required signatures of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-required-signatures-of-protected-branch
|
||||
* xxi. Remove status check contexts
|
||||
* https://docs.github.com/en/rest/reference/repos#remove-status-check-contexts
|
||||
*/
|
||||
|
||||
/*
|
||||
* xix. Get admin enforcement of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#get-admin-enforcement-of-protected-branch
|
||||
* xxii. Get access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#get-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xx. Add admin enforcement of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#add-admin-enforcement-of-protected-branch
|
||||
* xxiii. Delete access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxi. Remove admin enforcement of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-admin-enforcement-of-protected-branch
|
||||
* xxiv. Get apps with access to the protected branch
|
||||
* https://docs.github.com/en/rest/reference/repos#get-apps-with-access-to-the-protected-branch
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxii. Get restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#get-restrictions-of-protected-branch
|
||||
* xxv. Add app access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#add-app-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxiii. Remove restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-restrictions-of-protected-branch
|
||||
* xxvi. Set app access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#set-app-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxiv. List team restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#list-team-restrictions-of-protected-branch
|
||||
* xxvii. Remove app access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#remove-app-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxv. Replace team restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#replace-team-restrictions-of-protected-branch
|
||||
* xxviii. Get teams with access to the protected branch
|
||||
* https://docs.github.com/en/rest/reference/repos#get-teams-with-access-to-the-protected-branch
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxvi. Add team restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#add-team-restrictions-of-protected-branch
|
||||
* xxix. Add team access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#add-team-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxvii. Remove team restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-team-restrictions-of-protected-branch
|
||||
* xxx. Set team access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#set-team-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxviii. List user restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#list-user-restrictions-of-protected-branch
|
||||
* xxxi. Remove team access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#remove-team-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxix. Replace user restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#replace-user-restrictions-of-protected-branch
|
||||
* xxxii. Get users with access to the protected branch
|
||||
* https://docs.github.com/en/rest/reference/repos#get-users-with-access-to-the-protected-branch
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxx. Add user restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#add-user-restrictions-of-protected-branch
|
||||
* xxxiii. Add user access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#add-user-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxxi. Remove user restrictions of protected branch
|
||||
* https://developer.github.com/v3/repos/branches/#remove-user-restrictions-of-protected-branch
|
||||
* xxxiv. Set user access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#set-user-access-restrictions
|
||||
*/
|
||||
|
||||
/*
|
||||
* xxxv. Remove user access restrictions
|
||||
* https://docs.github.com/en/rest/reference/repos#remove-user-access-restrictions
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api.{AddACollaborator, ApiUser, JsonFormat}
|
||||
import gitbucket.core.api.{AddACollaborator, ApiRepositoryCollaborator, ApiUser, JsonFormat}
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.service.{AccountService, RepositoryService}
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -10,8 +10,8 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase {
|
||||
self: RepositoryService with AccountService with ReferrerAuthenticator with OwnerAuthenticator =>
|
||||
|
||||
/*
|
||||
* i. List collaborators
|
||||
* https://developer.github.com/v3/repos/collaborators/#list-collaborators
|
||||
* i. List repository collaborators
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#list-repository-collaborators
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/collaborators")(referrersOnly { repository =>
|
||||
// TODO Should ApiUser take permission? getCollaboratorUserNames does not return owner group members.
|
||||
@@ -19,19 +19,40 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase {
|
||||
getCollaboratorUserNames(params("owner"), params("repository")).map(u => ApiUser(getAccountByUserName(u).get))
|
||||
)
|
||||
})
|
||||
|
||||
/*
|
||||
* ii. Check if a user is a collaborator
|
||||
* https://developer.github.com/v3/repos/collaborators/#check-if-a-user-is-a-collaborator
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#check-if-a-user-is-a-repository-collaborator
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/collaborators/:userName")(referrersOnly { repository =>
|
||||
(for (account <- getAccountByUserName(params("userName"))) yield {
|
||||
if (getCollaboratorUserNames(repository.owner, repository.name).contains(account.userName)) {
|
||||
NoContent()
|
||||
} else {
|
||||
NotFound()
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* iii. Review a user's permission level
|
||||
* https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level
|
||||
* iii. Get repository permissions for a user
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-repository-permissions-for-a-user
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/collaborators/:userName/permission")(referrersOnly { repository =>
|
||||
(for {
|
||||
account <- getAccountByUserName(params("userName"))
|
||||
collaborator <- getCollaborators(repository.owner, repository.name)
|
||||
.find(p => p._1.collaboratorName == account.userName)
|
||||
} yield {
|
||||
JsonFormat(
|
||||
ApiRepositoryCollaborator(collaborator._1.role, ApiUser(account))
|
||||
)
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* iv. Add user as a collaborator
|
||||
* https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator
|
||||
* iv. Add a repository collaborator
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#add-a-repository-collaborator
|
||||
* requested #1586
|
||||
*/
|
||||
put("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository =>
|
||||
@@ -44,8 +65,8 @@ trait ApiRepositoryCollaboratorControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
/*
|
||||
* v. Remove user as a collaborator
|
||||
* https://developer.github.com/v3/repos/collaborators/#remove-user-as-a-collaborator
|
||||
* v. Remove a repository collaborator
|
||||
* https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#remove-a-repository-collaborator
|
||||
* requested #1586
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/collaborators/:userName")(ownerOnly { repository =>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api.{ApiCommits, JsonFormat}
|
||||
import gitbucket.core.api.{ApiBranchCommit, ApiBranchForHeadCommit, ApiCommits, JsonFormat}
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.model.Account
|
||||
import gitbucket.core.service.{AccountService, CommitsService}
|
||||
import gitbucket.core.service.{AccountService, CommitsService, ProtectedBranchService}
|
||||
import gitbucket.core.util.Directory.getRepositoryDir
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import gitbucket.core.util.JGitUtil.{CommitInfo, getBranches, getBranchesOfCommit}
|
||||
import gitbucket.core.util.{JGitUtil, ReferrerAuthenticator, RepositoryName}
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.revwalk.RevWalk
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Using
|
||||
|
||||
trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||
self: AccountService with CommitsService with ReferrerAuthenticator =>
|
||||
self: AccountService with CommitsService with ProtectedBranchService with ReferrerAuthenticator =>
|
||||
/*
|
||||
* i. List commits on a repository
|
||||
* https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository
|
||||
@@ -22,7 +23,7 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||
val owner = repository.owner
|
||||
val name = repository.name
|
||||
// TODO: The following parameters need to be implemented. [:path, :author, :since, :until]
|
||||
val sha = if (request.body.nonEmpty) (parse(request.body) \ "sha").extract[String] else "refs/heads/master";
|
||||
val sha = params.getOrElse("sha", "refs/heads/master")
|
||||
Using.resource(Git.open(getRepositoryDir(owner, name))) {
|
||||
git =>
|
||||
val repo = git.getRepository
|
||||
@@ -107,7 +108,25 @@ trait ApiRepositoryCommitControllerBase extends ControllerBase {
|
||||
*/
|
||||
|
||||
/*
|
||||
* v. Commit signature verification
|
||||
* https://developer.github.com/v3/repos/commits/#commit-signature-verification
|
||||
*/
|
||||
* v. Commit signature verification
|
||||
* https://developer.github.com/v3/repos/commits/#commit-signature-verification
|
||||
*/
|
||||
|
||||
/*
|
||||
* vi. List branches for HEAD commit
|
||||
* https://docs.github.com/en/rest/reference/repos#list-branches-for-head-commit
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/commits/:sha/branches-where-head")(referrersOnly { repository =>
|
||||
val sha = params("sha")
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
val apiBranchForCommits = for {
|
||||
branch <- getBranchesOfCommit(git, sha)
|
||||
br <- getBranches(git, branch, repository.repository.originUserName.isEmpty).find(_.name == branch)
|
||||
} yield {
|
||||
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
|
||||
ApiBranchForHeadCommit(branch, ApiBranchCommit(br.commitId), protection.enabled)
|
||||
}
|
||||
JsonFormat(apiBranchForCommits)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api.{ApiContents, ApiError, CreateAFile, JsonFormat}
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.{RepositoryCommitFileService, RepositoryService}
|
||||
import gitbucket.core.util.Directory.getRepositoryDir
|
||||
import gitbucket.core.util.JGitUtil.{FileInfo, getContentFromId, getFileList}
|
||||
@@ -8,15 +9,30 @@ import gitbucket.core.util._
|
||||
import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
|
||||
import gitbucket.core.util.Implicits._
|
||||
import org.eclipse.jgit.api.Git
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
||||
self: ReferrerAuthenticator with WritableUsersAuthenticator with RepositoryCommitFileService =>
|
||||
|
||||
/*
|
||||
* i. Get the README
|
||||
* https://developer.github.com/v3/repos/contents/#get-the-readme
|
||||
/**
|
||||
* i. Get a repository README
|
||||
* https://docs.github.com/en/rest/reference/repos#get-a-repository-readme
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/readme")(referrersOnly { repository =>
|
||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) {
|
||||
git =>
|
||||
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
|
||||
val files = getFileList(git, refStr, ".", maxFiles = context.settings.repositoryViewer.maxFiles)
|
||||
files // files should be sorted alphabetically.
|
||||
.find { file =>
|
||||
!file.isDirectory && RepositoryService.readmeFiles.contains(file.name.toLowerCase)
|
||||
} match {
|
||||
case Some(x) => getContents(repository = repository, path = x.name, refStr = refStr, ignoreCase = true)
|
||||
case _ => NotFound()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* ii. Get contents
|
||||
@@ -34,21 +50,32 @@ trait ApiRepositoryContentsControllerBase extends ControllerBase {
|
||||
getContents(repository, multiParams("splat").head, params.getOrElse("ref", repository.repository.defaultBranch))
|
||||
})
|
||||
|
||||
private def getContents(repository: RepositoryService.RepositoryInfo, path: String, refStr: String) = {
|
||||
def getFileInfo(git: Git, revision: String, pathStr: String): Option[FileInfo] = {
|
||||
private def getContents(
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
path: String,
|
||||
refStr: String,
|
||||
ignoreCase: Boolean = false
|
||||
) = {
|
||||
def getFileInfo(git: Git, revision: String, pathStr: String, ignoreCase: Boolean): Option[FileInfo] = {
|
||||
val (dirName, fileName) = pathStr.lastIndexOf('/') match {
|
||||
case -1 =>
|
||||
(".", pathStr)
|
||||
case n =>
|
||||
(pathStr.take(n), pathStr.drop(n + 1))
|
||||
}
|
||||
getFileList(git, revision, dirName).find(f => f.name.equals(fileName))
|
||||
if (ignoreCase) {
|
||||
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
|
||||
.find(_.name.toLowerCase.equals(fileName.toLowerCase))
|
||||
} else {
|
||||
getFileList(git, revision, dirName, maxFiles = context.settings.repositoryViewer.maxFiles)
|
||||
.find(_.name.equals(fileName))
|
||||
}
|
||||
}
|
||||
|
||||
Using.resource(Git.open(getRepositoryDir(params("owner"), params("repository")))) { git =>
|
||||
val fileList = getFileList(git, refStr, path)
|
||||
val fileList = getFileList(git, refStr, path, maxFiles = context.settings.repositoryViewer.maxFiles)
|
||||
if (fileList.isEmpty) { // file or NotFound
|
||||
getFileInfo(git, refStr, path)
|
||||
getFileInfo(git, refStr, path, ignoreCase)
|
||||
.flatMap { f =>
|
||||
val largeFile = params.get("large_file").exists(s => s.equals("true"))
|
||||
val content = getContentFromId(git, f.id, largeFile)
|
||||
|
||||
@@ -54,11 +54,15 @@ trait ApiRepositoryControllerBase extends ControllerBase {
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* iv. List all public repositories
|
||||
* https://developer.github.com/v3/repos/#list-all-public-repositories
|
||||
* Not implemented
|
||||
* https://developer.github.com/v3/repos/#list-public-repositories
|
||||
*/
|
||||
get("/api/v3/repositories") {
|
||||
JsonFormat(getPublicRepositories().map { r =>
|
||||
ApiRepository(r, getAccountByUserName(r.owner).get)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* v. Create
|
||||
@@ -174,9 +178,14 @@ trait ApiRepositoryControllerBase extends ControllerBase {
|
||||
*/
|
||||
|
||||
/*
|
||||
* xiii. List tags
|
||||
* https://developer.github.com/v3/repos/#list-tags
|
||||
* xiii. List repository tags
|
||||
* https://docs.github.com/en/rest/reference/repos#list-repository-tags
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/tags")(referrersOnly { repository =>
|
||||
JsonFormat(
|
||||
repository.tags.map(tagInfo => ApiTag(tagInfo.name, RepositoryName(repository), tagInfo.id))
|
||||
)
|
||||
})
|
||||
|
||||
/*
|
||||
* xiv. Delete a repository
|
||||
|
||||
@@ -47,7 +47,7 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase {
|
||||
ref <- params.get("ref")
|
||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||
} yield {
|
||||
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map {
|
||||
JsonFormat(getCommitStatusesWithCreator(repository.owner, repository.name, sha).map {
|
||||
case (status, creator) =>
|
||||
ApiCommitStatus(status, ApiUser(creator))
|
||||
})
|
||||
@@ -73,7 +73,7 @@ trait ApiRepositoryStatusControllerBase extends ControllerBase {
|
||||
owner <- getAccountByUserName(repository.owner)
|
||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||
} yield {
|
||||
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
||||
val statuses = getCommitStatusesWithCreator(repository.owner, repository.name, sha)
|
||||
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package gitbucket.core.controller.api
|
||||
import gitbucket.core.api._
|
||||
import gitbucket.core.controller.ControllerBase
|
||||
import gitbucket.core.model.{WebHook, WebHookContentType}
|
||||
import gitbucket.core.service.{RepositoryService, WebHookService}
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import org.scalatra.NoContent
|
||||
|
||||
trait ApiRepositoryWebhookControllerBase extends ControllerBase {
|
||||
self: RepositoryService with WebHookService with ReferrerAuthenticator with WritableUsersAuthenticator =>
|
||||
|
||||
/*
|
||||
* i. List repository webhooks
|
||||
* https://docs.github.com/en/rest/reference/repos#list-repository-webhooks
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/hooks")(referrersOnly { repository =>
|
||||
val apiWebhooks = for {
|
||||
(hook, events) <- getWebHooks(repository.owner, repository.name)
|
||||
} yield {
|
||||
ApiWebhook("Repository", hook, events)
|
||||
}
|
||||
JsonFormat(apiWebhooks)
|
||||
})
|
||||
|
||||
/*
|
||||
* ii. Create a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#create-a-repository-webhook
|
||||
*/
|
||||
post("/api/v3/repos/:owner/:repository/hooks")(writableUsersOnly { repository =>
|
||||
(for {
|
||||
data <- extractFromJsonBody[CreateARepositoryWebhook] if data.isValid
|
||||
ctype = if (data.config.content_type == "form") WebHookContentType.FORM else WebHookContentType.JSON
|
||||
events = data.events.map(p => WebHook.Event.valueOf(p)).toSet
|
||||
} yield {
|
||||
addWebHook(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
data.config.url,
|
||||
events,
|
||||
ctype,
|
||||
data.config.secret
|
||||
)
|
||||
getWebHook(repository.owner, repository.name, data.config.url) match {
|
||||
case Some(createdHook) => JsonFormat(ApiWebhook("Repository", createdHook._1, createdHook._2))
|
||||
case _ =>
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* iii. Get a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#get-a-repository-webhook
|
||||
*/
|
||||
get("/api/v3/repos/:owner/:repository/hooks/:id")(referrersOnly { repository =>
|
||||
val hookId = params("id").toInt
|
||||
getWebHookById(hookId) match {
|
||||
case Some(hook) => JsonFormat(ApiWebhook("Repository", hook._1, hook._2))
|
||||
case _ => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* iv. Update a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#update-a-repository-webhook
|
||||
*/
|
||||
patch("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository =>
|
||||
val hookId = params("id").toInt
|
||||
(for {
|
||||
data <- extractFromJsonBody[UpdateARepositoryWebhook] if data.isValid
|
||||
ctype = data.config.content_type match {
|
||||
case "json" => WebHookContentType.JSON
|
||||
case _ => WebHookContentType.FORM
|
||||
}
|
||||
} yield {
|
||||
val events = (data.events ++ data.add_events)
|
||||
.filterNot(p => data.remove_events.contains(p))
|
||||
.map(p => WebHook.Event.valueOf(p))
|
||||
.toSet
|
||||
updateWebHookByApi(
|
||||
hookId,
|
||||
repository.owner,
|
||||
repository.name,
|
||||
data.config.url,
|
||||
events,
|
||||
ctype,
|
||||
data.config.secret
|
||||
)
|
||||
getWebHookById(hookId) match {
|
||||
case Some(updatedHook) => JsonFormat(ApiWebhook("Repository", updatedHook._1, updatedHook._2))
|
||||
case _ =>
|
||||
}
|
||||
}) getOrElse NotFound()
|
||||
})
|
||||
|
||||
/*
|
||||
* v. Delete a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#delete-a-repository-webhook
|
||||
*/
|
||||
delete("/api/v3/repos/:owner/:repository/hooks/:id")(writableUsersOnly { repository =>
|
||||
val hookId = params("id").toInt
|
||||
getWebHookById(hookId) match {
|
||||
case Some(_) =>
|
||||
deleteWebHookById(params("id").toInt)
|
||||
NoContent()
|
||||
case _ => NotFound()
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
* vi. Ping a repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#ping-a-repository-webhook
|
||||
*/
|
||||
|
||||
/*
|
||||
* vi. Test the push repository webhook
|
||||
* https://docs.github.com/en/rest/reference/repos#test-the-push-repository-webhook
|
||||
*/
|
||||
|
||||
}
|
||||
21
src/main/scala/gitbucket/core/model/AccountPreference.scala
Normal file
21
src/main/scala/gitbucket/core/model/AccountPreference.scala
Normal file
@@ -0,0 +1,21 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
trait AccountPreferenceComponent { self: Profile =>
|
||||
import profile.api._
|
||||
|
||||
lazy val AccountPreferences = TableQuery[AccountPreferences]
|
||||
|
||||
class AccountPreferences(tag: Tag) extends Table[AccountPreference](tag, "ACCOUNT_PREFERENCE") {
|
||||
val userName = column[String]("USER_NAME", O PrimaryKey)
|
||||
val highlighterTheme = column[String]("HIGHLIGHTER_THEME")
|
||||
def * =
|
||||
(userName, highlighterTheme) <> (AccountPreference.tupled, AccountPreference.unapply)
|
||||
|
||||
def byPrimaryKey(userName: String): Rep[Boolean] = this.userName === userName.bind
|
||||
}
|
||||
}
|
||||
|
||||
case class AccountPreference(
|
||||
userName: String,
|
||||
highlighterTheme: String = "prettify"
|
||||
)
|
||||
@@ -1,5 +1,9 @@
|
||||
package gitbucket.core.model
|
||||
|
||||
/**
|
||||
* ActivityComponent has been deprecated, but keep it for binary compatibility.
|
||||
*/
|
||||
@deprecated("ActivityComponent has been deprecated, but keep it for binary compatibility.", "4.34.0")
|
||||
trait ActivityComponent extends TemplateComponent { self: Profile =>
|
||||
import profile.api._
|
||||
import self._
|
||||
@@ -7,14 +11,7 @@ trait ActivityComponent extends TemplateComponent { self: Profile =>
|
||||
lazy val Activities = TableQuery[Activities]
|
||||
|
||||
class Activities(tag: Tag) extends Table[Activity](tag, "ACTIVITY") with BasicTemplate {
|
||||
val activityId = column[Int]("ACTIVITY_ID", O AutoInc)
|
||||
val activityUserName = column[String]("ACTIVITY_USER_NAME")
|
||||
val activityType = column[String]("ACTIVITY_TYPE")
|
||||
val message = column[String]("MESSAGE")
|
||||
val additionalInfo = column[String]("ADDITIONAL_INFO")
|
||||
val activityDate = column[java.util.Date]("ACTIVITY_DATE")
|
||||
def * =
|
||||
(userName, repositoryName, activityUserName, activityType, message, additionalInfo.?, activityDate, activityId) <> (Activity.tupled, Activity.unapply)
|
||||
def * = ???
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +23,5 @@ case class Activity(
|
||||
message: String,
|
||||
additionalInfo: Option[String],
|
||||
activityDate: java.util.Date,
|
||||
activityId: Int = 0
|
||||
activityId: String
|
||||
)
|
||||
|
||||
@@ -45,7 +45,7 @@ trait CoreProfile
|
||||
with Profile
|
||||
with AccessTokenComponent
|
||||
with AccountComponent
|
||||
with ActivityComponent
|
||||
with ActivityComponent // ActivityComponent has been deprecated, but keep it for binary compatibility
|
||||
with CollaboratorComponent
|
||||
with CommitCommentComponent
|
||||
with CommitStatusComponent
|
||||
@@ -70,5 +70,6 @@ trait CoreProfile
|
||||
with ReleaseTagComponent
|
||||
with ReleaseAssetComponent
|
||||
with AccountExtraMailAddressComponent
|
||||
with AccountPreferenceComponent
|
||||
|
||||
object Profile extends CoreProfile
|
||||
|
||||
@@ -9,20 +9,25 @@ trait RepositoryWebHookComponent extends TemplateComponent { self: Profile =>
|
||||
lazy val RepositoryWebHooks = TableQuery[RepositoryWebHooks]
|
||||
|
||||
class RepositoryWebHooks(tag: Tag) extends Table[RepositoryWebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||
val hookId = column[Int]("HOOK_ID", O AutoInc)
|
||||
val url = column[String]("URL")
|
||||
val token = column[Option[String]]("TOKEN")
|
||||
val ctype = column[WebHookContentType]("CTYPE")
|
||||
def * =
|
||||
(userName, repositoryName, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply)
|
||||
(userName, repositoryName, hookId, url, ctype, token) <> ((RepositoryWebHook.apply _).tupled, RepositoryWebHook.unapply)
|
||||
|
||||
def byPrimaryKey(owner: String, repository: String, url: String) =
|
||||
def byRepositoryUrl(owner: String, repository: String, url: String) =
|
||||
byRepository(owner, repository) && (this.url === url.bind)
|
||||
|
||||
def byId(id: Int) =
|
||||
(this.hookId === id.bind)
|
||||
}
|
||||
}
|
||||
|
||||
case class RepositoryWebHook(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
hookId: Int = 0,
|
||||
url: String,
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
|
||||
trait BaseActivityInfo {
|
||||
|
||||
def toActivity: Activity
|
||||
|
||||
protected def trimInfoString(str: String, maxLen: Int): String =
|
||||
if (str.length > maxLen) s"${str.substring(0, maxLen)}..."
|
||||
else str
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
|
||||
final case class PushInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
branchName: String,
|
||||
commits: List[CommitInfo]
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"push",
|
||||
s"[user:$activityUserName] pushed to [branch:$userName/$repositoryName#$branchName] at [repo:$userName/$repositoryName]",
|
||||
Some(buildCommitSummary(commits)),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
|
||||
private[this] def buildCommitSummary(commits: List[CommitInfo]): String =
|
||||
commits
|
||||
.take(5)
|
||||
.map(commit => s"${commit.id}:${commit.shortMessage}")
|
||||
.mkString("\n")
|
||||
}
|
||||
|
||||
final case class CreateBranchInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
branchName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"create_branch",
|
||||
s"[user:$activityUserName] created branch [branch:$userName/$repositoryName#$branchName] at [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class DeleteBranchInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
branchName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"delete_branch",
|
||||
s"[user:$activityUserName] deleted branch $branchName at [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class IssueCommentInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
comment: String,
|
||||
issueId: Int
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"comment_issue",
|
||||
s"[user:${activityUserName}] commented on issue [issue:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(trimInfoString(comment, 200)),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class PullRequestCommentInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
comment: String,
|
||||
issueId: Int
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"comment_issue",
|
||||
s"[user:$activityUserName] commented on pull request [pullreq:$userName/$repositoryName#$issueId]",
|
||||
Some(trimInfoString(comment, 200)),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class CommitCommentInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
comment: String,
|
||||
commitId: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"comment_commit",
|
||||
s"[user:$activityUserName] commented on commit [commit:$userName/$repositoryName@$commitId]",
|
||||
Some(trimInfoString(comment, 200)),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class ForkInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
forkedUserName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"fork",
|
||||
s"[user:$activityUserName] forked [repo:$userName/$repositoryName] to [repo:$forkedUserName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class CreateIssueInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"open_issue",
|
||||
s"[user:$activityUserName] opened issue [issue:$userName/$repositoryName#$issueId]",
|
||||
Some(title),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class CloseIssueInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"close_issue",
|
||||
s"[user:$activityUserName] closed issue [issue:$userName/$repositoryName#$issueId]",
|
||||
Some(title),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class ReopenIssueInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"reopen_issue",
|
||||
s"[user:$activityUserName] reopened issue [issue:$userName/$repositoryName#$issueId]",
|
||||
Some(title),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class OpenPullRequestInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"open_pullreq",
|
||||
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class ClosePullRequestInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"close_issue",
|
||||
s"[user:$activityUserName] closed pull request [pullreq:$userName/$repositoryName#$issueId]",
|
||||
Some(title),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class ReopenPullRequestInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"reopen_issue",
|
||||
s"[user:$activityUserName] reopened pull request [issue:$userName/$repositoryName#$issueId]",
|
||||
Some(title),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class MergeInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
message: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"merge_pullreq",
|
||||
s"[user:$activityUserName] merged pull request [pullreq:$userName/$repositoryName#$issueId]",
|
||||
Some(message),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class ReleaseInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
releaseName: String,
|
||||
tagName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"release",
|
||||
s"[user:$activityUserName] released [release:$userName/$repositoryName/$tagName:$releaseName] at [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class CreateRepositoryInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"create_repository",
|
||||
s"[user:$activityUserName] created [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class DeleteRepositoryInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"delete_repository",
|
||||
s"[user:$activityUserName] deleted [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class TransferRepositoryInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
oldUserName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"transfer_repository",
|
||||
s"[user:$activityUserName] transferred [repo:$oldUserName/$repositoryName] to [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class RenameRepositoryInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
oldRepositoryName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"rename_repository",
|
||||
s"[user:$activityUserName] renamed [repo:$userName/$oldRepositoryName] at [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class CreateTagInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
tagName: String,
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"create_tag",
|
||||
s"[user:$activityUserName] created tag [tag:$userName/$repositoryName#$tagName] at [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class DeleteTagInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
tagName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"delete_tag",
|
||||
s"[user:$activityUserName] deleted tag $tagName at [repo:$userName/$repositoryName]",
|
||||
None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package gitbucket.core.model.activity
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.model.Profile.currentDate
|
||||
|
||||
final case class CreateWikiPageInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
pageName: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"create_wiki",
|
||||
s"[user:$activityUserName] created the [repo:$userName/$repositoryName] wiki",
|
||||
Some(pageName),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class EditWikiPageInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
pageName: String,
|
||||
commitId: String
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"edit_wiki",
|
||||
s"[user:$activityUserName] edited the [repo:$userName/$repositoryName] wiki",
|
||||
Some(s"$pageName:$commitId"),
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
|
||||
final case class DeleteWikiInfo(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
pageName: String,
|
||||
) extends BaseActivityInfo {
|
||||
|
||||
override def toActivity: Activity =
|
||||
Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"delete_wiki",
|
||||
s"[user:$activityUserName] deleted the page [$pageName] in the [repo:$userName/$repositoryName] wiki",
|
||||
additionalInfo = None,
|
||||
currentDate,
|
||||
UUID.randomUUID().toString
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,14 @@ trait IssueHook {
|
||||
implicit session: Session,
|
||||
context: Context
|
||||
): Unit = ()
|
||||
def deletedComment(commentId: Int, issue: Issue, repository: RepositoryInfo)(
|
||||
implicit session: Session,
|
||||
context: Context
|
||||
): Unit = ()
|
||||
def updatedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(
|
||||
implicit session: Session,
|
||||
context: Context
|
||||
): Unit = ()
|
||||
def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
|
||||
def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
|
||||
def assigned(
|
||||
|
||||
@@ -57,6 +57,7 @@ class PluginRegistry {
|
||||
private val textDecorators = new ConcurrentLinkedQueue[TextDecorator]
|
||||
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
|
||||
suggestionProviders.add(new UserNameSuggestionProvider())
|
||||
suggestionProviders.add(new IssueSuggestionProvider())
|
||||
private val sshCommandProviders = new ConcurrentLinkedQueue[PartialFunction[String, Command]]()
|
||||
|
||||
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
|
||||
|
||||
@@ -6,11 +6,25 @@ import profile.api._
|
||||
|
||||
trait ReceiveHook {
|
||||
|
||||
def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(
|
||||
def preReceive(
|
||||
owner: String,
|
||||
repository: String,
|
||||
receivePack: ReceivePack,
|
||||
command: ReceiveCommand,
|
||||
pusher: String,
|
||||
mergePullRequest: Boolean
|
||||
)(
|
||||
implicit session: Session
|
||||
): Option[String] = None
|
||||
|
||||
def postReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)(
|
||||
def postReceive(
|
||||
owner: String,
|
||||
repository: String,
|
||||
receivePack: ReceivePack,
|
||||
command: ReceiveCommand,
|
||||
pusher: String,
|
||||
mergePullRequest: Boolean
|
||||
)(
|
||||
implicit session: Session
|
||||
): Unit = ()
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ trait SuggestionProvider {
|
||||
* If this suggestion provider needs some additional process to assemble the proposal list (e.g. It need to use Ajax
|
||||
* to get a proposal list from the server), then override this method and return any JavaScript code.
|
||||
*/
|
||||
def additionalScript(implicit context: Context): String = ""
|
||||
def additionalScript(repository: RepositoryInfo)(implicit context: Context): String = ""
|
||||
|
||||
}
|
||||
|
||||
@@ -99,6 +99,14 @@ class UserNameSuggestionProvider extends SuggestionProvider {
|
||||
override val id: String = "user"
|
||||
override val prefix: String = "@"
|
||||
override val context: Seq[String] = Seq("issues")
|
||||
override def additionalScript(implicit context: Context): String =
|
||||
override def additionalScript(repository: RepositoryInfo)(implicit context: Context): String =
|
||||
s"""$$.get('${context.path}/_user/proposals', { query: '', user: true, group: false }, function (data) { user = data.options; });"""
|
||||
}
|
||||
|
||||
class IssueSuggestionProvider extends SuggestionProvider {
|
||||
override val id: String = "issue"
|
||||
override val prefix: String = "#"
|
||||
override val context: Seq[String] = Seq("issues")
|
||||
override def additionalScript(repository: RepositoryInfo)(implicit context: Context): String =
|
||||
s"""$$.get('${context.path}/${repository.owner}/${repository.name}/_issue/proposals', function (data) { issue = data.options; });"""
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import gitbucket.core.model.{Account, AccountExtraMailAddress, GroupMember}
|
||||
import gitbucket.core.model.{Account, AccountExtraMailAddress, AccountPreference, GroupMember}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
@@ -17,7 +17,9 @@ trait AccountService {
|
||||
def authenticate(settings: SystemSettings, userName: String, password: String)(
|
||||
implicit s: Session
|
||||
): Option[Account] = {
|
||||
val account = if (settings.ldapAuthentication) {
|
||||
val account = if (password.isEmpty) {
|
||||
None
|
||||
} else if (settings.ldapAuthentication) {
|
||||
ldapAuthentication(settings, userName, password)
|
||||
} else {
|
||||
defaultAuthentication(userName, password)
|
||||
@@ -308,6 +310,33 @@ trait AccountService {
|
||||
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list.distinct
|
||||
}
|
||||
|
||||
/*
|
||||
* For account preference
|
||||
*/
|
||||
def getAccountPreference(userName: String)(
|
||||
implicit s: Session
|
||||
): Option[AccountPreference] = {
|
||||
AccountPreferences filter (_.byPrimaryKey(userName)) firstOption
|
||||
}
|
||||
|
||||
def addAccountPreference(userName: String, highlighterTheme: String)(implicit s: Session): Unit = {
|
||||
AccountPreferences insert AccountPreference(userName = userName, highlighterTheme = highlighterTheme)
|
||||
}
|
||||
|
||||
def updateAccountPreference(userName: String, highlighterTheme: String)(implicit s: Session): Unit = {
|
||||
AccountPreferences
|
||||
.filter(_.byPrimaryKey(userName))
|
||||
.map(t => t.highlighterTheme)
|
||||
.update(highlighterTheme)
|
||||
}
|
||||
|
||||
def addOrUpdateAccountPreference(userName: String, highlighterTheme: String)(implicit s: Session): Unit = {
|
||||
getAccountPreference(userName) match {
|
||||
case Some(_) => updateAccountPreference(userName, highlighterTheme)
|
||||
case _ => addAccountPreference(userName, highlighterTheme)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object AccountService extends AccountService
|
||||
|
||||
@@ -3,389 +3,101 @@ package gitbucket.core.service
|
||||
import gitbucket.core.model.Activity
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.util.Directory._
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.Serialization
|
||||
import org.json4s.jackson.Serialization.{read, write}
|
||||
|
||||
import scala.util.Using
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.activity.BaseActivityInfo
|
||||
import org.apache.commons.io.input.ReversedLinesFileReader
|
||||
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
trait ActivityService {
|
||||
self: RequestCache =>
|
||||
|
||||
def deleteOldActivities(limit: Int)(implicit s: Session): Int = {
|
||||
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id =>
|
||||
Activities.filter(_.activityId <= id.bind).delete
|
||||
} getOrElse 0
|
||||
private implicit val formats = Serialization.formats(NoTypeHints)
|
||||
|
||||
private def writeLog(activity: Activity): Unit = {
|
||||
Using.resource(new FileOutputStream(ActivityLog, true)) { out =>
|
||||
out.write((write(activity) + "\n").getBytes(StandardCharsets.UTF_8))
|
||||
}
|
||||
}
|
||||
|
||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
|
||||
Activities
|
||||
.join(Repositories)
|
||||
.on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))
|
||||
.filter {
|
||||
case (t1, t2) =>
|
||||
if (isPublic) {
|
||||
(t1.activityUserName === activityUserName.bind) && (t2.isPrivate === false.bind)
|
||||
} else {
|
||||
(t1.activityUserName === activityUserName.bind)
|
||||
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit context: Context): List[Activity] = {
|
||||
if (!ActivityLog.exists()) {
|
||||
List.empty
|
||||
} else {
|
||||
val list = new ListBuffer[Activity]
|
||||
Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
|
||||
var json: String = null
|
||||
while (list.length < 50 && { json = reader.readLine(); json } != null) {
|
||||
val activity = read[Activity](json)
|
||||
if (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
|
||||
list.toList
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
def getRecentPublicActivities()(implicit context: Context): List[Activity] = {
|
||||
if (!ActivityLog.exists()) {
|
||||
List.empty
|
||||
} else {
|
||||
val list = new ListBuffer[Activity]
|
||||
Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
|
||||
var json: String = null
|
||||
while (list.length < 50 && { json = reader.readLine(); json } != null) {
|
||||
val activity = read[Activity](json)
|
||||
if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
|
||||
.map(_.isPrivate)
|
||||
.getOrElse(true)) {
|
||||
list += activity
|
||||
}
|
||||
.mkString("\n")
|
||||
),
|
||||
currentDate
|
||||
)
|
||||
}
|
||||
}
|
||||
list.toList
|
||||
}
|
||||
}
|
||||
|
||||
def recordCreateTagActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
tagName: String,
|
||||
commits: List[JGitUtil.CommitInfo]
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"create_tag",
|
||||
s"[user:${activityUserName}] created tag [tag:${userName}/${repositoryName}#${tagName}] at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate
|
||||
)
|
||||
def getRecentActivitiesByOwners(owners: Set[String])(implicit context: Context): List[Activity] = {
|
||||
if (!ActivityLog.exists()) {
|
||||
List.empty
|
||||
} else {
|
||||
val list = new ListBuffer[Activity]
|
||||
Using.resource(new ReversedLinesFileReader(ActivityLog, StandardCharsets.UTF_8)) { reader =>
|
||||
var json: String = null
|
||||
while (list.length < 50 && { json = reader.readLine(); json } != null) {
|
||||
val activity = read[Activity](json)
|
||||
if (owners.contains(activity.userName)) {
|
||||
list += activity
|
||||
} else if (!getRepositoryInfoFromCache(activity.userName, activity.repositoryName)
|
||||
.map(_.isPrivate)
|
||||
.getOrElse(true)) {
|
||||
list += activity
|
||||
}
|
||||
}
|
||||
}
|
||||
list.toList
|
||||
}
|
||||
}
|
||||
|
||||
def recordDeleteTagActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
tagName: String,
|
||||
commits: List[JGitUtil.CommitInfo]
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"delete_tag",
|
||||
s"[user:${activityUserName}] deleted tag ${tagName} at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate
|
||||
)
|
||||
|
||||
def recordCreateBranchActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
branchName: String
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"create_branch",
|
||||
s"[user:${activityUserName}] created branch [branch:${userName}/${repositoryName}#${branchName}] at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate
|
||||
)
|
||||
|
||||
def recordDeleteBranchActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
branchName: String
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"delete_branch",
|
||||
s"[user:${activityUserName}] deleted branch ${branchName} at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate
|
||||
)
|
||||
|
||||
def recordForkActivity(userName: String, repositoryName: String, activityUserName: String, forkedUserName: String)(
|
||||
implicit s: Session
|
||||
): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"fork",
|
||||
s"[user:${activityUserName}] forked [repo:${userName}/${repositoryName}] to [repo:${forkedUserName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate
|
||||
)
|
||||
|
||||
def recordPullRequestActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
title: String
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"open_pullreq",
|
||||
s"[user:${activityUserName}] opened pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(title),
|
||||
currentDate
|
||||
)
|
||||
|
||||
def recordMergeActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
issueId: Int,
|
||||
message: String
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"merge_pullreq",
|
||||
s"[user:${activityUserName}] merged pull request [pullreq:${userName}/${repositoryName}#${issueId}]",
|
||||
Some(message),
|
||||
currentDate
|
||||
)
|
||||
|
||||
def recordReleaseActivity(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
activityUserName: String,
|
||||
releaseName: String,
|
||||
tagName: String
|
||||
)(implicit s: Session): Unit =
|
||||
Activities insert Activity(
|
||||
userName,
|
||||
repositoryName,
|
||||
activityUserName,
|
||||
"release",
|
||||
s"[user:${activityUserName}] released [release:${userName}/${repositoryName}/${tagName}:${releaseName}] at [repo:${userName}/${repositoryName}]",
|
||||
None,
|
||||
currentDate
|
||||
)
|
||||
|
||||
private def cut(value: String, length: Int): String =
|
||||
if (value.length > length) value.substring(0, length) + "..." else value
|
||||
def recordActivity[T <: { def toActivity: Activity }](info: T): Unit =
|
||||
writeLog(info.toActivity)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,18 @@ trait CommitStatusService {
|
||||
)
|
||||
}
|
||||
|
||||
def getCommitStatusWithSummary(userName: String, repositoryName: String, sha: String)(
|
||||
implicit s: Session
|
||||
): Option[(CommitState, List[CommitStatus])] = {
|
||||
val statuses = getCommitStatuses(userName, repositoryName, sha)
|
||||
if (statuses.isEmpty) {
|
||||
None
|
||||
} else {
|
||||
val summary = CommitState.combine(statuses.groupBy(_.state).keySet)
|
||||
Some((summary, statuses))
|
||||
}
|
||||
}
|
||||
|
||||
def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session): Option[CommitStatus] =
|
||||
CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption
|
||||
|
||||
@@ -55,10 +67,12 @@ trait CommitStatusService {
|
||||
): Option[CommitStatus] =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind).firstOption
|
||||
|
||||
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session): List[CommitStatus] =
|
||||
byCommitStatues(userName, repositoryName, sha).list
|
||||
def getCommitStatuses(userName: String, repositoryName: String, sha: String)(
|
||||
implicit s: Session
|
||||
): List[CommitStatus] =
|
||||
byCommitStatus(userName, repositoryName, sha).list
|
||||
|
||||
def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)(
|
||||
def getRecentStatusContexts(userName: String, repositoryName: String, time: java.util.Date)(
|
||||
implicit s: Session
|
||||
): List[String] =
|
||||
CommitStatuses
|
||||
@@ -68,15 +82,15 @@ trait CommitStatusService {
|
||||
.map(_._1)
|
||||
.list
|
||||
|
||||
def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(
|
||||
def getCommitStatusesWithCreator(userName: String, repositoryName: String, sha: String)(
|
||||
implicit s: Session
|
||||
): List[(CommitStatus, Account)] =
|
||||
byCommitStatues(userName, repositoryName, sha)
|
||||
byCommitStatus(userName, repositoryName, sha)
|
||||
.join(Accounts)
|
||||
.filter { case (t, a) => t.creator === a.userName }
|
||||
.list
|
||||
|
||||
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
|
||||
protected def byCommitStatus(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
|
||||
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha)).sortBy(_.updatedDate desc)
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import gitbucket.core.model.{Account, CommitComment}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
import gitbucket.core.model.activity.{CommitCommentInfo, PullRequestCommentInfo}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.Directory._
|
||||
@@ -80,13 +81,9 @@ trait CommitsService {
|
||||
case Some(issueId) =>
|
||||
getPullRequest(repository.owner, repository.name, issueId).foreach {
|
||||
case (issue, pullRequest) =>
|
||||
recordCommentPullRequestActivity(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issueId,
|
||||
content
|
||||
)
|
||||
val pullRequestCommentInfo =
|
||||
PullRequestCommentInfo(repository.owner, repository.name, loginAccount.userName, content, issueId)
|
||||
recordActivity(pullRequestCommentInfo)
|
||||
PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, content, issue, repository))
|
||||
callPullRequestReviewCommentWebHook(
|
||||
"create",
|
||||
@@ -99,13 +96,9 @@ trait CommitsService {
|
||||
)
|
||||
}
|
||||
case None =>
|
||||
recordCommentCommitActivity(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
commitId,
|
||||
content
|
||||
)
|
||||
val commitCommentInfo =
|
||||
CommitCommentInfo(repository.owner, repository.name, loginAccount.userName, content, commitId)
|
||||
recordActivity(commitCommentInfo)
|
||||
}
|
||||
|
||||
commentId
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.Issue
|
||||
import gitbucket.core.model.{Issue, IssueComment}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.model.activity.{
|
||||
CloseIssueInfo,
|
||||
ClosePullRequestInfo,
|
||||
IssueCommentInfo,
|
||||
PullRequestCommentInfo,
|
||||
ReopenIssueInfo,
|
||||
ReopenPullRequestInfo
|
||||
}
|
||||
import gitbucket.core.plugin.{IssueHook, PluginRegistry}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Implicits._
|
||||
|
||||
@@ -29,25 +38,31 @@ trait HandleCommentService {
|
||||
case (owner, name) =>
|
||||
val userName = loginAccount.userName
|
||||
|
||||
val (action, actionActivity) = actionOpt
|
||||
actionOpt.collect {
|
||||
case "close" if !issue.closed =>
|
||||
updateClosed(owner, name, issue.issueId, true)
|
||||
case "reopen" if issue.closed =>
|
||||
updateClosed(owner, name, issue.issueId, false)
|
||||
}
|
||||
|
||||
val (action, _) = actionOpt
|
||||
.collect {
|
||||
case "close" if (!issue.closed) =>
|
||||
true ->
|
||||
(Some("close") -> Some(
|
||||
if (issue.isPullRequest) recordClosePullRequestActivity _
|
||||
else recordCloseIssueActivity _
|
||||
))
|
||||
case "reopen" if (issue.closed) =>
|
||||
false ->
|
||||
(Some("reopen") -> Some(
|
||||
if (issue.isPullRequest) recordReopenPullRequestActivity _
|
||||
else recordReopenIssueActivity _
|
||||
))
|
||||
}
|
||||
.map {
|
||||
case (closed, t) =>
|
||||
updateClosed(owner, name, issue.issueId, closed)
|
||||
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)
|
||||
|
||||
@@ -68,8 +83,12 @@ trait HandleCommentService {
|
||||
)
|
||||
|
||||
// record comment activity
|
||||
if (issue.isPullRequest) recordCommentPullRequestActivity(owner, name, userName, issue.issueId, content)
|
||||
else recordCommentIssueActivity(owner, name, userName, issue.issueId, content)
|
||||
val commentInfo = if (issue.isPullRequest) {
|
||||
PullRequestCommentInfo(owner, name, userName, content, issue.issueId)
|
||||
} else {
|
||||
IssueCommentInfo(owner, name, userName, content, issue.issueId)
|
||||
}
|
||||
recordActivity(commentInfo)
|
||||
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, content, loginAccount)
|
||||
@@ -77,10 +96,6 @@ trait HandleCommentService {
|
||||
id
|
||||
}
|
||||
|
||||
actionActivity.foreach { f =>
|
||||
f(owner, name, userName, issue.issueId, issue.title)
|
||||
}
|
||||
|
||||
// call web hooks
|
||||
action match {
|
||||
case None =>
|
||||
@@ -121,4 +136,59 @@ trait HandleCommentService {
|
||||
}
|
||||
}
|
||||
|
||||
def deleteCommentByApi(repoInfo: RepositoryInfo, comment: IssueComment, issue: Issue)(
|
||||
implicit context: Context,
|
||||
s: Session
|
||||
): Option[IssueComment] = context.loginAccount.flatMap { _ =>
|
||||
comment.action match {
|
||||
case "comment" =>
|
||||
val deleteResult = deleteComment(repoInfo.owner, repoInfo.name, comment.issueId, comment.commentId)
|
||||
val registry = PluginRegistry()
|
||||
val hooks: Seq[IssueHook] = if (issue.isPullRequest) registry.getPullRequestHooks else registry.getIssueHooks
|
||||
hooks.foreach(_.deletedComment(comment.commentId, issue, repoInfo))
|
||||
deleteResult match {
|
||||
case n if n > 0 => Some(comment)
|
||||
case _ => None
|
||||
}
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
|
||||
def updateCommentByApi(
|
||||
repository: RepositoryService.RepositoryInfo,
|
||||
issue: Issue,
|
||||
commentId: String,
|
||||
content: Option[String]
|
||||
)(implicit context: Context, s: Session): Option[(Issue, Int)] = {
|
||||
context.loginAccount.flatMap { loginAccount =>
|
||||
defining(repository.owner, repository.name) {
|
||||
case (owner, name) =>
|
||||
val userName = loginAccount.userName
|
||||
content match {
|
||||
case Some(content) =>
|
||||
// Update comment
|
||||
val _commentId = Some(updateComment(issue.issueId, commentId.toInt, content))
|
||||
// Record comment activity
|
||||
val commentInfo = if (issue.isPullRequest) {
|
||||
PullRequestCommentInfo(owner, name, userName, content, issue.issueId)
|
||||
} else {
|
||||
IssueCommentInfo(owner, name, userName, content, issue.issueId)
|
||||
}
|
||||
recordActivity(commentInfo)
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, content, loginAccount)
|
||||
// call web hooks
|
||||
commentId foreach (callIssueCommentWebHook(repository, issue, _, loginAccount, context.settings))
|
||||
// call hooks
|
||||
if (issue.isPullRequest)
|
||||
PluginRegistry().getPullRequestHooks
|
||||
.foreach(_.updatedComment(commentId.toInt, content, issue, repository))
|
||||
else
|
||||
PluginRegistry().getIssueHooks.foreach(_.updatedComment(commentId.toInt, content, issue, repository))
|
||||
_commentId.map(issue -> _)
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package gitbucket.core.service
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Account, Issue}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.activity.CreateIssueInfo
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.Implicits._
|
||||
@@ -51,7 +52,8 @@ trait IssueCreationService {
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordCreateIssueActivity(owner, name, userName, issueId, title)
|
||||
val createIssueInfo = CreateIssueInfo(owner, name, userName, issueId, title)
|
||||
recordActivity(createIssueInfo)
|
||||
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, title + " " + body.getOrElse(""), loginAccount)
|
||||
@@ -72,6 +74,13 @@ trait IssueCreationService {
|
||||
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can manage issues comment.
|
||||
*/
|
||||
protected def isIssueCommentManageable(repository: RepositoryInfo)(implicit context: Context, s: Session): Boolean = {
|
||||
hasOwnerRole(repository.owner, repository.name, context.loginAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether an logged-in user can post issues.
|
||||
*/
|
||||
|
||||
@@ -31,6 +31,9 @@ trait IssuesService {
|
||||
Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
|
||||
else None
|
||||
|
||||
def getOpenIssues(owner: String, repository: String)(implicit s: Session): List[Issue] =
|
||||
Issues filter (_.byRepository(owner, repository)) filterNot (_.closed) sortBy (_.issueId desc) list
|
||||
|
||||
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
|
||||
IssueComments filter (_.byIssue(owner, repository, issueId)) sortBy (_.commentId asc) list
|
||||
|
||||
@@ -68,6 +71,20 @@ trait IssuesService {
|
||||
else None
|
||||
}
|
||||
|
||||
def getCommentForApi(owner: String, repository: String, commentId: Int)(
|
||||
implicit s: Session
|
||||
): Option[(IssueComment, Account, Issue)] =
|
||||
IssueComments
|
||||
.filter(_.byRepository(owner, repository))
|
||||
.filter(_.commentId === commentId)
|
||||
.filter(_.action inSetBind Set("comment", "close_comment", "reopen_comment"))
|
||||
.join(Accounts)
|
||||
.on { case t1 ~ t2 => t1.commentedUserName === t2.userName }
|
||||
.join(Issues)
|
||||
.on { case t1 ~ t2 ~ t3 => t3.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
|
||||
.map { case t1 ~ t2 ~ t3 => (t1, t2, t3) }
|
||||
.firstOption
|
||||
|
||||
def getIssueLabels(owner: String, repository: String, issueId: Int)(implicit s: Session): List[Label] = {
|
||||
IssueLabels
|
||||
.join(Labels)
|
||||
@@ -90,14 +107,14 @@ trait IssuesService {
|
||||
* Returns the count of the search result against issues.
|
||||
*
|
||||
* @param condition the search condition
|
||||
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
|
||||
* @param searchOption if true then counts only pull request, false then counts both of issue and pull request.
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
* @return the count of the search result
|
||||
*/
|
||||
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean, repos: (String, String)*)(
|
||||
def countIssue(condition: IssueSearchCondition, searchOption: IssueSearchOption, repos: (String, String)*)(
|
||||
implicit s: Session
|
||||
): Int = {
|
||||
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
|
||||
Query(searchIssueQuery(repos, condition, searchOption).length).first
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +132,7 @@ trait IssuesService {
|
||||
filterUser: Map[String, String]
|
||||
)(implicit s: Session): Map[String, Int] = {
|
||||
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
||||
.join(IssueLabels)
|
||||
.on {
|
||||
case t1 ~ t2 =>
|
||||
@@ -153,7 +170,7 @@ trait IssuesService {
|
||||
filterUser: Map[String, String]
|
||||
)(implicit s: Session): Map[String, Int] = {
|
||||
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
|
||||
.join(Priorities)
|
||||
.on {
|
||||
case t1 ~ t2 =>
|
||||
@@ -171,42 +188,11 @@ trait IssuesService {
|
||||
.toMap
|
||||
}
|
||||
|
||||
def getCommitStatues(userName: String, repositoryName: String, issueId: Int)(
|
||||
implicit s: Session
|
||||
): Option[CommitStatusInfo] = {
|
||||
val status = PullRequests
|
||||
.filter { pr =>
|
||||
pr.userName === userName.bind && pr.repositoryName === repositoryName.bind && pr.issueId === issueId.bind
|
||||
}
|
||||
.join(CommitStatuses)
|
||||
.on {
|
||||
case pr ~ cs =>
|
||||
pr.userName === cs.userName && pr.repositoryName === cs.repositoryName && pr.commitIdTo === cs.commitId
|
||||
}
|
||||
.list
|
||||
|
||||
if (status.nonEmpty) {
|
||||
val (_, cs) = status.head
|
||||
Some(
|
||||
CommitStatusInfo(
|
||||
count = status.length,
|
||||
successCount = status.count(_._2.state == CommitState.SUCCESS),
|
||||
context = (if (status.length == 1) Some(cs.context) else None),
|
||||
state = (if (status.length == 1) Some(cs.state) else None),
|
||||
targetUrl = (if (status.length == 1) cs.targetUrl else None),
|
||||
description = (if (status.length == 1) cs.description else None)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the search result against issues.
|
||||
*
|
||||
* @param condition the search condition
|
||||
* @param pullRequest if true then returns only pull requests, false then returns only issues.
|
||||
* @param searchOption if true then returns only pull requests, false then returns only issues.
|
||||
* @param offset the offset for pagination
|
||||
* @param limit the limit for pagination
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
@@ -214,13 +200,13 @@ trait IssuesService {
|
||||
*/
|
||||
def searchIssue(
|
||||
condition: IssueSearchCondition,
|
||||
pullRequest: Boolean,
|
||||
searchOption: IssueSearchOption,
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
repos: (String, String)*
|
||||
)(implicit s: Session): List[IssueInfo] = {
|
||||
// get issues and comment count and labels
|
||||
val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
|
||||
val result = searchIssueQueryBase(condition, searchOption, offset, limit, repos)
|
||||
.joinLeft(IssueLabels)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||
.joinLeft(Labels)
|
||||
@@ -229,9 +215,11 @@ trait IssuesService {
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
|
||||
.joinLeft(Priorities)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
|
||||
.joinLeft(PullRequests)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t1.byIssue(t7.userName, t7.repositoryName, t7.issueId) }
|
||||
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
|
||||
.map {
|
||||
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 =>
|
||||
case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 =>
|
||||
(
|
||||
t1,
|
||||
t2.commentCount,
|
||||
@@ -239,7 +227,8 @@ trait IssuesService {
|
||||
t4.map(_.labelName),
|
||||
t4.map(_.color),
|
||||
t5.map(_.title),
|
||||
t6.map(_.priorityName)
|
||||
t6.map(_.priorityName),
|
||||
t7.map(_.commitIdTo)
|
||||
)
|
||||
}
|
||||
.list
|
||||
@@ -249,7 +238,7 @@ trait IssuesService {
|
||||
|
||||
result.map { issues =>
|
||||
issues.head match {
|
||||
case (issue, commentCount, _, _, _, milestone, priority) =>
|
||||
case (issue, commentCount, _, _, _, milestone, priority, commitId) =>
|
||||
IssueInfo(
|
||||
issue,
|
||||
issues.flatMap { t =>
|
||||
@@ -258,7 +247,7 @@ trait IssuesService {
|
||||
milestone,
|
||||
priority,
|
||||
commentCount,
|
||||
getCommitStatues(issue.userName, issue.repositoryName, issue.issueId)
|
||||
commitId
|
||||
)
|
||||
}
|
||||
} toList
|
||||
@@ -271,7 +260,7 @@ trait IssuesService {
|
||||
implicit s: Session
|
||||
): List[(Issue, Account, Option[Account])] = {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQueryBase(condition, false, offset, limit, repos)
|
||||
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
|
||||
.join(Accounts)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName }
|
||||
.joinLeft(Accounts)
|
||||
@@ -288,7 +277,7 @@ trait IssuesService {
|
||||
implicit s: Session
|
||||
): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
|
||||
// get issues and comment count and labels
|
||||
searchIssueQueryBase(condition, true, offset, limit, repos)
|
||||
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
|
||||
.join(PullRequests)
|
||||
.on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
|
||||
.join(Repositories)
|
||||
@@ -306,12 +295,12 @@ trait IssuesService {
|
||||
|
||||
private def searchIssueQueryBase(
|
||||
condition: IssueSearchCondition,
|
||||
pullRequest: Boolean,
|
||||
searchOption: IssueSearchOption,
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
repos: Seq[(String, String)]
|
||||
)(implicit s: Session) =
|
||||
searchIssueQuery(repos, condition, pullRequest)
|
||||
searchIssueQuery(repos, condition, searchOption)
|
||||
.join(IssueOutline)
|
||||
.on { (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
@@ -349,7 +338,11 @@ trait IssuesService {
|
||||
/**
|
||||
* Assembles query for conditional issue searching.
|
||||
*/
|
||||
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(
|
||||
private def searchIssueQuery(
|
||||
repos: Seq[(String, String)],
|
||||
condition: IssueSearchCondition,
|
||||
searchOption: IssueSearchOption
|
||||
)(
|
||||
implicit s: Session
|
||||
) =
|
||||
Issues filter { t1 =>
|
||||
@@ -363,7 +356,11 @@ trait IssuesService {
|
||||
(t1.priorityId.? isEmpty, condition.priority == Some(None)) &&
|
||||
(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) &&
|
||||
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
||||
(t1.pullRequest === pullRequest.bind) &&
|
||||
(searchOption match {
|
||||
case IssueSearchOption.Issues => t1.pullRequest === false
|
||||
case IssueSearchOption.PullRequests => t1.pullRequest === true
|
||||
case IssueSearchOption.Both => t1.pullRequest === false || t1.pullRequest === true
|
||||
}) &&
|
||||
// Milestone filter
|
||||
(Milestones filter { t2 =>
|
||||
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) &&
|
||||
@@ -635,7 +632,10 @@ trait IssuesService {
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
|
||||
}
|
||||
|
||||
def deleteComment(issueId: Int, commentId: Int)(implicit s: Session): Int = {
|
||||
def deleteComment(owner: String, repository: String, issueId: Int, commentId: Int)(
|
||||
implicit context: Context,
|
||||
s: Session
|
||||
): Int = {
|
||||
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).firstOption match {
|
||||
case Some(c) if c.action == "reopen_comment" =>
|
||||
@@ -644,6 +644,16 @@ trait IssuesService {
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.action)).update("Close", "close")
|
||||
case Some(_) =>
|
||||
IssueComments.filter(_.byPrimaryKey(commentId)).delete
|
||||
IssueComments insert IssueComment(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
issueId = issueId,
|
||||
action = "delete_comment",
|
||||
commentedUserName = context.loginAccount.map(_.userName).getOrElse("Unknown user"),
|
||||
content = s"",
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,27 +920,46 @@ object IssuesService {
|
||||
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
|
||||
)
|
||||
|
||||
def apply(request: HttpServletRequest, milestone: String): IssueSearchCondition =
|
||||
IssueSearchCondition(
|
||||
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
|
||||
Some(Some(milestone)),
|
||||
param(request, "priority").map {
|
||||
case "none" => None
|
||||
case x => Some(x)
|
||||
},
|
||||
param(request, "author"),
|
||||
param(request, "assigned").map {
|
||||
case "none" => None
|
||||
case x => Some(x)
|
||||
},
|
||||
param(request, "mentioned"),
|
||||
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
||||
param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"),
|
||||
param(request, "direction", Seq("asc", "desc")).getOrElse("desc"),
|
||||
param(request, "visibility"),
|
||||
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
|
||||
)
|
||||
|
||||
def page(request: HttpServletRequest) = {
|
||||
PaginationHelper.page(param(request, "page"))
|
||||
}
|
||||
}
|
||||
|
||||
case class CommitStatusInfo(
|
||||
count: Int,
|
||||
successCount: Int,
|
||||
context: Option[String],
|
||||
state: Option[CommitState],
|
||||
targetUrl: Option[String],
|
||||
description: Option[String]
|
||||
)
|
||||
|
||||
case class IssueInfo(
|
||||
issue: Issue,
|
||||
labels: List[Label],
|
||||
milestone: Option[String],
|
||||
priority: Option[String],
|
||||
commentCount: Int,
|
||||
status: Option[CommitStatusInfo]
|
||||
commitId: Option[String]
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
sealed trait IssueSearchOption
|
||||
|
||||
object IssueSearchOption {
|
||||
case object Issues extends IssueSearchOption
|
||||
case object PullRequests extends IssueSearchOption
|
||||
case object Both extends IssueSearchOption
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@ package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.api.JsonFormat
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.{Account, PullRequest, WebHook}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
|
||||
import gitbucket.core.plugin.{PluginRegistry, ReceiveHook}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.{JGitUtil, LockUtil}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.activity.{CloseIssueInfo, MergeInfo, PushInfo}
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
import org.eclipse.jgit.merge.{MergeStrategy, Merger, RecursiveMerger}
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.transport.RefSpec
|
||||
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack, RefSpec}
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import org.eclipse.jgit.lib.{CommitBuilder, ObjectId, PersonIdent, Repository}
|
||||
import org.eclipse.jgit.revwalk.{RevCommit, RevWalk}
|
||||
@@ -35,7 +37,7 @@ trait MergeService {
|
||||
*/
|
||||
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Option[String] = {
|
||||
Using.resource(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
new MergeCacheInfo(git, branch, issueId).checkConflict()
|
||||
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, Nil).checkConflict()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,41 +54,47 @@ trait MergeService {
|
||||
issueId: Int
|
||||
): Option[Option[String]] = {
|
||||
Using.resource(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
|
||||
new MergeCacheInfo(git, branch, issueId).checkConflictCache()
|
||||
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, Nil).checkConflictCache()
|
||||
}
|
||||
}
|
||||
|
||||
/** merge the pull request with a merge commit */
|
||||
def mergePullRequest(
|
||||
def mergeWithMergeCommit(
|
||||
git: Git,
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
issueId: Int,
|
||||
message: String,
|
||||
committer: PersonIdent
|
||||
): ObjectId = {
|
||||
new MergeCacheInfo(git, branch, issueId).merge(message, committer)
|
||||
)(implicit s: Session): ObjectId = {
|
||||
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).merge(message, committer)
|
||||
}
|
||||
|
||||
/** rebase to the head of the pull request branch */
|
||||
def rebasePullRequest(
|
||||
def mergeWithRebase(
|
||||
git: Git,
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
issueId: Int,
|
||||
commits: Seq[RevCommit],
|
||||
committer: PersonIdent
|
||||
): ObjectId = {
|
||||
new MergeCacheInfo(git, branch, issueId).rebase(committer, commits)
|
||||
)(implicit s: Session): ObjectId = {
|
||||
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).rebase(committer, commits)
|
||||
}
|
||||
|
||||
/** squash commits in the pull request and append it */
|
||||
def squashPullRequest(
|
||||
def mergeWithSquash(
|
||||
git: Git,
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
issueId: Int,
|
||||
message: String,
|
||||
committer: PersonIdent
|
||||
): ObjectId = {
|
||||
new MergeCacheInfo(git, branch, issueId).squash(message, committer)
|
||||
)(implicit s: Session): ObjectId = {
|
||||
new MergeCacheInfo(git, userName, repositoryName, branch, issueId, getReceiveHooks()).squash(message, committer)
|
||||
}
|
||||
|
||||
/** fetch remote branch to my repository refs/pull/{issueId}/head */
|
||||
@@ -168,7 +176,7 @@ trait MergeService {
|
||||
remoteBranch: String,
|
||||
loginAccount: Account,
|
||||
message: String,
|
||||
pullreq: Option[PullRequest],
|
||||
pullRequest: Option[PullRequest],
|
||||
settings: SystemSettings
|
||||
)(implicit s: Session, c: JsonFormat.Context): Option[ObjectId] = {
|
||||
val localUserName = localRepository.owner
|
||||
@@ -200,13 +208,14 @@ trait MergeService {
|
||||
}
|
||||
|
||||
// record activity
|
||||
recordPushActivity(
|
||||
val pushInfo = PushInfo(
|
||||
localUserName,
|
||||
localRepositoryName,
|
||||
loginAccount.userName,
|
||||
localBranch,
|
||||
commits
|
||||
)
|
||||
recordActivity(pushInfo)
|
||||
|
||||
// close issue by commit message
|
||||
if (localBranch == localRepository.repository.defaultBranch) {
|
||||
@@ -215,6 +224,14 @@ trait MergeService {
|
||||
.foreach { issueId =>
|
||||
getIssue(localRepository.owner, localRepository.name, issueId.toString).foreach { issue =>
|
||||
callIssuesWebHook("closed", localRepository, issue, loginAccount, settings)
|
||||
val closeIssueInfo = CloseIssueInfo(
|
||||
localRepository.owner,
|
||||
localRepository.name,
|
||||
localUserName,
|
||||
issue.issueId,
|
||||
issue.title
|
||||
)
|
||||
recordActivity(closeIssueInfo)
|
||||
PluginRegistry().getIssueHooks
|
||||
.foreach(
|
||||
_.closedByCommitComment(issue, localRepository, commit.fullMessage, loginAccount)
|
||||
@@ -224,7 +241,7 @@ trait MergeService {
|
||||
}
|
||||
}
|
||||
|
||||
pullreq.foreach { pullreq =>
|
||||
pullRequest.foreach { pullRequest =>
|
||||
callWebHookOf(localRepository.owner, localRepository.name, WebHook.Push, settings) {
|
||||
for {
|
||||
ownerAccount <- getAccountByUserName(localRepository.owner)
|
||||
@@ -232,7 +249,7 @@ trait MergeService {
|
||||
WebHookService.WebHookPushPayload(
|
||||
git,
|
||||
loginAccount,
|
||||
pullreq.requestBranch,
|
||||
pullRequest.requestBranch,
|
||||
localRepository,
|
||||
commits,
|
||||
ownerAccount,
|
||||
@@ -247,6 +264,10 @@ trait MergeService {
|
||||
}.toOption
|
||||
}
|
||||
|
||||
protected def getReceiveHooks(): Seq[ReceiveHook] = {
|
||||
PluginRegistry().getReceiveHooks
|
||||
}
|
||||
|
||||
def mergePullRequest(
|
||||
repository: RepositoryInfo,
|
||||
issueId: Int,
|
||||
@@ -261,73 +282,52 @@ trait MergeService {
|
||||
LockUtil.lock(s"${repository.owner}/${repository.name}") {
|
||||
getPullRequest(repository.owner, repository.name, issueId)
|
||||
.map {
|
||||
case (issue, pullreq) =>
|
||||
case (issue, pullRequest) =>
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
// mark issue as merged and close.
|
||||
val commentId =
|
||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, message, "merge")
|
||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(repository.owner, repository.name, issueId, true)
|
||||
|
||||
// record activity
|
||||
recordMergeActivity(repository.owner, repository.name, loginAccount.userName, issueId, message)
|
||||
|
||||
val (commits, _) = getRequestCompareInfo(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
pullreq.commitIdFrom,
|
||||
pullreq.requestUserName,
|
||||
pullreq.requestRepositoryName,
|
||||
pullreq.commitIdTo
|
||||
pullRequest.commitIdFrom,
|
||||
pullRequest.requestUserName,
|
||||
pullRequest.requestRepositoryName,
|
||||
pullRequest.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 {
|
||||
mergeGitRepository(
|
||||
git,
|
||||
repository,
|
||||
issue,
|
||||
pullRequest,
|
||||
loginAccount,
|
||||
message,
|
||||
strategy,
|
||||
commits,
|
||||
getReceiveHooks()
|
||||
) match {
|
||||
case Some(newCommitId) =>
|
||||
// mark issue as merged and close.
|
||||
val commentId =
|
||||
createComment(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issueId,
|
||||
message,
|
||||
"merge"
|
||||
)
|
||||
createComment(repository.owner, repository.name, loginAccount.userName, issueId, "Close", "close")
|
||||
updateClosed(repository.owner, repository.name, issueId, true)
|
||||
|
||||
// record activity
|
||||
val mergeInfo =
|
||||
MergeInfo(repository.owner, repository.name, loginAccount.userName, issueId, message)
|
||||
recordActivity(mergeInfo)
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
|
||||
// close issue by content of pull request
|
||||
val defaultBranch = getRepository(repository.owner, repository.name).get.repository.defaultBranch
|
||||
if (pullreq.branch == defaultBranch) {
|
||||
if (pullRequest.branch == defaultBranch) {
|
||||
commits.flatten.foreach { commit =>
|
||||
closeIssuesFromMessage(
|
||||
commit.fullMessage,
|
||||
@@ -337,6 +337,14 @@ trait MergeService {
|
||||
).foreach { issueId =>
|
||||
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||
callIssuesWebHook("closed", repository, issue, loginAccount, context.settings)
|
||||
val closeIssueInfo = CloseIssueInfo(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issue.issueId,
|
||||
issue.title
|
||||
)
|
||||
recordActivity(closeIssueInfo)
|
||||
PluginRegistry().getIssueHooks
|
||||
.foreach(_.closedByCommitComment(issue, repository, commit.fullMessage, loginAccount))
|
||||
}
|
||||
@@ -351,6 +359,14 @@ trait MergeService {
|
||||
).foreach { issueId =>
|
||||
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||
callIssuesWebHook("closed", repository, issue, loginAccount, context.settings)
|
||||
val closeIssueInfo = CloseIssueInfo(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issue.issueId,
|
||||
issue.title
|
||||
)
|
||||
recordActivity(closeIssueInfo)
|
||||
PluginRegistry().getIssueHooks
|
||||
.foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount))
|
||||
}
|
||||
@@ -359,6 +375,14 @@ trait MergeService {
|
||||
.foreach { issueId =>
|
||||
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||
callIssuesWebHook("closed", repository, issue, loginAccount, context.settings)
|
||||
val closeIssueInfo = CloseIssueInfo(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issue.issueId,
|
||||
issue.title
|
||||
)
|
||||
recordActivity(closeIssueInfo)
|
||||
PluginRegistry().getIssueHooks
|
||||
.foreach(_.closedByCommitComment(issue, repository, issueContent, loginAccount))
|
||||
}
|
||||
@@ -370,7 +394,7 @@ trait MergeService {
|
||||
updatePullRequests(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
pullreq.branch,
|
||||
pullRequest.branch,
|
||||
loginAccount,
|
||||
"closed",
|
||||
settings
|
||||
@@ -394,6 +418,67 @@ trait MergeService {
|
||||
} else Left("Strategy not allowed")
|
||||
} else Left("Draft pull requests cannot be merged")
|
||||
}
|
||||
|
||||
private def mergeGitRepository(
|
||||
git: Git,
|
||||
repository: RepositoryInfo,
|
||||
issue: Issue,
|
||||
pullRequest: PullRequest,
|
||||
loginAccount: Account,
|
||||
message: String,
|
||||
strategy: String,
|
||||
commits: Seq[Seq[CommitInfo]],
|
||||
receiveHooks: Seq[ReceiveHook]
|
||||
)(implicit s: Session): Option[ObjectId] = {
|
||||
val revCommits = Using
|
||||
.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
commits.flatten.map { commit =>
|
||||
revWalk.parseCommit(git.getRepository.resolve(commit.id))
|
||||
}
|
||||
}
|
||||
.reverse
|
||||
|
||||
strategy match {
|
||||
case "merge-commit" =>
|
||||
Some(
|
||||
mergeWithMergeCommit(
|
||||
git,
|
||||
repository.owner,
|
||||
repository.name,
|
||||
pullRequest.branch,
|
||||
issue.issueId,
|
||||
s"Merge pull request #${issue.issueId} from ${pullRequest.requestUserName}/${pullRequest.requestBranch}\n\n" + message,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
)
|
||||
)
|
||||
case "rebase" =>
|
||||
Some(
|
||||
mergeWithRebase(
|
||||
git,
|
||||
repository.owner,
|
||||
repository.name,
|
||||
pullRequest.branch,
|
||||
issue.issueId,
|
||||
revCommits,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
)
|
||||
)
|
||||
case "squash" =>
|
||||
Some(
|
||||
mergeWithSquash(
|
||||
git,
|
||||
repository.owner,
|
||||
repository.name,
|
||||
pullRequest.branch,
|
||||
issue.issueId,
|
||||
s"${issue.title} (#${issue.issueId})\n\n" + message,
|
||||
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
|
||||
)
|
||||
)
|
||||
case _ =>
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object MergeService {
|
||||
@@ -440,18 +525,22 @@ object MergeService {
|
||||
}
|
||||
}
|
||||
|
||||
class MergeCacheInfo(git: Git, branch: String, issueId: Int) {
|
||||
|
||||
private val repository = git.getRepository
|
||||
|
||||
class MergeCacheInfo(
|
||||
git: Git,
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
branch: String,
|
||||
issueId: Int,
|
||||
receiveHooks: Seq[ReceiveHook]
|
||||
) {
|
||||
private val mergedBranchName = s"refs/pull/${issueId}/merge"
|
||||
private val conflictedBranchName = s"refs/pull/${issueId}/conflict"
|
||||
|
||||
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
|
||||
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
|
||||
lazy val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
|
||||
lazy val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
|
||||
|
||||
def checkConflictCache(): Option[Option[String]] = {
|
||||
Option(repository.resolve(mergedBranchName))
|
||||
Option(git.getRepository.resolve(mergedBranchName))
|
||||
.flatMap { merged =>
|
||||
if (parseCommit(merged).getParents().toSet == Set(mergeBaseTip, mergeTip)) {
|
||||
// merged branch exists
|
||||
@@ -460,7 +549,7 @@ object MergeService {
|
||||
None
|
||||
}
|
||||
}
|
||||
.orElse(Option(repository.resolve(conflictedBranchName)).flatMap { conflicted =>
|
||||
.orElse(Option(git.getRepository.resolve(conflictedBranchName)).flatMap { conflicted =>
|
||||
val commit = parseCommit(conflicted)
|
||||
if (commit.getParents().toSet == Set(mergeBaseTip, mergeTip)) {
|
||||
// conflict branch exists
|
||||
@@ -476,19 +565,19 @@ object MergeService {
|
||||
}
|
||||
|
||||
def checkConflictForce(): Option[String] = {
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
val conflicted = try {
|
||||
!merger.merge(mergeBaseTip, mergeTip)
|
||||
} catch {
|
||||
case e: NoMergeBaseException => true
|
||||
}
|
||||
val mergeTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeTip))
|
||||
val mergeTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeTip))
|
||||
val committer = mergeTipCommit.getCommitterIdent
|
||||
|
||||
def _updateBranch(treeId: ObjectId, message: String, branchName: String): Unit = {
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(treeId, committer, message)
|
||||
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
|
||||
Util.updateRefs(git.getRepository, branchName, mergeCommitId, true, committer)
|
||||
}
|
||||
|
||||
if (!conflicted) {
|
||||
@@ -504,26 +593,48 @@ object MergeService {
|
||||
}
|
||||
|
||||
// update branch from cache
|
||||
def merge(message: String, committer: PersonIdent): ObjectId = {
|
||||
def merge(message: String, committer: PersonIdent)(implicit s: Session): ObjectId = {
|
||||
if (checkConflict().isDefined) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
val mergeResultCommit = parseCommit(Option(repository.resolve(mergedBranchName)).getOrElse {
|
||||
val mergeResultCommit = parseCommit(Option(git.getRepository.resolve(mergedBranchName)).getOrElse {
|
||||
throw new RuntimeException(s"Not found branch ${mergedBranchName}")
|
||||
})
|
||||
// creates merge commit
|
||||
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// update refs
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
|
||||
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]): ObjectId = {
|
||||
def rebase(committer: PersonIdent, commits: Seq[RevCommit])(implicit s: Session): ObjectId = {
|
||||
if (checkConflict().isDefined) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
def _cloneCommit(commit: RevCommit, parentId: ObjectId, baseId: ObjectId): CommitBuilder = {
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
|
||||
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
|
||||
merger.merge(commit.toObjectId, baseId)
|
||||
|
||||
val newCommit = new CommitBuilder()
|
||||
@@ -535,10 +646,10 @@ object MergeService {
|
||||
newCommit
|
||||
}
|
||||
|
||||
val mergeBaseTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeBaseTip))
|
||||
val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip))
|
||||
var previousId = mergeBaseTipCommit.getId
|
||||
|
||||
Using.resource(repository.newObjectInserter) { inserter =>
|
||||
Using.resource(git.getRepository.newObjectInserter) { inserter =>
|
||||
commits.foreach { commit =>
|
||||
val nextCommit = _cloneCommit(commit, previousId, mergeBaseTipCommit.getId)
|
||||
previousId = inserter.insert(nextCommit)
|
||||
@@ -546,17 +657,40 @@ object MergeService {
|
||||
inserter.flush()
|
||||
}
|
||||
|
||||
Util.updateRefs(repository, s"refs/heads/${branch}", previousId, false, committer, Some("rebased"))
|
||||
val refName = s"refs/heads/${branch}"
|
||||
val currentObjectId = git.getRepository.resolve(refName)
|
||||
val receivePack = new ReceivePack(git.getRepository)
|
||||
val receiveCommand = new ReceiveCommand(currentObjectId, previousId, refName)
|
||||
|
||||
// call pre-commit hooks
|
||||
val error = receiveHooks.flatMap { hook =>
|
||||
hook.preReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||
}.headOption
|
||||
|
||||
error.foreach { error =>
|
||||
throw new RuntimeException(error)
|
||||
}
|
||||
|
||||
// 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): ObjectId = {
|
||||
def squash(message: String, committer: PersonIdent)(implicit s: Session): ObjectId = {
|
||||
if (checkConflict().isDefined) {
|
||||
throw new RuntimeException("This pull request can't merge automatically.")
|
||||
}
|
||||
|
||||
val mergeBaseTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeBaseTip))
|
||||
val mergeBaseTipCommit = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(mergeBaseTip))
|
||||
val mergeBranchHeadCommit =
|
||||
Using.resource(new RevWalk(repository))(_.parseCommit(repository.resolve(mergedBranchName)))
|
||||
Using.resource(new RevWalk(git.getRepository))(_.parseCommit(git.getRepository.resolve(mergedBranchName)))
|
||||
|
||||
// Create squash commit
|
||||
val mergeCommit = new CommitBuilder()
|
||||
@@ -567,30 +701,52 @@ object MergeService {
|
||||
mergeCommit.setMessage(message)
|
||||
|
||||
// insertObject and got squash commit Object Id
|
||||
val newCommitId = Using.resource(repository.newObjectInserter) { inserter =>
|
||||
val newCommitId = Using.resource(git.getRepository.newObjectInserter) { inserter =>
|
||||
val newCommitId = inserter.insert(mergeCommit)
|
||||
inserter.flush()
|
||||
newCommitId
|
||||
}
|
||||
|
||||
Util.updateRefs(repository, mergedBranchName, newCommitId, true, committer)
|
||||
val refName = s"refs/heads/${branch}"
|
||||
val currentObjectId = git.getRepository.resolve(refName)
|
||||
val receivePack = new ReceivePack(git.getRepository)
|
||||
val receiveCommand = new ReceiveCommand(currentObjectId, newCommitId, refName)
|
||||
|
||||
// call pre-commit hooks
|
||||
val error = receiveHooks.flatMap { hook =>
|
||||
hook.preReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||
}.headOption
|
||||
|
||||
error.foreach { error =>
|
||||
throw new RuntimeException(error)
|
||||
}
|
||||
|
||||
// update refs
|
||||
Util.updateRefs(git.getRepository, mergedBranchName, newCommitId, true, committer)
|
||||
|
||||
// rebase to squash commit
|
||||
Util.updateRefs(
|
||||
repository,
|
||||
val objectId = Util.updateRefs(
|
||||
git.getRepository,
|
||||
s"refs/heads/${branch}",
|
||||
repository.resolve(mergedBranchName),
|
||||
git.getRepository.resolve(mergedBranchName),
|
||||
false,
|
||||
committer,
|
||||
Some("squashed")
|
||||
)
|
||||
|
||||
// call post-commit hook
|
||||
receiveHooks.foreach { hook =>
|
||||
hook.postReceive(userName, repositoryName, receivePack, receiveCommand, committer.getName, true)
|
||||
}
|
||||
|
||||
objectId
|
||||
}
|
||||
|
||||
// return treeId
|
||||
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
|
||||
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
|
||||
Util.createMergeCommit(git.getRepository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
|
||||
|
||||
private def parseCommit(id: ObjectId) = Using.resource(new RevWalk(repository))(_.parseCommit(id))
|
||||
private def parseCommit(id: ObjectId) = Using.resource(new RevWalk(git.getRepository))(_.parseCommit(id))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ trait MilestonesService {
|
||||
title: String,
|
||||
description: Option[String],
|
||||
dueDate: Option[java.util.Date]
|
||||
)(implicit s: Session): Unit =
|
||||
Milestones insert Milestone(
|
||||
)(implicit s: Session): Int = {
|
||||
Milestones returning Milestones.map(_.milestoneId) insert Milestone(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
title = title,
|
||||
@@ -22,6 +22,7 @@ trait MilestonesService {
|
||||
dueDate = dueDate,
|
||||
closedDate = None
|
||||
)
|
||||
}
|
||||
|
||||
def updateMilestone(milestone: Milestone)(implicit s: Session): Unit =
|
||||
Milestones
|
||||
|
||||
@@ -24,13 +24,15 @@ trait ProtectedBranchService {
|
||||
}
|
||||
.map {
|
||||
case (t1, contexts) =>
|
||||
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
|
||||
new ProtectedBranchInfo(t1.userName, t1.repositoryName, t1.branch, true, contexts, t1.statusCheckAdmin)
|
||||
}
|
||||
|
||||
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(
|
||||
implicit session: Session
|
||||
): ProtectedBranchInfo =
|
||||
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
|
||||
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(
|
||||
ProtectedBranchInfo.disabled(owner, repository, branch)
|
||||
)
|
||||
|
||||
def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] =
|
||||
ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list
|
||||
@@ -66,7 +68,22 @@ object ProtectedBranchService {
|
||||
repository: String,
|
||||
receivePack: ReceivePack,
|
||||
command: ReceiveCommand,
|
||||
pusher: String
|
||||
pusher: String,
|
||||
mergePullRequest: Boolean
|
||||
)(implicit session: Session): Option[String] = {
|
||||
if (mergePullRequest == true) {
|
||||
None
|
||||
} else {
|
||||
checkBranchProtection(owner, repository, receivePack, command, pusher)
|
||||
}
|
||||
}
|
||||
|
||||
private def checkBranchProtection(
|
||||
owner: String,
|
||||
repository: String,
|
||||
receivePack: ReceivePack,
|
||||
command: ReceiveCommand,
|
||||
pusher: String,
|
||||
)(implicit session: Session): Option[String] = {
|
||||
val branch = command.getRefName.stripPrefix("refs/heads/")
|
||||
if (branch != command.getRefName) {
|
||||
@@ -91,6 +108,7 @@ object ProtectedBranchService {
|
||||
case class ProtectedBranchInfo(
|
||||
owner: String,
|
||||
repository: String,
|
||||
branch: String,
|
||||
enabled: Boolean,
|
||||
/**
|
||||
* Require status checks to pass before merging
|
||||
@@ -151,7 +169,7 @@ object ProtectedBranchService {
|
||||
if (contexts.isEmpty) {
|
||||
Set.empty
|
||||
} else {
|
||||
contexts.toSet -- getCommitStatues(owner, repository, sha1)
|
||||
contexts.toSet -- getCommitStatuses(owner, repository, sha1)
|
||||
.filter(_.state == CommitState.SUCCESS)
|
||||
.map(_.context)
|
||||
.toSet
|
||||
@@ -165,7 +183,7 @@ object ProtectedBranchService {
|
||||
}
|
||||
}
|
||||
object ProtectedBranchInfo {
|
||||
def disabled(owner: String, repository: String): ProtectedBranchInfo =
|
||||
ProtectedBranchInfo(owner, repository, false, Nil, false)
|
||||
def disabled(owner: String, repository: String, branch: String): ProtectedBranchInfo =
|
||||
ProtectedBranchInfo(owner, repository, branch, false, Nil, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import difflib.{Delta, DiffUtils}
|
||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||
import gitbucket.core.api.JsonFormat
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.activity.OpenPullRequestInfo
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util.JGitUtil
|
||||
import gitbucket.core.util.StringUtil._
|
||||
import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo}
|
||||
import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo, getBranches}
|
||||
import gitbucket.core.view
|
||||
import gitbucket.core.view.helpers
|
||||
import org.eclipse.jgit.api.Git
|
||||
@@ -57,6 +58,15 @@ trait PullRequestService {
|
||||
.map(pr => pr.isDraft)
|
||||
.update(false)
|
||||
|
||||
def updateBaseBranch(owner: String, repository: String, issueId: Int, baseBranch: String, commitIdTo: String)(
|
||||
implicit s: Session
|
||||
): Unit = {
|
||||
PullRequests
|
||||
.filter(_.byPrimaryKey(owner, repository, issueId))
|
||||
.map(pr => pr.branch -> pr.commitIdTo)
|
||||
.update((baseBranch, commitIdTo))
|
||||
}
|
||||
|
||||
def getPullRequestCountGroupByUser(closed: Boolean, owner: Option[String], repository: Option[String])(
|
||||
implicit s: Session
|
||||
): List[PullRequestCount] =
|
||||
@@ -135,13 +145,14 @@ trait PullRequestService {
|
||||
)
|
||||
|
||||
// record activity
|
||||
recordPullRequestActivity(
|
||||
val openPullRequestInfo = OpenPullRequestInfo(
|
||||
originRepository.owner,
|
||||
originRepository.name,
|
||||
loginAccount.userName,
|
||||
issueId,
|
||||
baseIssue.title
|
||||
)
|
||||
recordActivity(openPullRequestInfo)
|
||||
|
||||
// call web hook
|
||||
callPullRequestWebHook("opened", originRepository, issueId, loginAccount, settings)
|
||||
@@ -291,6 +302,76 @@ trait PullRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
def updatePullRequestsByApi(
|
||||
repository: RepositoryInfo,
|
||||
issueId: Int,
|
||||
loginAccount: Account,
|
||||
settings: SystemSettings,
|
||||
title: Option[String],
|
||||
body: Option[String],
|
||||
state: Option[String],
|
||||
base: Option[String]
|
||||
)(
|
||||
implicit s: Session,
|
||||
c: JsonFormat.Context
|
||||
): Unit = {
|
||||
getPullRequest(repository.owner, repository.name, issueId).foreach {
|
||||
case (issue, pr) =>
|
||||
if (Repositories.filter(_.byRepository(pr.userName, pr.repositoryName)).exists.run) {
|
||||
// Update base branch
|
||||
base.foreach { _base =>
|
||||
if (pr.branch != _base) {
|
||||
Using.resource(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
getBranches(git, repository.repository.defaultBranch, origin = true)
|
||||
.find(_.name == _base)
|
||||
.foreach(br => updateBaseBranch(repository.owner, repository.name, issueId, br.name, br.commitId))
|
||||
}
|
||||
createComment(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issue.issueId,
|
||||
pr.branch + "\r\n" + _base,
|
||||
"change_base_branch"
|
||||
)
|
||||
}
|
||||
}
|
||||
// Update title and content
|
||||
title.foreach { _title =>
|
||||
updateIssue(repository.owner, repository.name, issueId, _title, body)
|
||||
if (issue.title != _title) {
|
||||
createComment(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issue.issueId,
|
||||
issue.title + "\r\n" + _title,
|
||||
"change_title"
|
||||
)
|
||||
}
|
||||
}
|
||||
// Update state
|
||||
val action = (state, issue.closed) match {
|
||||
case (Some("open"), true) =>
|
||||
updateClosed(repository.owner, repository.name, issueId, closed = false)
|
||||
"reopened"
|
||||
case (Some("closed"), false) =>
|
||||
updateClosed(repository.owner, repository.name, issueId, closed = true)
|
||||
"closed"
|
||||
case _ => "edited"
|
||||
}
|
||||
// Call web hook
|
||||
callPullRequestWebHookByRequestBranch(
|
||||
action,
|
||||
getRepository(repository.owner, repository.name).get,
|
||||
pr.requestBranch,
|
||||
loginAccount,
|
||||
settings
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def getPullRequestByRequestCommit(
|
||||
userName: String,
|
||||
repositoryName: String,
|
||||
@@ -530,7 +611,7 @@ object PullRequestService {
|
||||
|
||||
case class MergeStatus(
|
||||
conflictMessage: Option[String],
|
||||
commitStatues: List[CommitStatus],
|
||||
commitStatuses: List[CommitStatus],
|
||||
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
|
||||
branchIsOutOfDate: Boolean,
|
||||
hasUpdatePermission: Boolean,
|
||||
@@ -541,7 +622,7 @@ object PullRequestService {
|
||||
|
||||
val hasConflict = conflictMessage.isDefined
|
||||
val statuses: List[CommitStatus] =
|
||||
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet)
|
||||
commitStatuses ++ (branchProtection.contexts.toSet -- commitStatuses.map(_.context).toSet)
|
||||
.map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
|
||||
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(
|
||||
context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package gitbucket.core.service
|
||||
import gitbucket.core.api.JsonFormat
|
||||
import gitbucket.core.model.{Account, WebHook}
|
||||
import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.activity.{CloseIssueInfo, PushInfo}
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||
import gitbucket.core.service.WebHookService.WebHookPushPayload
|
||||
@@ -17,7 +17,12 @@ import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
|
||||
import scala.util.Using
|
||||
|
||||
trait RepositoryCommitFileService {
|
||||
self: AccountService with ActivityService with IssuesService with PullRequestService with WebHookPullRequestService =>
|
||||
self: AccountService
|
||||
with ActivityService
|
||||
with IssuesService
|
||||
with PullRequestService
|
||||
with WebHookPullRequestService
|
||||
with RepositoryService =>
|
||||
import RepositoryCommitFileService._
|
||||
|
||||
def commitFiles(
|
||||
@@ -149,9 +154,9 @@ trait RepositoryCommitFileService {
|
||||
val receivePack = new ReceivePack(git.getRepository)
|
||||
val receiveCommand = new ReceiveCommand(headTip, commitId, headName)
|
||||
|
||||
// call post commit hook
|
||||
// call pre-commit hook
|
||||
val error = PluginRegistry().getReceiveHooks.flatMap { hook =>
|
||||
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName)
|
||||
hook.preReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false)
|
||||
}.headOption
|
||||
|
||||
error match {
|
||||
@@ -175,8 +180,10 @@ trait RepositoryCommitFileService {
|
||||
updatePullRequests(repository.owner, repository.name, branch, loginAccount, "synchronize", settings)
|
||||
|
||||
// record activity
|
||||
updateLastActivityDate(repository.owner, repository.name)
|
||||
val commitInfo = new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo))
|
||||
val pushInfo = PushInfo(repository.owner, repository.name, loginAccount.userName, branch, List(commitInfo))
|
||||
recordActivity(pushInfo)
|
||||
|
||||
// create issue comment by commit message
|
||||
createIssueComment(repository.owner, repository.name, commitInfo)
|
||||
@@ -186,15 +193,23 @@ trait RepositoryCommitFileService {
|
||||
closeIssuesFromMessage(message, committerName, repository.owner, repository.name).foreach { issueId =>
|
||||
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||
callIssuesWebHook("closed", repository, issue, loginAccount, settings)
|
||||
val closeIssueInfo = CloseIssueInfo(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
loginAccount.userName,
|
||||
issue.issueId,
|
||||
issue.title
|
||||
)
|
||||
recordActivity(closeIssueInfo)
|
||||
PluginRegistry().getIssueHooks
|
||||
.foreach(_.closedByCommitComment(issue, repository, message, loginAccount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// call post commit hook
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach { hook =>
|
||||
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName)
|
||||
hook.postReceive(repository.owner, repository.name, receivePack, receiveCommand, committerName, false)
|
||||
}
|
||||
|
||||
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.nio.file.Files
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.activity.{CreateRepositoryInfo, ForkInfo}
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.{FileUtil, JGitUtil, LockUtil}
|
||||
import gitbucket.core.model.{Account, Role}
|
||||
@@ -160,7 +161,7 @@ trait RepositoryCreationService {
|
||||
createWikiRepository(loginAccount, owner, name)
|
||||
|
||||
// Record activity
|
||||
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||
recordActivity(CreateRepositoryInfo(owner, name, loginUserName))
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.created(owner, name))
|
||||
@@ -180,12 +181,14 @@ trait RepositoryCreationService {
|
||||
Database() withTransaction { implicit session =>
|
||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||
val originDefaultBranchName = repository.repository.defaultBranch
|
||||
|
||||
insertRepository(
|
||||
repositoryName = repository.name,
|
||||
userName = accountName,
|
||||
description = repository.repository.description,
|
||||
isPrivate = repository.repository.isPrivate,
|
||||
defaultBranch = originDefaultBranchName,
|
||||
originRepositoryName = Some(originRepositoryName),
|
||||
originUserName = Some(originUserName),
|
||||
parentRepositoryName = Some(repository.name),
|
||||
@@ -227,7 +230,8 @@ trait RepositoryCreationService {
|
||||
}
|
||||
|
||||
// Record activity
|
||||
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
|
||||
val forkInfo = ForkInfo(repository.owner, repository.name, loginUserName, accountName)
|
||||
recordActivity(forkInfo)
|
||||
|
||||
// Call hooks
|
||||
PluginRegistry().getRepositoryHooks.foreach(_.forked(repository.owner, accountName, repository.name))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.api.JsonFormat
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.util.SyntaxSugars._
|
||||
@@ -9,14 +8,11 @@ import gitbucket.core.model.Profile._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.Profile.dateColumnType
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.WebHookService.WebHookPushPayload
|
||||
import gitbucket.core.util.Directory.{getRepositoryDir, getRepositoryFilesDir, getTemporaryDir, getWikiRepositoryDir}
|
||||
import gitbucket.core.util.JGitUtil.{CommitInfo, FileInfo}
|
||||
import gitbucket.core.util.JGitUtil.FileInfo
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.dircache.{DirCache, DirCacheBuilder}
|
||||
import org.eclipse.jgit.lib.{Repository => _, _}
|
||||
import org.eclipse.jgit.transport.{ReceiveCommand, ReceivePack}
|
||||
import scala.util.Using
|
||||
|
||||
trait RepositoryService {
|
||||
@@ -38,6 +34,7 @@ trait RepositoryService {
|
||||
userName: String,
|
||||
description: Option[String],
|
||||
isPrivate: Boolean,
|
||||
defaultBranch: String = "master",
|
||||
originRepositoryName: Option[String] = None,
|
||||
originUserName: Option[String] = None,
|
||||
parentRepositoryName: Option[String] = None,
|
||||
@@ -49,7 +46,7 @@ trait RepositoryService {
|
||||
repositoryName = repositoryName,
|
||||
isPrivate = isPrivate,
|
||||
description = description,
|
||||
defaultBranch = "master",
|
||||
defaultBranch = defaultBranch,
|
||||
registeredDate = currentDate,
|
||||
updatedDate = currentDate,
|
||||
lastActivityDate = currentDate,
|
||||
@@ -119,15 +116,6 @@ trait RepositoryService {
|
||||
}
|
||||
.update(newUserName, newRepositoryName)
|
||||
|
||||
// Updates activity fk before deleting repository because activity is sorted by activityId
|
||||
// and it can't be changed by deleting-and-inserting record.
|
||||
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
||||
Activities
|
||||
.filter(_.activityId === activity.activityId.bind)
|
||||
.map(x => (x.userName, x.repositoryName))
|
||||
.update(newUserName, newRepositoryName)
|
||||
}
|
||||
|
||||
deleteRepositoryOnModel(oldUserName, oldRepositoryName)
|
||||
|
||||
RepositoryWebHooks.insertAll(
|
||||
@@ -213,50 +201,6 @@ trait RepositoryService {
|
||||
collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)): _*
|
||||
)
|
||||
|
||||
// Update activity messages
|
||||
Activities
|
||||
.filter { t =>
|
||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") ||
|
||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}@%")
|
||||
}
|
||||
.map { t =>
|
||||
t.activityId -> t.message
|
||||
}
|
||||
.list
|
||||
.foreach {
|
||||
case (activityId, message) =>
|
||||
Activities
|
||||
.filter(_.activityId === activityId.bind)
|
||||
.map(_.message)
|
||||
.update(
|
||||
message
|
||||
.replace(
|
||||
s"[repo:${oldUserName}/${oldRepositoryName}]",
|
||||
s"[repo:${newUserName}/${newRepositoryName}]"
|
||||
)
|
||||
.replace(
|
||||
s"[branch:${oldUserName}/${oldRepositoryName}#",
|
||||
s"[branch:${newUserName}/${newRepositoryName}#"
|
||||
)
|
||||
.replace(
|
||||
s"[tag:${oldUserName}/${oldRepositoryName}#",
|
||||
s"[tag:${newUserName}/${newRepositoryName}#"
|
||||
)
|
||||
.replace(
|
||||
s"[pullreq:${oldUserName}/${oldRepositoryName}#",
|
||||
s"[pullreq:${newUserName}/${newRepositoryName}#"
|
||||
)
|
||||
.replace(
|
||||
s"[issue:${oldUserName}/${oldRepositoryName}#",
|
||||
s"[issue:${newUserName}/${newRepositoryName}#"
|
||||
)
|
||||
.replace(
|
||||
s"[commit:${oldUserName}/${oldRepositoryName}@",
|
||||
s"[commit:${newUserName}/${newRepositoryName}@"
|
||||
)
|
||||
)
|
||||
}
|
||||
// Move git repository
|
||||
defining(getRepositoryDir(oldUserName, oldRepositoryName)) { dir =>
|
||||
if (dir.isDirectory) {
|
||||
@@ -304,7 +248,7 @@ trait RepositoryService {
|
||||
}
|
||||
|
||||
private def deleteRepositoryOnModel(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||
Activities.filter(_.byRepository(userName, repositoryName)).delete
|
||||
// Activities.filter(_.byRepository(userName, repositoryName)).delete
|
||||
Collaborators.filter(_.byRepository(userName, repositoryName)).delete
|
||||
CommitComments.filter(_.byRepository(userName, repositoryName)).delete
|
||||
IssueLabels.filter(_.byRepository(userName, repositoryName)).delete
|
||||
@@ -399,6 +343,10 @@ trait RepositoryService {
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
),
|
||||
getOpenMilestones(
|
||||
repository.originUserName.getOrElse(repository.userName),
|
||||
repository.originRepositoryName.getOrElse(repository.repositoryName)
|
||||
),
|
||||
getRepositoryManagers(repository.userName, repository.repositoryName)
|
||||
)
|
||||
}
|
||||
@@ -432,6 +380,21 @@ trait RepositoryService {
|
||||
.list
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the all public repositories.
|
||||
*
|
||||
* @return the repository information list
|
||||
*/
|
||||
def getPublicRepositories(withoutPhysicalInfo: Boolean = false)(implicit s: Session): List[RepositoryInfo] = {
|
||||
Repositories
|
||||
.filter { t1 =>
|
||||
t1.isPrivate === false.bind
|
||||
}
|
||||
.sortBy(_.lastActivityDate desc)
|
||||
.list
|
||||
.map(createRepositoryInfo(_, withoutPhysicalInfo))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of repositories which are owned by the specified user.
|
||||
* This list includes group repositories if the specified user is a member of the group.
|
||||
@@ -482,7 +445,7 @@ trait RepositoryService {
|
||||
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
|
||||
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
|
||||
* branches and tags
|
||||
* @param limit if true then result will include only repositories associated with the login account.
|
||||
* @param limit if true then result will include only repositories owned by the login account. otherwise, result will be all visible repositories.
|
||||
* @return the repository information which is sorted in descending order of lastActivityDate.
|
||||
*/
|
||||
def getVisibleRepositories(
|
||||
@@ -754,6 +717,14 @@ trait RepositoryService {
|
||||
(t.originUserName === userName.bind) && (t.originRepositoryName === repositoryName.bind)
|
||||
}.length).first
|
||||
|
||||
private def getOpenMilestones(userName: String, repositoryName: String)(implicit s: Session): Int =
|
||||
Query(
|
||||
Milestones
|
||||
.filter(_.byRepository(userName, repositoryName))
|
||||
.filter(_.closedDate.isEmpty)
|
||||
.length
|
||||
).first
|
||||
|
||||
def getForkedRepositories(userName: String, repositoryName: String)(implicit s: Session): List[Repository] =
|
||||
Repositories
|
||||
.filter { t =>
|
||||
@@ -785,9 +756,10 @@ trait RepositoryService {
|
||||
|
||||
// Get template file from project root. When didn't find, will lookup default folder.
|
||||
Using.resource(Git.open(Directory.getRepositoryDir(repository.owner, repository.name))) { git =>
|
||||
choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, "."))
|
||||
// maxFiles = 1 means not get commit info because the only objectId and filename are necessary here
|
||||
choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".", maxFiles = 1))
|
||||
.orElse {
|
||||
choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".gitbucket"))
|
||||
choiceTemplate(JGitUtil.getFileList(git, repository.repository.defaultBranch, ".gitbucket", maxFiles = 1))
|
||||
}
|
||||
.map { file =>
|
||||
JGitUtil.getContentFromId(git, file.id, true).collect {
|
||||
@@ -806,6 +778,7 @@ object RepositoryService {
|
||||
issueCount: Int,
|
||||
pullCount: Int,
|
||||
forkedCount: Int,
|
||||
milestoneCount: Int,
|
||||
branchList: Seq[String],
|
||||
tags: Seq[JGitUtil.TagInfo],
|
||||
managers: Seq[String]
|
||||
@@ -820,15 +793,27 @@ object RepositoryService {
|
||||
issueCount: Int,
|
||||
pullCount: Int,
|
||||
forkedCount: Int,
|
||||
milestoneCount: Int,
|
||||
managers: Seq[String]
|
||||
) =
|
||||
this(repo.owner, repo.name, model, issueCount, pullCount, forkedCount, repo.branchList, repo.tags, managers)
|
||||
this(
|
||||
repo.owner,
|
||||
repo.name,
|
||||
model,
|
||||
issueCount,
|
||||
pullCount,
|
||||
forkedCount,
|
||||
milestoneCount,
|
||||
repo.branchList,
|
||||
repo.tags,
|
||||
managers
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates instance without issue and pull request count.
|
||||
* Creates instance without issue, pull request, and milestone count.
|
||||
*/
|
||||
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
|
||||
this(repo.owner, repo.name, model, 0, 0, forkedCount, repo.branchList, repo.tags, managers)
|
||||
this(repo.owner, repo.name, model, 0, 0, forkedCount, 0, repo.branchList, repo.tags, managers)
|
||||
|
||||
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
|
||||
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
|
||||
@@ -854,4 +839,9 @@ object RepositoryService {
|
||||
} else None
|
||||
def openRepoUrl(openUrl: String)(implicit context: Context): String =
|
||||
s"github-${context.platform}://openRepo/${openUrl}"
|
||||
|
||||
def readmeFiles: Seq[String] =
|
||||
PluginRegistry().renderableExtensions.map { extension =>
|
||||
s"readme.${extension}"
|
||||
} ++ Seq("readme.txt", "readme")
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package gitbucket.core.service
|
||||
|
||||
import gitbucket.core.model.{Session, Issue, Account}
|
||||
import gitbucket.core.model.{Account, Issue, Repository, Session}
|
||||
import gitbucket.core.util.Implicits
|
||||
import gitbucket.core.controller.Context
|
||||
import Implicits.request2Session
|
||||
import gitbucket.core.model.Profile.{Accounts, Repositories}
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
|
||||
/**
|
||||
* This service is used for a view helper mainly.
|
||||
@@ -23,21 +25,41 @@ trait RequestCache
|
||||
private implicit def context2Session(implicit context: Context): Session =
|
||||
request2Session(context.request)
|
||||
|
||||
def getIssue(userName: String, repositoryName: String, issueId: String)(implicit context: Context): Option[Issue] = {
|
||||
def getIssueFromCache(userName: String, repositoryName: String, issueId: String)(
|
||||
implicit context: Context
|
||||
): Option[Issue] = {
|
||||
context.cache(s"issue.${userName}/${repositoryName}#${issueId}") {
|
||||
super.getIssue(userName, repositoryName, issueId)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByUserName(userName: String)(implicit context: Context): Option[Account] = {
|
||||
def getAccountByUserNameFromCache(userName: String)(implicit context: Context): Option[Account] = {
|
||||
context.cache(s"account.${userName}") {
|
||||
super.getAccountByUserName(userName)
|
||||
}
|
||||
}
|
||||
|
||||
def getAccountByMailAddress(mailAddress: String)(implicit context: Context): Option[Account] = {
|
||||
def getAccountByMailAddressFromCache(mailAddress: String)(implicit context: Context): Option[Account] = {
|
||||
context.cache(s"account.${mailAddress}") {
|
||||
super.getAccountByMailAddress(mailAddress)
|
||||
}
|
||||
}
|
||||
|
||||
def getRepositoryInfoFromCache(userName: String, repositoryName: String)(
|
||||
implicit context: Context
|
||||
): Option[Repository] = {
|
||||
context.cache(s"repository.${userName}/${repositoryName}") {
|
||||
Repositories
|
||||
.join(Accounts)
|
||||
.on(_.userName === _.userName)
|
||||
.filter {
|
||||
case (t1, t2) =>
|
||||
t1.byRepository(userName, repositoryName) && t2.removed === false.bind
|
||||
}
|
||||
.map {
|
||||
case (t1, t2) => t1
|
||||
}
|
||||
.firstOption
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ trait SystemSettingsService {
|
||||
props.setProperty(RepositoryOperationFork, settings.repositoryOperation.fork.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
|
||||
props.setProperty(LimitVisibleRepositories, settings.limitVisibleRepositories.toString)
|
||||
props.setProperty(SshEnabled, settings.ssh.enabled.toString)
|
||||
settings.ssh.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
|
||||
@@ -83,6 +82,7 @@ trait SystemSettingsService {
|
||||
props.setProperty(UploadTimeout, settings.upload.timeout.toString)
|
||||
props.setProperty(UploadLargeMaxFileSize, settings.upload.largeMaxFileSize.toString)
|
||||
props.setProperty(UploadLargeTimeout, settings.upload.largeTimeout.toString)
|
||||
props.setProperty(RepositoryViewerMaxFiles, settings.repositoryViewer.maxFiles.toString)
|
||||
|
||||
Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out =>
|
||||
props.store(out, null)
|
||||
@@ -112,7 +112,6 @@ trait SystemSettingsService {
|
||||
),
|
||||
getValue(props, Gravatar, false),
|
||||
getValue(props, Notification, false),
|
||||
getOptionValue[Int](props, ActivityLogLimit, None),
|
||||
getValue(props, LimitVisibleRepositories, false),
|
||||
Ssh(
|
||||
getValue(props, SshEnabled, false),
|
||||
@@ -179,6 +178,9 @@ trait SystemSettingsService {
|
||||
getValue(props, UploadTimeout, 3 * 10000),
|
||||
getValue(props, UploadLargeMaxFileSize, 3 * 1024 * 1024),
|
||||
getValue(props, UploadLargeTimeout, 3 * 10000)
|
||||
),
|
||||
RepositoryViewerSettings(
|
||||
getValue(props, RepositoryViewerMaxFiles, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -200,7 +202,6 @@ object SystemSettingsService {
|
||||
repositoryOperation: RepositoryOperation,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
activityLogLimit: Option[Int],
|
||||
limitVisibleRepositories: Boolean,
|
||||
ssh: Ssh,
|
||||
useSMTP: Boolean,
|
||||
@@ -213,7 +214,8 @@ object SystemSettingsService {
|
||||
userDefinedCss: Option[String],
|
||||
showMailAddress: Boolean,
|
||||
webHook: WebHook,
|
||||
upload: Upload
|
||||
upload: Upload,
|
||||
repositoryViewer: RepositoryViewerSettings
|
||||
) {
|
||||
|
||||
def baseUrl(request: HttpServletRequest): String =
|
||||
@@ -303,6 +305,8 @@ object SystemSettingsService {
|
||||
|
||||
case class Upload(maxFileSize: Long, timeout: Long, largeMaxFileSize: Long, largeTimeout: Long)
|
||||
|
||||
case class RepositoryViewerSettings(maxFiles: Int)
|
||||
|
||||
val DefaultSshPort = 29418
|
||||
val DefaultSmtpPort = 25
|
||||
val DefaultLdapPort = 389
|
||||
@@ -360,6 +364,7 @@ object SystemSettingsService {
|
||||
private val UploadTimeout = "upload.timeout"
|
||||
private val UploadLargeMaxFileSize = "upload.largeMaxFileSize"
|
||||
private val UploadLargeTimeout = "upload.largeTimeout"
|
||||
private val RepositoryViewerMaxFiles = "repository_viewer_max_files"
|
||||
|
||||
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
|
||||
getConfigValue(key).getOrElse {
|
||||
|
||||
@@ -83,7 +83,7 @@ trait WebHookService {
|
||||
implicit s: Session
|
||||
): Option[(RepositoryWebHook, Set[WebHook.Event])] =
|
||||
RepositoryWebHooks
|
||||
.filter(_.byPrimaryKey(owner, repository, url))
|
||||
.filter(_.byRepositoryUrl(owner, repository, url))
|
||||
.join(RepositoryWebHookEvents)
|
||||
.on { (w, t) =>
|
||||
t.byRepositoryWebHook(w)
|
||||
@@ -95,6 +95,24 @@ trait WebHookService {
|
||||
.mapValues(_.map(_._2).toSet)
|
||||
.headOption
|
||||
|
||||
/** get All WebHook informations of repository */
|
||||
def getWebHookById(id: Int)(
|
||||
implicit s: Session
|
||||
): Option[(RepositoryWebHook, Set[WebHook.Event])] =
|
||||
RepositoryWebHooks
|
||||
.filter(_.byId(id))
|
||||
.join(RepositoryWebHookEvents)
|
||||
.on { (w, t) =>
|
||||
t.byRepositoryWebHook(w)
|
||||
}
|
||||
.map { case (w, t) => w -> t.event }
|
||||
.list
|
||||
.groupBy(_._1)
|
||||
.view
|
||||
.mapValues(_.map(_._2).toSet)
|
||||
.toList
|
||||
.headOption
|
||||
|
||||
def addWebHook(
|
||||
owner: String,
|
||||
repository: String,
|
||||
@@ -103,7 +121,13 @@ trait WebHookService {
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
)(implicit s: Session): Unit = {
|
||||
RepositoryWebHooks insert RepositoryWebHook(owner, repository, url, ctype, token)
|
||||
RepositoryWebHooks insert RepositoryWebHook(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
url = url,
|
||||
ctype = ctype,
|
||||
token = token
|
||||
)
|
||||
events.map { event: WebHook.Event =>
|
||||
RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
|
||||
}
|
||||
@@ -118,7 +142,7 @@ trait WebHookService {
|
||||
token: Option[String]
|
||||
)(implicit s: Session): Unit = {
|
||||
RepositoryWebHooks
|
||||
.filter(_.byPrimaryKey(owner, repository, url))
|
||||
.filter(_.byRepositoryUrl(owner, repository, url))
|
||||
.map(w => (w.ctype, w.token))
|
||||
.update((ctype, token))
|
||||
RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
|
||||
@@ -127,8 +151,30 @@ trait WebHookService {
|
||||
}
|
||||
}
|
||||
|
||||
def updateWebHookByApi(
|
||||
id: Int,
|
||||
owner: String,
|
||||
repository: String,
|
||||
url: String,
|
||||
events: Set[WebHook.Event],
|
||||
ctype: WebHookContentType,
|
||||
token: Option[String]
|
||||
)(implicit s: Session): Unit = {
|
||||
RepositoryWebHooks
|
||||
.filter(_.byId(id))
|
||||
.map(w => (w.url, w.ctype, w.token))
|
||||
.update((url, ctype, token))
|
||||
RepositoryWebHookEvents.filter(_.byRepositoryWebHook(owner, repository, url)).delete
|
||||
events.map { event: WebHook.Event =>
|
||||
RepositoryWebHookEvents insert RepositoryWebHookEvent(owner, repository, url, event)
|
||||
}
|
||||
}
|
||||
|
||||
def deleteWebHook(owner: String, repository: String, url: String)(implicit s: Session): Unit =
|
||||
RepositoryWebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
|
||||
RepositoryWebHooks.filter(_.byRepositoryUrl(owner, repository, url)).delete
|
||||
|
||||
def deleteWebHookById(id: Int)(implicit s: Session): Unit =
|
||||
RepositoryWebHooks.filter(_.byId(id)).delete
|
||||
|
||||
/** get All AccountWebHook informations of user */
|
||||
def getAccountWebHooks(owner: String)(implicit s: Session): List[(AccountWebHook, Set[WebHook.Event])] =
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util
|
||||
import java.util.Date
|
||||
|
||||
import scala.util.Using
|
||||
|
||||
import gitbucket.core.api
|
||||
import gitbucket.core.model.WebHook
|
||||
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
||||
@@ -16,6 +15,19 @@ import gitbucket.core.util.SyntaxSugars._
|
||||
import gitbucket.core.util.Implicits._
|
||||
import gitbucket.core.util._
|
||||
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||
import gitbucket.core.model.activity.{
|
||||
BaseActivityInfo,
|
||||
CloseIssueInfo,
|
||||
CreateBranchInfo,
|
||||
CreateTagInfo,
|
||||
CreateWikiPageInfo,
|
||||
DeleteBranchInfo,
|
||||
DeleteTagInfo,
|
||||
DeleteWikiInfo,
|
||||
EditWikiPageInfo,
|
||||
PushInfo
|
||||
}
|
||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||
// Imported names have higher precedence than names, defined in other files.
|
||||
// If Database is not bound by explicit import, then "Database" refers to the Database introduced by the wildcard import above.
|
||||
import gitbucket.core.servlet.Database
|
||||
@@ -240,7 +252,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
with WebHookPullRequestService
|
||||
with WebHookPullRequestReviewCommentService
|
||||
with CommitsService
|
||||
with SystemSettingsService {
|
||||
with SystemSettingsService
|
||||
with RequestCache {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
|
||||
private var existIds: Seq[String] = Nil
|
||||
@@ -251,7 +264,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
commands.asScala.foreach { command =>
|
||||
// call pre-commit hook
|
||||
PluginRegistry().getReceiveHooks
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
|
||||
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher, false))
|
||||
.headOption
|
||||
.foreach { error =>
|
||||
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
|
||||
@@ -305,8 +318,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
|
||||
// Retrieve all issue count in the repository
|
||||
val issueCount =
|
||||
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
||||
countIssue(IssueSearchCondition(state = "open"), IssueSearchOption.Issues, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), IssueSearchOption.Issues, owner -> repository)
|
||||
|
||||
// Extract new commit and apply issue comment
|
||||
val defaultBranch = repositoryInfo.repository.defaultBranch
|
||||
@@ -321,6 +334,9 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository).foreach { issueId =>
|
||||
getIssue(owner, repository, issueId.toString).foreach { issue =>
|
||||
callIssuesWebHook("closed", repositoryInfo, issue, pusherAccount, settings)
|
||||
val closeIssueInfo =
|
||||
CloseIssueInfo(owner, repository, pusherAccount.userName, issue.issueId, issue.title)
|
||||
recordActivity(closeIssueInfo)
|
||||
PluginRegistry().getIssueHooks
|
||||
.foreach(_.closedByCommitComment(issue, repositoryInfo, commit.fullMessage, pusherAccount))
|
||||
}
|
||||
@@ -348,17 +364,25 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
// record activity
|
||||
if (refName(1) == "heads") {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE => recordCreateBranchActivity(owner, repository, pusher, branchName)
|
||||
case ReceiveCommand.Type.UPDATE => recordPushActivity(owner, repository, pusher, branchName, newCommits)
|
||||
case ReceiveCommand.Type.DELETE => recordDeleteBranchActivity(owner, repository, pusher, branchName)
|
||||
case _ =>
|
||||
case ReceiveCommand.Type.CREATE =>
|
||||
val createBranchInfo = CreateBranchInfo(owner, repository, pusher, branchName)
|
||||
recordActivity(createBranchInfo)
|
||||
case ReceiveCommand.Type.UPDATE =>
|
||||
val pushInfo = PushInfo(owner, repository, pusher, branchName, newCommits)
|
||||
recordActivity(pushInfo)
|
||||
case ReceiveCommand.Type.DELETE =>
|
||||
val deleteBranchInfo = DeleteBranchInfo(owner, repository, pusher, branchName)
|
||||
recordActivity(deleteBranchInfo)
|
||||
case _ =>
|
||||
}
|
||||
} else if (refName(1) == "tags") {
|
||||
command.getType match {
|
||||
case ReceiveCommand.Type.CREATE =>
|
||||
recordCreateTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
val createTagInfo = CreateTagInfo(owner, repository, pusher, branchName)
|
||||
recordActivity(createTagInfo)
|
||||
case ReceiveCommand.Type.DELETE =>
|
||||
recordDeleteTagActivity(owner, repository, pusher, branchName, newCommits)
|
||||
val deleteTagInfo = DeleteTagInfo(owner, repository, pusher, branchName)
|
||||
recordActivity(deleteTagInfo)
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
@@ -411,7 +435,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
}
|
||||
|
||||
// call post-commit hook
|
||||
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
|
||||
PluginRegistry().getReceiveHooks
|
||||
.foreach(_.postReceive(owner, repository, receivePack, command, pusher, false))
|
||||
}
|
||||
}
|
||||
// update repository last modified time.
|
||||
@@ -432,7 +457,9 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
with WebHookService
|
||||
with AccountService
|
||||
with RepositoryService
|
||||
with SystemSettingsService {
|
||||
with ActivityService
|
||||
with SystemSettingsService
|
||||
with RequestCache {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
|
||||
|
||||
@@ -460,18 +487,22 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
val diffs = JGitUtil.getDiffs(git, None, commit.id, false, false)
|
||||
diffs.collect {
|
||||
case diff if diff.newPath.toLowerCase.endsWith(".md") =>
|
||||
val action = if (diff.changeType == ChangeType.ADD) "created" else "edited"
|
||||
val action = mapToAction(diff.changeType)
|
||||
val fileName = diff.newPath
|
||||
updateLastActivityDate(owner, repository)
|
||||
buildWikiRecord(action, owner, repository, commit, fileName).foreach(recordActivity)
|
||||
(action, fileName, commit.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pages = commits
|
||||
.groupBy { case (action, fileName, commitId) => fileName }
|
||||
.groupBy { case (_, fileName, _) => fileName }
|
||||
.map {
|
||||
case (fileName, commits) =>
|
||||
(commits.head._1, fileName, commits.last._3)
|
||||
val (commitHeadAction, _, _) = commits.head
|
||||
val (_, _, commitLastId) = commits.last
|
||||
(commitHeadAction, fileName, commitLastId)
|
||||
}
|
||||
|
||||
callWebHookOf(owner, repository, WebHook.Gollum, settings) {
|
||||
@@ -494,6 +525,32 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
}
|
||||
}
|
||||
|
||||
private[this] def mapToAction(changeType: ChangeType): String = changeType match {
|
||||
case ChangeType.ADD | ChangeType.RENAME => "created"
|
||||
case ChangeType.MODIFY => "edited"
|
||||
case ChangeType.DELETE => "deleted"
|
||||
case other =>
|
||||
logger.error(s"Unsupported Wiki action: $other")
|
||||
"unsupported action"
|
||||
}
|
||||
|
||||
private[this] def buildWikiRecord(
|
||||
action: String,
|
||||
owner: String,
|
||||
repo: String,
|
||||
commit: CommitInfo,
|
||||
fileName: String
|
||||
): Option[BaseActivityInfo] = {
|
||||
val pageName = fileName.dropRight(".md".length)
|
||||
action match {
|
||||
case "created" => Some(CreateWikiPageInfo(owner, repo, commit.committerName, pageName))
|
||||
case "edited" => Some(EditWikiPageInfo(owner, repo, commit.committerName, pageName, commit.id))
|
||||
case "deleted" => Some(DeleteWikiInfo(owner, repo, commit.committerName, pageName))
|
||||
case other =>
|
||||
logger.info(s"Attempted to build wiki record for unsupported action: $other")
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object GitLfs {
|
||||
|
||||
@@ -2,11 +2,10 @@ package gitbucket.core.servlet
|
||||
|
||||
import java.io.{File, FileOutputStream}
|
||||
|
||||
import akka.event.Logging
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import gitbucket.core.GitBucketCoreModule
|
||||
import gitbucket.core.plugin.PluginRegistry
|
||||
import gitbucket.core.service.{ActivityService, SystemSettingsService}
|
||||
import gitbucket.core.service.SystemSettingsService
|
||||
import gitbucket.core.util.DatabaseConfig
|
||||
import gitbucket.core.util.Directory._
|
||||
import gitbucket.core.util.JDBCUtil._
|
||||
@@ -21,8 +20,6 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
|
||||
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.slf4j.LoggerFactory
|
||||
import akka.actor.{Actor, ActorSystem, Props}
|
||||
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
||||
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.Using
|
||||
@@ -35,23 +32,23 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
|
||||
|
||||
// ActorSystem for Quartz scheduler
|
||||
private val system = ActorSystem(
|
||||
"job",
|
||||
ConfigFactory.parseString("""
|
||||
|akka {
|
||||
| daemonic = on
|
||||
| coordinated-shutdown.run-by-jvm-shutdown-hook = off
|
||||
| quartz {
|
||||
| schedules {
|
||||
| Daily {
|
||||
| expression = "0 0 0 * * ?"
|
||||
| }
|
||||
| }
|
||||
| }
|
||||
|}
|
||||
""".stripMargin)
|
||||
)
|
||||
// // ActorSystem for Quartz scheduler
|
||||
// private val system = ActorSystem(
|
||||
// "job",
|
||||
// ConfigFactory.parseString("""
|
||||
// |akka {
|
||||
// | daemonic = on
|
||||
// | coordinated-shutdown.run-by-jvm-shutdown-hook = off
|
||||
// | quartz {
|
||||
// | schedules {
|
||||
// | Daily {
|
||||
// | expression = "0 0 0 * * ?"
|
||||
// | }
|
||||
// | }
|
||||
// | }
|
||||
// |}
|
||||
// """.stripMargin)
|
||||
// )
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
@@ -95,10 +92,10 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
|
||||
}
|
||||
|
||||
// Start Quartz scheduler
|
||||
val scheduler = QuartzSchedulerExtension(system)
|
||||
|
||||
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
|
||||
// // Start Quartz scheduler
|
||||
// val scheduler = QuartzSchedulerExtension(system)
|
||||
//
|
||||
// scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
|
||||
}
|
||||
|
||||
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
|
||||
@@ -172,8 +169,8 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
}
|
||||
|
||||
override def contextDestroyed(event: ServletContextEvent): Unit = {
|
||||
// Shutdown Quartz scheduler
|
||||
system.terminate()
|
||||
// // Shutdown Quartz scheduler
|
||||
// system.terminate()
|
||||
// Shutdown plugins
|
||||
PluginRegistry.shutdown(event.getServletContext, loadSystemSettings())
|
||||
// Close datasource
|
||||
@@ -181,21 +178,3 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService {
|
||||
|
||||
private val logger = Logging(context.system, this)
|
||||
|
||||
def receive = {
|
||||
case s: String => {
|
||||
loadSystemSettings().activityLogLimit.foreach { limit =>
|
||||
if (limit > 0) {
|
||||
Database() withTransaction { implicit session =>
|
||||
val rows = deleteOldActivities(limit)
|
||||
logger.info(s"Deleted ${rows} activity logs")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ object Directory {
|
||||
|
||||
val GitBucketConf = new File(GitBucketHome, "gitbucket.conf")
|
||||
|
||||
val ActivityLog = new File(GitBucketHome, "activity.log")
|
||||
|
||||
val RepositoryHome = s"${GitBucketHome}/repositories"
|
||||
|
||||
val DatabaseHome = s"${GitBucketHome}/data"
|
||||
|
||||
@@ -374,6 +374,7 @@ object JGitUtil {
|
||||
* @param path the directory path (optional)
|
||||
* @param baseUrl the base url of GitBucket instance. This parameter is used to generate links of submodules (optional)
|
||||
* @param commitCount the number of commit of this repository (optional). If this number is greater than threshold, the commit info is cached in memory.
|
||||
* @param maxFiles don't fetch commit info if the number of files in the directory is bigger than this number.
|
||||
* @return The list of files in the specified directory. If the number of files are greater than threshold, the returned file list won't include the commit info.
|
||||
*/
|
||||
def getFileList(
|
||||
@@ -381,7 +382,8 @@ object JGitUtil {
|
||||
revision: String,
|
||||
path: String = ".",
|
||||
baseUrl: Option[String] = None,
|
||||
commitCount: Int = 0
|
||||
commitCount: Int = 0,
|
||||
maxFiles: Int = 100
|
||||
): List[FileInfo] = {
|
||||
Using.resource(new RevWalk(git.getRepository)) { revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
@@ -436,13 +438,13 @@ object JGitUtil {
|
||||
): List[(ObjectId, FileMode, String, String, Option[String], Option[RevCommit])] = {
|
||||
fileList.map {
|
||||
case (id, mode, name, path, opt) =>
|
||||
// Don't attempt to get the last commit if the number of files is very large.
|
||||
if (fileList.size >= 100) {
|
||||
if (maxFiles > 0 && fileList.size >= maxFiles) {
|
||||
// Don't attempt to get the last commit if the number of files is very large.
|
||||
(id, mode, name, path, opt, None)
|
||||
} else if (commitCount < 10000) {
|
||||
val i = git
|
||||
(id, mode, name, path, opt, Some(getCommit(path)))
|
||||
} else {
|
||||
// Use in-memory cache if the commit count is too big.
|
||||
val cached = objectCommitCache.getEntry(id)
|
||||
if (cached == null) {
|
||||
val commit = getCommit(path)
|
||||
|
||||
@@ -151,8 +151,13 @@ object StringUtil {
|
||||
* @return the iterator of issue id
|
||||
*/
|
||||
def extractCloseId(message: String): Seq[String] =
|
||||
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(?!\\w)".r
|
||||
.findAllIn(message)
|
||||
"#(\\d+)".r
|
||||
.findAllIn(
|
||||
"(?i)(?<!\\w)(?:fix(?:e[sd])?|resolve[sd]?|close[sd]?)\\s+#(\\d+)(,\\s?#(\\d+))*(?!\\w)".r
|
||||
.findAllIn(message)
|
||||
.toSeq
|
||||
.mkString(",")
|
||||
)
|
||||
.matchData
|
||||
.map(_.group(1))
|
||||
.toSeq
|
||||
@@ -177,4 +182,11 @@ object StringUtil {
|
||||
}
|
||||
}
|
||||
|
||||
def cutTail(txt: String, limit: Int, suffix: String = ""): String = {
|
||||
txt.length match {
|
||||
case x if x > limit => txt.substring(0, limit).concat(suffix)
|
||||
case _ => txt
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
|
||||
val src = if (mailAddress.isEmpty) {
|
||||
// by user name
|
||||
getAccountByUserName(userName).map { account =>
|
||||
getAccountByUserNameFromCache(userName).map { account =>
|
||||
if (account.image.isEmpty && context.settings.gravatar) {
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
@@ -28,7 +28,7 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
}
|
||||
} else {
|
||||
// by mail address
|
||||
getAccountByMailAddress(mailAddress).map { account =>
|
||||
getAccountByMailAddressFromCache(mailAddress).map { account =>
|
||||
if (account.image.isEmpty && context.settings.gravatar) {
|
||||
s"""https://www.gravatar.com/avatar/${StringUtil.md5(account.mailAddress.toLowerCase)}?s=${size}&d=retro&r=g"""
|
||||
} else {
|
||||
@@ -45,11 +45,11 @@ trait AvatarImageProvider { self: RequestCache =>
|
||||
|
||||
if (tooltip) {
|
||||
Html(
|
||||
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}"/>"""
|
||||
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" data-toggle="tooltip" title="${userName}" alt="@${userName}" />"""
|
||||
)
|
||||
} else {
|
||||
Html(
|
||||
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" />"""
|
||||
s"""<img src="${src}" class="${if (size > 20) { "avatar" } else { "avatar-mini" }}" style="width: ${size}px; height: ${size}px;" alt="@${userName}" />"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ trait LinkConverter { self: RequestCache =>
|
||||
val userName = repository.repository.userName
|
||||
val repositoryName = repository.repository.repositoryName
|
||||
|
||||
getIssue(userName, repositoryName, issueId.toString) match {
|
||||
getIssueFromCache(userName, repositoryName, issueId.toString) match {
|
||||
case Some(issue) =>
|
||||
s"""<a href="${context.path}/${userName}/${repositoryName}/${if (issue.isPullRequest) "pull" else "issues"}/${issueId}"><strong>${StringUtil
|
||||
.escapeHtml(title)}</strong> #${issueId}</a>"""
|
||||
@@ -43,7 +43,7 @@ trait LinkConverter { self: RequestCache =>
|
||||
escaped
|
||||
// convert username/project@SHA to link
|
||||
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r) { m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
getAccountByUserNameFromCache(m.group(2)).map { _ =>
|
||||
s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(
|
||||
3
|
||||
)}@${m.group(4).substring(0, 7)}</a></code>"""
|
||||
@@ -53,13 +53,15 @@ trait LinkConverter { self: RequestCache =>
|
||||
// convert username/project#Num to link
|
||||
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) {
|
||||
m =>
|
||||
getIssue(m.group(2), m.group(3), m.group(4)) match {
|
||||
case Some(issue) if (issue.isPullRequest) =>
|
||||
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m.group(4)}">${m.group(2)}/${m.group(
|
||||
getIssueFromCache(m.group(2), m.group(3), m.group(4)) match {
|
||||
case Some(pull) if (pull.isPullRequest) =>
|
||||
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/pull/${m
|
||||
.group(4)}" title="${pull.title}">${m.group(2)}/${m.group(
|
||||
3
|
||||
)}#${m.group(4)}</a>""")
|
||||
case Some(_) =>
|
||||
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/issues/${m.group(4)}">${m.group(2)}/${m
|
||||
case Some(issue) =>
|
||||
Some(s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/issues/${m
|
||||
.group(4)}" title="${issue.title}">${m.group(2)}/${m
|
||||
.group(3)}#${m.group(4)}</a>""")
|
||||
case None =>
|
||||
Some(s"""${m.group(2)}/${m.group(3)}#${m.group(4)}""")
|
||||
@@ -68,7 +70,7 @@ trait LinkConverter { self: RequestCache =>
|
||||
|
||||
// convert username@SHA to link
|
||||
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r) { m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
getAccountByUserNameFromCache(m.group(2)).map { _ =>
|
||||
s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m
|
||||
.group(3)
|
||||
.substring(0, 7)}</a></code>"""
|
||||
@@ -77,7 +79,7 @@ trait LinkConverter { self: RequestCache =>
|
||||
|
||||
// convert username#Num to link
|
||||
.replaceBy(("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)" + issueIdPrefix + "([0-9]+)(?=(\\W|$))").r) { m =>
|
||||
getIssue(m.group(2), repository.name, m.group(3)) match {
|
||||
getIssueFromCache(m.group(2), repository.name, m.group(3)) match {
|
||||
case Some(issue) if (issue.isPullRequest) =>
|
||||
Some(s"""<a href="${context.path}/${m.group(2)}/${repository.name}/pull/${m.group(3)}">${m.group(2)}#${m
|
||||
.group(3)}</a>""")
|
||||
@@ -92,12 +94,14 @@ trait LinkConverter { self: RequestCache =>
|
||||
// convert issue id to link
|
||||
.replaceBy(("(?<=(^|\\W))(GH-|(?<!&)" + issueIdPrefix + ")([0-9]+)(?=(\\W|$))").r) { m =>
|
||||
val prefix = if (m.group(2) == "issue:") "#" else m.group(2)
|
||||
getIssue(repository.owner, repository.name, m.group(3)) match {
|
||||
case Some(issue) if (issue.isPullRequest) =>
|
||||
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m.group(3)}">${prefix}${m
|
||||
getIssueFromCache(repository.owner, repository.name, m.group(3)) match {
|
||||
case Some(pull) if (pull.isPullRequest) =>
|
||||
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/pull/${m
|
||||
.group(3)}" title="${pull.title}">${prefix}${m
|
||||
.group(3)}</a>""")
|
||||
case Some(_) =>
|
||||
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m.group(3)}">${prefix}${m
|
||||
case Some(issue) =>
|
||||
Some(s"""<a href="${context.path}/${repository.owner}/${repository.name}/issues/${m
|
||||
.group(3)}" title="${issue.title}">${prefix}${m
|
||||
.group(3)}</a>""")
|
||||
case None =>
|
||||
Some(s"""${m.group(2)}${m.group(3)}""")
|
||||
@@ -106,7 +110,7 @@ trait LinkConverter { self: RequestCache =>
|
||||
|
||||
// convert @username to link
|
||||
.replaceBy("(?<=(^|\\W))@([a-zA-Z0-9\\-_\\.]+)(?=(\\W|$))".r) { m =>
|
||||
getAccountByUserName(m.group(2)).map { _ =>
|
||||
getAccountByUserNameFromCache(m.group(2)).map { _ =>
|
||||
s"""<a href="${context.path}/${m.group(2)}">@${m.group(2)}</a>"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package gitbucket.core.view
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.{Date, Locale, TimeZone}
|
||||
|
||||
import com.nimbusds.jose.util.JSONObjectUtils
|
||||
import gitbucket.core.controller.Context
|
||||
import gitbucket.core.model.CommitState
|
||||
import gitbucket.core.model.PullRequest
|
||||
@@ -187,16 +186,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html =
|
||||
Html(decorateHtml(convertRefsLinks(value, repository), repository))
|
||||
|
||||
def cut(value: String, length: Int): String =
|
||||
if (value.length > length) {
|
||||
value.substring(0, length) + "..."
|
||||
} else {
|
||||
value
|
||||
}
|
||||
|
||||
import scala.util.matching.Regex._
|
||||
implicit class RegexReplaceString(private val s: String) extends AnyVal {
|
||||
def replaceAll(pattern: String, replacer: (Match) => String): String = {
|
||||
def replaceAll(pattern: String)(replacer: Match => String): String = {
|
||||
pattern.r.replaceAllIn(s, (m: Match) => replacer(m).replace("$", "\\$"))
|
||||
}
|
||||
}
|
||||
@@ -204,50 +196,66 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
/**
|
||||
* Convert link notations in the activity message.
|
||||
*/
|
||||
// format: off
|
||||
def activityMessage(message: String)(implicit context: Context): Html =
|
||||
Html(
|
||||
message
|
||||
.replaceAll(
|
||||
"\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]",
|
||||
s"""<a href="${context.path}/$$1/$$2/issues/$$3">$$1/$$2#$$3</a>"""
|
||||
)
|
||||
.replaceAll(
|
||||
"\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]",
|
||||
s"""<a href="${context.path}/$$1/$$2/pull/$$3">$$1/$$2#$$3</a>"""
|
||||
)
|
||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]", s"""<a href="${context.path}/$$1/$$2\">$$1/$$2</a>""")
|
||||
.replaceAll(
|
||||
"\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]",
|
||||
(m: Match) =>
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil
|
||||
.escapeHtml(
|
||||
m.group(3)
|
||||
)}</a>"""
|
||||
)
|
||||
.replaceAll(
|
||||
"\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]",
|
||||
(m: Match) =>
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil
|
||||
.escapeHtml(
|
||||
m.group(3)
|
||||
)}</a>"""
|
||||
)
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]", (m: Match) => user(m.group(1)).body)
|
||||
.replaceAll(
|
||||
"\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]",
|
||||
(m: Match) =>
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m
|
||||
.group(2)}@${m.group(3).substring(0, 7)}</a>"""
|
||||
)
|
||||
.replaceAll(
|
||||
"\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]",
|
||||
(m: Match) =>
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil
|
||||
.escapeHtml(
|
||||
m.group(4)
|
||||
)}</a>"""
|
||||
)
|
||||
.replaceAll("\\[issue:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
|
||||
val issue = getIssueFromCache(m.group(1), m.group(2), m.group(3))
|
||||
if (issue.isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/issues/${m.group(3)}" title="${issue.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
|
||||
} else {
|
||||
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
|
||||
}
|
||||
}
|
||||
.replaceAll("\\[pullreq:([^\\s]+?)/([^\\s]+?)#((\\d+))\\]"){ m =>
|
||||
val pullreq = getIssueFromCache(m.group(1), m.group(2), m.group(3))
|
||||
if (pullreq.isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/pull/${m.group(3)}" title="${pullreq.get.title}">${m.group(1)}/${m.group(2)}#${m.group(3)}</a>"""
|
||||
} else {
|
||||
s"${m.group(1)}/${m.group(2)}#${m.group(3)}"
|
||||
}
|
||||
}
|
||||
.replaceAll("\\[repo:([^\\s]+?)/([^\\s]+?)\\]") { m =>
|
||||
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}">${m.group(1)}/${m.group(2)}</a>"""
|
||||
} else {
|
||||
s"${m.group(1)}/${m.group(2)}"
|
||||
}
|
||||
}
|
||||
.replaceAll("\\[branch:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
|
||||
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>"""
|
||||
} else {
|
||||
StringUtil.escapeHtml(m.group(3))
|
||||
}
|
||||
}
|
||||
.replaceAll("\\[tag:([^\\s]+?)/([^\\s]+?)#([^\\s]+?)\\]") { m =>
|
||||
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/tree/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(3))}</a>"""
|
||||
} else {
|
||||
StringUtil.escapeHtml(m.group(3))
|
||||
}
|
||||
}
|
||||
.replaceAll("\\[user:([^\\s]+?)\\]") { m =>
|
||||
user(m.group(1)).body
|
||||
}
|
||||
.replaceAll("\\[commit:([^\\s]+?)/([^\\s]+?)\\@([^\\s]+?)\\]") { m =>
|
||||
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/commit/${m.group(3)}">${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}</a>"""
|
||||
} else {
|
||||
s"${m.group(1)}/${m.group(2)}@${m.group(3).substring(0, 7)}"
|
||||
}
|
||||
}
|
||||
.replaceAll("\\[release:([^\\s]+?)/([^\\s]+?)/([^\\s]+?):(.+)\\]") { m =>
|
||||
if (getRepositoryInfoFromCache(m.group(1), m.group(2)).isDefined) {
|
||||
s"""<a href="${context.path}/${m.group(1)}/${m.group(2)}/releases/${encodeRefName(m.group(3))}">${StringUtil.escapeHtml(m.group(4))}</a>"""
|
||||
} else {
|
||||
StringUtil.escapeHtml(m.group(4))
|
||||
}
|
||||
}
|
||||
)
|
||||
// format: off
|
||||
|
||||
/**
|
||||
* Remove html tags from the given Html instance.
|
||||
@@ -333,9 +341,9 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
|
||||
content: Html
|
||||
)(implicit context: Context): Html =
|
||||
(if (mailAddress.isEmpty) {
|
||||
getAccountByUserName(userName)
|
||||
getAccountByUserNameFromCache(userName)
|
||||
} else {
|
||||
getAccountByMailAddress(mailAddress)
|
||||
getAccountByMailAddressFromCache(mailAddress)
|
||||
}).map { account =>
|
||||
Html(s"""<a href="${url(account.userName)}" class="${styleClass}">${content}</a>""")
|
||||
} getOrElse content
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<fieldset class="form-group" id="extraMailAddresses">
|
||||
<span class="strong">Additional Mail Address:</span>
|
||||
@extraMailAddresses.zipWithIndex.map { case (mail, idx) =>
|
||||
<input type="text" name="extraMailAddresses[@idx]" id="extraMailAddresses[@idx]" class="form-control extraMailAddress" value="@mail"/>
|
||||
<input type="text" name="extraMailAddresses[@idx]" id="extraMailAddresses[@idx]" class="form-control extraMailAddress" value="@mail" aria-label="Additional mail address"/>
|
||||
<span id="error-extraMailAddresses_@idx" class="error"></span>
|
||||
}
|
||||
</fieldset>
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
}
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label class="strong">URL (Optional)</label>
|
||||
<label class="strong" for="url">URL (Optional)</label>
|
||||
<div>
|
||||
<span id="error-url" class="error"></span>
|
||||
</div>
|
||||
<input type="text" name="url" id="url" class="form-control" value="@account.map(_.url)"/>
|
||||
</fieldset>
|
||||
<fieldset class="form-group">
|
||||
<label for="groupDescription" class="strong">Description (Optional)</label>
|
||||
<label for="description" class="strong">Description (Optional)</label>
|
||||
<div>
|
||||
<textarea name="description" id="description" class="form-control">@account.map(_.description)</textarea>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ $(function(){
|
||||
.append(' ')
|
||||
.append($('<a>').attr('href', '@context.path/' + userName).text(userName))
|
||||
.append(' ')
|
||||
.append($('<a href="#" class="remove pull-right"><span class="octicon octicon-x"></span></a>')));
|
||||
.append($('<a href="#" class="remove pull-right"><span class="octicon octicon-x" aria-label="Remove"></span></a>')));
|
||||
}
|
||||
|
||||
function updateMembers(){
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
Webhooks allow external services to be notified when certain events happen within your repository.
|
||||
When the specified events happen, we’ll send a POST request to each of the URLs you provide.
|
||||
Learn more in <a href="https://github.com/takezoe/gitbucket/wiki/API-WebHook" target="_blank">GitBucket Wiki Webhook Page</a>.
|
||||
These webhooks notify external services when certain events occur within any of your repositories.
|
||||
When any of the specified events occur, GitBucket will send a POST request to all of the endpoints (URLs) you provide.
|
||||
Learn more about this feature on the <a href="https://github.com/gitbucket/gitbucket/wiki/API-WebHook" target="_blank" rel="noopener">GitBucket Wiki</a>.
|
||||
</p>
|
||||
<a href="@helpers.url(account.userName)/_hooks/new" class="btn btn-success pull-right" style="margin-bottom: 10px;">Add webhook</a>
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
</td><td>
|
||||
<div class="btn-group pull-right">
|
||||
<a href="@helpers.url(account.userName)/_hooks/edit?url=@helpers.urlEncode(webHook.url)" class="btn btn-default">
|
||||
<span class="octicon octicon-pencil"></span>
|
||||
<span class="octicon octicon-pencil" aria-label="Edit hook"></span>
|
||||
</a>
|
||||
<a href="@helpers.url(account.userName)/_hooks/delete?url=@helpers.urlEncode(webHook.url)" class="btn btn-danger" onclick="return confirm('delete webhook for @webHook.url ?')">
|
||||
<span class="octicon octicon-x"></span>
|
||||
<span class="octicon octicon-x" aria-label="Remove hook"></span>
|
||||
</a>
|
||||
</div>
|
||||
</td></tr>
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
<i class="menu-icon octicon octicon-zap"></i> <span>Service Hooks</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu-item-hover @if(active=="preferences"){active}">
|
||||
<a href="@context.path/@userName/_preferences">
|
||||
<i class="menu-icon octicon octicon-star"></i> <span>Preferences</span>
|
||||
</a>
|
||||
</li>
|
||||
@gitbucket.core.plugin.PluginRegistry().getAccountSettingMenus.map { menu =>
|
||||
@menu(context).map { link =>
|
||||
<li class="menu-item-hover @if(active==link.id){active}">
|
||||
|
||||
@@ -6,7 +6,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<div class="content body">
|
||||
<h2>Create a new repository</h2>
|
||||
<p class="muted">
|
||||
A repository contains all the files for your project including the revision history.
|
||||
A repository contains all your project's files, including revision history.
|
||||
</p>
|
||||
<form id="form" method="post" action="@context.path/new" validate="true" autocomplete="off">
|
||||
<fieldset class="border-top form-group">
|
||||
@@ -32,7 +32,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<dl>
|
||||
<dt>Repository name</dt>
|
||||
<dd style="margin-left: 0px;">
|
||||
<input type="text" name="name" id="name" class="form-control" style="width: 300px; display: inline;" autofocus/>
|
||||
<input type="text" name="name" id="name" class="form-control" style="width: 300px; display: inline;" autofocus aria-label="Repository name"/>
|
||||
<span id="error-name" class="error"></span>
|
||||
</dd>
|
||||
</dl>
|
||||
@@ -62,31 +62,31 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
||||
<input type="radio" name="initOption" value="EMPTY" checked/>
|
||||
<span class="strong">Create an empty repository</span>
|
||||
<div class="normal muted">
|
||||
Create an empty repository. You have to initialize by yourself initially.
|
||||
Create an empty repository. You must initialize it yourself.
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="EMPTY_COMMIT"/>
|
||||
<span class="strong">Initialize this repository with an empty commit</span>
|
||||
<div class="normal muted">
|
||||
Create an empty repository with empty commit. You can clone the repository immediately.
|
||||
Create an empty repository with an empty commit. You can clone the repository immediately.
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="README"/>
|
||||
<span class="strong">Initialize this repository with a README</span>
|
||||
<div class="normal muted">
|
||||
Create a repository which has README.md. You can clone the repository immediately.
|
||||
Create a repository and commit README.md. You can clone the repository immediately.
|
||||
</div>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="initOption" value="COPY"/>
|
||||
<span class="strong">Copy existing git repository</span>
|
||||
<div class="normal muted">
|
||||
Create new repository from existing git repository.
|
||||
Create a new repository by cloning an existing git repository.
|
||||
</div>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" disabled placeholder="Source git repository URL..."/>
|
||||
<input type="text" class="form-control" name="sourceUrl" id="sourceUrl" disabled placeholder="Source git repository URL..." aria-label="Source URL"/>
|
||||
<span id="error-sourceUrl" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset class="border-top form-actions">
|
||||
|
||||
82
src/main/twirl/gitbucket/core/account/preferences.scala.html
Normal file
82
src/main/twirl/gitbucket/core/account/preferences.scala.html
Normal file
@@ -0,0 +1,82 @@
|
||||
@(account: gitbucket.core.model.Account, currentTheme: String)(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Preferences", highlighterTheme = currentTheme){
|
||||
@gitbucket.core.account.html.menu("preferences", context.loginAccount.get.userName, false){
|
||||
<form method="POST" action="@context.path/@account.userName/_preferences/highlighter" validate="true" autocomplete="off">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Syntax Highlighter Theme</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<fieldset class="form-group">
|
||||
<label for="highlighterTheme" class="strong">Theme</label>
|
||||
<select id="highlighterTheme" name="highlighterTheme" class="form-control">
|
||||
@Seq(
|
||||
("atelier-cave-dark", "Atelier Cave Dark"),
|
||||
("atelier-cave-light", "Atelier Cave Light"),
|
||||
("atelier-dune-dark", "Atelier Dune Dark"),
|
||||
("atelier-dune-light", "Atelier Dune Light"),
|
||||
("atelier-estuary-dark", "Atelier Estuary Dark"),
|
||||
("atelier-estuary-light", "Atelier Estuary Light"),
|
||||
("atelier-forest-dark", "Atelier Forest Dark"),
|
||||
("atelier-forest-light", "Atelier Forest Light"),
|
||||
("atelier-heath-dark", "Atelier Heath Dark"),
|
||||
("atelier-heath-light", "Atelier Heath Light"),
|
||||
("atelier-lakeside-dark", "Atelier Lakeside Dark"),
|
||||
("atelier-lakeside-light", "Atelier Lakeside Light"),
|
||||
("atelier-plateau-dark", "Atelier Plateau Dark"),
|
||||
("atelier-plateau-light", "Atelier Plateau Light"),
|
||||
("atelier-savanna-dark", "Atelier Savanna Dark"),
|
||||
("atelier-savanna-light", "Atelier Savanna Light"),
|
||||
("atelier-seaside-dark", "Atelier Seaside Dark"),
|
||||
("atelier-seaside-light", "Atelier Seaside Light"),
|
||||
("atelier-sulphurpool-dark", "Atelier Sulphurpool Dark"),
|
||||
("atelier-sulphurpool-light", "Atelier Sulphurpool Light"),
|
||||
("github", "GitHub"),
|
||||
("github-v2", "GitHub v2"),
|
||||
("hemisu-dark", "Hemisu Dark"),
|
||||
("hemisu-light", "Hemisu Light"),
|
||||
("tomorrow", "Tomorrow"),
|
||||
("tomorrow-night", "Tomorrow Night"),
|
||||
("tomorrow-night-blue", "Tomorrow Night Blue"),
|
||||
("tomorrow-night-bright", "Tomorrow Night Bright"),
|
||||
("tomorrow-night-eighties", "Tomorrow Night Eighties"),
|
||||
("vibrant-ink", "Vibrant Ink")
|
||||
).map{ theme =>
|
||||
<option value="@theme._1"@if(theme._1 == currentTheme){ selected=""}>@theme._2</option>
|
||||
}
|
||||
</select>
|
||||
</fieldset>
|
||||
<input type="submit" class="btn btn-success" value="Save"/>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div style="margin: 5px">
|
||||
<pre class="prettyprint linenums">@{
|
||||
"""#include <iostream>
|
||||
|
|
||||
|using namespace std;
|
||||
|
|
||||
|int main(){
|
||||
| cout << 'Hello world.' << endl;
|
||||
| return 0;
|
||||
|}""".stripMargin}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#highlighterTheme').change(function(evt) {
|
||||
var that = $(evt.target);
|
||||
var themeCss = $('link[rel="stylesheet"][href*="color-themes-for-google-code-prettify"]');
|
||||
var oldVal = new RegExp('color-themes-for-google-code-prettify/(.*?).min.css').exec(themeCss.attr('href'))[1];
|
||||
themeCss.attr('href', themeCss.attr('href').replace(oldVal, that.val()));
|
||||
$(document.body).removeClass(oldVal).addClass(that.val());
|
||||
});
|
||||
prettyPrint();
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,44 @@
|
||||
@(tableNames: Seq[String])(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Data export / import"){
|
||||
@gitbucket.core.admin.html.menu("data") {
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Export</div>
|
||||
<div class="panel-body">
|
||||
<form class="form form-horizontal" action="@context.path/admin/export" method="POST">
|
||||
@tableNames.map { tableName =>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tableNames" id="@tableName" value="@tableName" checked/> @tableName
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<input type="submit" class="btn btn-success pull-right" value="Export">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Import</div>
|
||||
<div class="panel-body">
|
||||
<form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form">
|
||||
<input type="file" name="file" id="file">
|
||||
<input type="submit" class="btn btn-success pull-right" value="Import" id="import">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#import-form').submit(function(){
|
||||
if($('#file').val() == ''){
|
||||
alert('Choose an import SQL file.');
|
||||
return false;
|
||||
} else if(!$('#file').val().endsWith(".sql")){
|
||||
alert('Import is available for only the SQL file.');
|
||||
return false;
|
||||
}
|
||||
return confirm('All existing data is deleted before importing.\nAre you sure?');
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@(tableNames: Seq[String])(implicit context: gitbucket.core.controller.Context)
|
||||
@gitbucket.core.html.main("Data export / import"){
|
||||
@gitbucket.core.admin.html.menu("data") {
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Export</div>
|
||||
<div class="panel-body">
|
||||
<form class="form form-horizontal" action="@context.path/admin/export" method="POST">
|
||||
@tableNames.map { tableName =>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tableNames" id="@tableName" value="@tableName" checked/> @tableName
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
<input type="submit" class="btn btn-success pull-right" value="Export">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading strong">Import</div>
|
||||
<div class="panel-body">
|
||||
<form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form">
|
||||
<input type="file" name="file" id="file" aria-label="Upload file">
|
||||
<input type="submit" class="btn btn-success pull-right" value="Import" id="import">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#import-form').submit(function(){
|
||||
if($('#file').val() == ''){
|
||||
alert('Choose an import SQL file.');
|
||||
return false;
|
||||
} else if(!$('#file').val().endsWith(".sql")){
|
||||
alert('Import is available for only the SQL file.');
|
||||
return false;
|
||||
}
|
||||
return confirm('All existing data is deleted before importing.\nAre you sure?');
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
@gitbucket.core.admin.html.menu("system"){
|
||||
@gitbucket.core.helper.html.information(info)
|
||||
<form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal" autocomplete="off">
|
||||
<ul class="nav nav-tabs fill-width" id="pullreq-tab">
|
||||
<ul class="nav nav-tabs fill-width" id="settings-tab">
|
||||
<li><a href="#system">System settings</a></li>
|
||||
<li><a href="#integrations">Integrations</a></li>
|
||||
<li><a href="#authentication">Authentication</a></li>
|
||||
</ul>
|
||||
<div class="tab-content fill-width" style="padding-top: 20px;">
|
||||
<div class="tab-pane" id="system">
|
||||
<div class="tab-pane" id="tab-system">
|
||||
@settings_system(info)
|
||||
</div>
|
||||
<div class="tab-pane" id="integrations">
|
||||
<div class="tab-pane" id="tab-integrations">
|
||||
@settings_integrations(info)
|
||||
</div>
|
||||
<div class="tab-pane" id="authentication">
|
||||
<div class="tab-pane" id="tab-authentication">
|
||||
@settings_authentication(info)
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,25 +30,29 @@
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
// Determine active tab from hash
|
||||
if(location.hash == '#authentication'){
|
||||
$('li:has(a[href="#authentication"])').addClass('active');
|
||||
$('div#authentication').addClass('active');
|
||||
} else if(location.hash == '#integrations'){
|
||||
$('li:has(a[href="#integrations"])').addClass('active');
|
||||
$('div#integrations').addClass('active');
|
||||
} else {
|
||||
$('li:has(a[href="#system"])').addClass('active');
|
||||
$('div#system').addClass('active');
|
||||
function updateTabs(){
|
||||
$('ul.nav-tabs li').removeClass('active');
|
||||
$('div.tab-pane').removeClass('active');
|
||||
|
||||
// Determine active tab from hash
|
||||
if(location.hash == '#authentication'){
|
||||
$('li:has(a[href="#authentication"])').addClass('active');
|
||||
$('div#tab-authentication').addClass('active');
|
||||
} else if(location.hash == '#integrations'){
|
||||
$('li:has(a[href="#integrations"])').addClass('active');
|
||||
$('div#tab-integrations').addClass('active');
|
||||
} else {
|
||||
$('li:has(a[href="#system"])').addClass('active');
|
||||
$('div#tab-system').addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Set hash when tab is clicked
|
||||
$('ul.nav-tabs li a').click(function(e){
|
||||
$('#settings-tab li a').click(function(e){
|
||||
location.href = $(e.delegateTarget).attr("href");
|
||||
updateTabs();
|
||||
});
|
||||
|
||||
$('#pullreq-tab a').click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).tab('show');
|
||||
});
|
||||
updateTabs();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<td class="danger">
|
||||
<p>@gitbucket.core.util.DatabaseConfig.url</p>
|
||||
<p>
|
||||
Your GitBucket is running on embedded H2 database.
|
||||
Recommend to <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure to use external database</a> if you would like to use GitBucket for important purpose.
|
||||
GitBucket is using the embedded H2 database.
|
||||
It's recommended that you <a href="https://github.com/gitbucket/gitbucket/wiki/External-database-configuration">configure GitBucket to use an external database</a> if you're running GitBucket in a production environment.
|
||||
</p>
|
||||
</td>
|
||||
}else{
|
||||
@@ -35,7 +35,7 @@
|
||||
<!-- Base URL -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
|
||||
<label for="baseUrl"><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
|
||||
<fieldset>
|
||||
<div class="controls">
|
||||
<input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@context.settings.baseUrl"/>
|
||||
@@ -43,17 +43,17 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
<p class="muted">
|
||||
The base URL is used for redirect, notification email, git repository URL box and more.
|
||||
If the base URL is empty, GitBucket generates URL from the request information.
|
||||
You can use this property to adjust to URL differences between the reverse proxy and GitBucket.
|
||||
The base URL is used for redirects, notification emails, git repository URL boxes, and more.
|
||||
If the base URL is empty, GitBucket generates the URL from the request information.
|
||||
You can use this property to adjust to URL differences between a reverse proxy and GitBucket.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Information -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Information</span> (HTML is available)</label>
|
||||
<label for="information"><span class="strong">Site notification</span> (Supports HTML)</label>
|
||||
<fieldset>
|
||||
<textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
|
||||
<textarea id="information" name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- AdminLTE SkinName -->
|
||||
@@ -97,9 +97,9 @@
|
||||
<!-- User-defined CSS -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">User-defined CSS</span></label>
|
||||
<label for="userDefinedCss"><span class="strong">User-defined CSS</span></label>
|
||||
<fieldset>
|
||||
<textarea name="userDefinedCss" class="form-control" style="height: 100px;">@context.settings.userDefinedCss</textarea>
|
||||
<textarea id="userDefinedCss" name="userDefinedCss" class="form-control" style="height: 100px;">@context.settings.userDefinedCss</textarea>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
@@ -266,34 +266,39 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Activity -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Limit of activity logs</span> (Unlimited if it is not specified or zero)</label>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="activityLogLimit">Limit</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@context.settings.activityLogLimit"/>
|
||||
<span id="error-activityLogLimit" class="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Sidebar -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Show Repositories in Sidebar</span></label>
|
||||
<label><span class="strong">Show repositories in sidebar</span></label>
|
||||
<fieldset>
|
||||
<label class="radio">
|
||||
<input type="radio" name="limitVisibleRepositories" value="false"@if(!context.settings.limitVisibleRepositories){ checked}>
|
||||
<span class="strong">All</span> <span class="normal">- Show all repositories in sidebar.</span>
|
||||
<span class="strong">All</span> <span class="normal">- Show all visible repositories in sidebar.</span>
|
||||
</label>
|
||||
<label class="radio">
|
||||
<input type="radio" name="limitVisibleRepositories" value="true"@if(context.settings.limitVisibleRepositories){ checked}>
|
||||
<span class="strong">Limited</span> <span class="normal">- Limit the visible repositories in sidebar.</span>
|
||||
<span class="strong">Limited</span> <span class="normal">- Show only owned repositories in sidebar.</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Repository viewer -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Repository viewer</span></label>
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="repositoryViewerMaxFiles">Max files in directory</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="repositoryViewer.maxFiles" id="repositoryViewerMaxFiles" class="form-control" value="@context.settings.repositoryViewer.maxFiles"/>
|
||||
<span id="error-repositoryViewerMaxFiles" class="error"></span>
|
||||
<p class="muted">
|
||||
If the number of files in the directory is bigger than this number, GitBucket doesn't show detailed commit info on the repository viewer
|
||||
because it can make the entire system quite heavy. 0 or negative number means no limitation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#skinName').change(function(evt) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user