mirror of
https://github.com/gitbucket/gitbucket.git
synced 2026-05-08 23:16:48 +02:00
Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b6a1d0adc | ||
|
|
6db43e6ca7 | ||
|
|
46177e814c | ||
|
|
2fe79baed8 | ||
|
|
02e17c76a7 | ||
|
|
4d8acfd286 | ||
|
|
4a5c287b8f | ||
|
|
0d4047b4ee | ||
|
|
2b730ef180 | ||
|
|
ecbe7228b9 | ||
|
|
e36d0f65d6 | ||
|
|
30818fb797 | ||
|
|
6d7685fcce | ||
|
|
2cec5be3d9 | ||
|
|
038b70ff0b | ||
|
|
23a5f7dcf9 | ||
|
|
baa27d6090 | ||
|
|
f71acfcbe8 | ||
|
|
c65e843491 | ||
|
|
9103c88f0e | ||
|
|
90170f0fcc | ||
|
|
2e3de336cb | ||
|
|
da50d317e7 | ||
|
|
7a91e14b03 | ||
|
|
58eff0a1a3 | ||
|
|
8559f3e354 | ||
|
|
7606097a4d | ||
|
|
dfbace3d26 | ||
|
|
d61ab632f1 | ||
|
|
c3b341d945 | ||
|
|
59f063627c | ||
|
|
b4aba76005 | ||
|
|
81afea350d | ||
|
|
5c5da60dd6 | ||
|
|
89c69cdfc2 | ||
|
|
0789010248 | ||
|
|
37c23f615f | ||
|
|
b4d3573a84 | ||
|
|
5161ece63b | ||
|
|
5b7955cee6 | ||
|
|
60099e2b0d | ||
|
|
f04c486251 | ||
|
|
7b23bbf9ba | ||
|
|
0a532d9774 | ||
|
|
208b98285c | ||
|
|
56c9aa32a4 | ||
|
|
35080a9f33 | ||
|
|
6c41505c91 | ||
|
|
05bfaafe32 | ||
|
|
7534a88607 | ||
|
|
d7817d3d88 | ||
|
|
37780d467d | ||
|
|
ad4af67b30 | ||
|
|
29b0c22b0e | ||
|
|
c400678550 | ||
|
|
3c40e93346 | ||
|
|
247b664654 | ||
|
|
b3db0a6a7b | ||
|
|
0a03e41f1c | ||
|
|
a79f105eea | ||
|
|
74f3f6bf2e | ||
|
|
a4bf73724d | ||
|
|
8d91253ede | ||
|
|
b7355af49a | ||
|
|
7ec85cbf99 | ||
|
|
a6790b049d | ||
|
|
dce747b1e8 | ||
|
|
c22ee8acfd | ||
|
|
30b8b738b6 | ||
|
|
b916595da3 | ||
|
|
1accafa8b1 | ||
|
|
11700f4cb4 | ||
|
|
c9de8dd323 | ||
|
|
fd694e38aa | ||
|
|
8822c36b5f | ||
|
|
e614e31162 | ||
|
|
ad47ad4269 | ||
|
|
90b63090cc | ||
|
|
345685ed7c | ||
|
|
1d9fe5770e | ||
|
|
0c50545cbd | ||
|
|
53cbc36a01 | ||
|
|
85b2053004 | ||
|
|
eba240de65 | ||
|
|
1e5114cd54 | ||
|
|
90cb5de5f0 | ||
|
|
11d33e9389 | ||
|
|
c71e9331ae | ||
|
|
ec307b84d3 | ||
|
|
f37b5fa682 | ||
|
|
8cb1ac734d | ||
|
|
05ff2a854c | ||
|
|
d956ade5e3 | ||
|
|
73228506a5 | ||
|
|
2525bbafa8 | ||
|
|
338946dd3a | ||
|
|
2d225641ee | ||
|
|
3c727fe678 | ||
|
|
523ea0d437 | ||
|
|
9eff8f248b | ||
|
|
d50c858a26 | ||
|
|
6f4e94ba9a | ||
|
|
f2750c20a2 | ||
|
|
2da2b426a1 | ||
|
|
5a0bc127b7 | ||
|
|
23a482bbba | ||
|
|
6c2fce1b16 | ||
|
|
5d7346db91 | ||
|
|
443498433d | ||
|
|
a58ce07736 | ||
|
|
1903c3990c | ||
|
|
90441c8eec | ||
|
|
601919bcc6 | ||
|
|
6903b096f5 | ||
|
|
e44fed09fa | ||
|
|
8ed0c8a170 | ||
|
|
31118ac285 | ||
|
|
c851b7582f | ||
|
|
102a02d527 | ||
|
|
1968bf871a | ||
|
|
5cd4e48173 | ||
|
|
111d212cb5 | ||
|
|
d648d34393 | ||
|
|
bbaa5b38e7 | ||
|
|
3c50a78be2 | ||
|
|
82cc1fa530 | ||
|
|
8c0581973e | ||
|
|
bfa15f5d75 | ||
|
|
57e87b581d | ||
|
|
5aa6f5bce3 | ||
|
|
9ba098a805 | ||
|
|
b2d8567c26 | ||
|
|
19d97c93ce | ||
|
|
cad2daa2f9 | ||
|
|
585f0b5769 | ||
|
|
dd23d1109b | ||
|
|
5e84221d39 | ||
|
|
faf3e6c26b | ||
|
|
969da2c63b | ||
|
|
ba61891510 | ||
|
|
a581871a89 | ||
|
|
d96e1fa503 | ||
|
|
b66812d76c | ||
|
|
ae32016856 | ||
|
|
56aec15e68 | ||
|
|
d0c8e33ec5 | ||
|
|
7ab260e688 | ||
|
|
0d2c923664 | ||
|
|
e7a47fe3a4 | ||
|
|
e454f78c5a | ||
|
|
192e4ade3e | ||
|
|
733797cb6f | ||
|
|
f0e4157a46 | ||
|
|
2ad6948bb4 | ||
|
|
dd46d649a6 | ||
|
|
bbe8a9b9e4 | ||
|
|
376b109602 | ||
|
|
1d085d52bb | ||
|
|
d1f42e0ed7 | ||
|
|
101f8598ed | ||
|
|
da62f6f8fb | ||
|
|
5750286b5d | ||
|
|
f2ca4fb64b | ||
|
|
5f6b577cbf | ||
|
|
62004c279c | ||
|
|
838c7fb991 | ||
|
|
93786f0fd6 | ||
|
|
f3514e5625 | ||
|
|
a2b0ee0c24 | ||
|
|
2bcab30529 | ||
|
|
91cda6d245 | ||
|
|
a82e579d57 | ||
|
|
94421c7a63 | ||
|
|
ea7c8e62de | ||
|
|
b84421723b | ||
|
|
9a42b93d1f | ||
|
|
e162cd956a | ||
|
|
6431d25409 | ||
|
|
c8686f4b34 | ||
|
|
43097b4c1c | ||
|
|
539751a1d9 | ||
|
|
70e2079c7f | ||
|
|
e8737d263a | ||
|
|
4c4b08f1b8 | ||
|
|
c7e1edf262 | ||
|
|
876bb396fd | ||
|
|
a6788f858f | ||
|
|
b263764730 | ||
|
|
1b1bd371a4 | ||
|
|
f194a08cfe | ||
|
|
1211bfc7be | ||
|
|
eab7011e0f | ||
|
|
6f30ffa865 | ||
|
|
de3026248c | ||
|
|
413e75be5a | ||
|
|
6a8ec18f9a | ||
|
|
5b1b2ef3d7 | ||
|
|
9a705c62bf | ||
|
|
b103180bf6 | ||
|
|
536a0d3fe2 | ||
|
|
356202e28a | ||
|
|
6db36e12b5 | ||
|
|
bfcd5a2855 | ||
|
|
e218b52b78 | ||
|
|
46998dc1fa | ||
|
|
ff3205b6c7 | ||
|
|
40e36e3f8b | ||
|
|
3d1c9bc9de | ||
|
|
5a5bf34fe0 | ||
|
|
af7043f4bf | ||
|
|
249b27593e | ||
|
|
1201271949 | ||
|
|
19e74a4fe1 | ||
|
|
130aa1e515 | ||
|
|
e92d1eae5a | ||
|
|
8b44a00299 | ||
|
|
f5c1c0703d | ||
|
|
5036b1a1aa |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
### Before submitting an issue to Gitbucket I have first:
|
### Before submitting an issue to Gitbucket I have first:
|
||||||
|
|
||||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
|
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||||
- [] searched for similar already existing issue
|
- [] searched for similar already existing issue
|
||||||
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
|
||||||
|
|
||||||
|
|||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
### Before submitting a pull-request to Gitbucket I have first:
|
### Before submitting a pull-request to Gitbucket I have first:
|
||||||
|
|
||||||
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
|
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
|
||||||
- [] rebased my branch over master
|
- [] rebased my branch over master
|
||||||
- [] verified that project is compiling
|
- [] verified that project is compiling
|
||||||
- [] verified that tests are passing
|
- [] verified that tests are passing
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
language: scala
|
language: scala
|
||||||
sudo: false
|
sudo: true
|
||||||
script:
|
script:
|
||||||
- sbt test
|
- sbt test
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- oraclejdk8
|
||||||
|
before_script:
|
||||||
|
- sudo apt-get install libaio1
|
||||||
|
- sudo /etc/init.d/mysql stop
|
||||||
|
- sudo /etc/init.d/postgresql stop
|
||||||
|
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -1,7 +1,10 @@
|
|||||||
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
|
GitBucket [](https://gitter.im/gitbucket/gitbucket) [](https://travis-ci.org/gitbucket/gitbucket)
|
||||||
=========
|
=========
|
||||||
|
|
||||||
GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
|
GitBucket is a Git platform powered by Scala offering:
|
||||||
|
- easy installation
|
||||||
|
- high extensibility by plugins
|
||||||
|
- API compatibility with Github
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
@@ -56,10 +59,36 @@ Support
|
|||||||
- Make sure check whether there is a same question or request in the past.
|
- Make sure check whether there is a same question or request in the past.
|
||||||
- When raise a new issue, write subject in **English** at least.
|
- When raise a new issue, write subject in **English** at least.
|
||||||
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
|
||||||
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
|
- First priority of GitBucket is easy installation and API compatibility with GitHub, so we might reject if your request is against it.
|
||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
--------
|
-------------
|
||||||
|
### 4.1 - 4 Jun 2016
|
||||||
|
|
||||||
|
- Generic ssh user
|
||||||
|
- Improve branch protection UI
|
||||||
|
- Default value of pull request title
|
||||||
|
|
||||||
|
### 4.0 - 30 Apr 2016
|
||||||
|
|
||||||
|
- MySQL and PostgreSQL support
|
||||||
|
- Data export and import
|
||||||
|
- Migration system has been switched to [solidbase](https://github.com/gitbucket/solidbase)
|
||||||
|
|
||||||
|
**Note:** You can upgrade to GitBucket 4.0 from 3.14. If your GitBucket is 3.13 or before, you have to upgrade 3.14 at first.
|
||||||
|
|
||||||
|
### 3.14 - 30 Apr 2016
|
||||||
|
|
||||||
|
- File attachment and search for wiki pages
|
||||||
|
- New extension points to add menus
|
||||||
|
- Content-Type of webhooks has been choosable
|
||||||
|
|
||||||
|
### 3.13 - 1 Apr 2016
|
||||||
|
- Refresh user interface for wide screen
|
||||||
|
- Add `pull_request` key in list issues API for pull requests
|
||||||
|
- Add `X-Hub-Signature` security to webhooks
|
||||||
|
- Provide SHA-256 checksum for `gitbucket.war`
|
||||||
|
|
||||||
### 3.12 - 27 Feb 2016
|
### 3.12 - 27 Feb 2016
|
||||||
- New GitHub UI
|
- New GitHub UI
|
||||||
- Improve mobile view
|
- Improve mobile view
|
||||||
|
|||||||
98
build.sbt
98
build.sbt
@@ -1,6 +1,6 @@
|
|||||||
val Organization = "gitbucket"
|
val Organization = "gitbucket"
|
||||||
val Name = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val GitBucketVersion = "3.12.0"
|
val GitBucketVersion = "4.1.0"
|
||||||
val ScalatraVersion = "2.4.0"
|
val ScalatraVersion = "2.4.0"
|
||||||
val JettyVersion = "9.3.6.v20151106"
|
val JettyVersion = "9.3.6.v20151106"
|
||||||
|
|
||||||
@@ -10,15 +10,17 @@ sourcesInBase := false
|
|||||||
organization := Organization
|
organization := Organization
|
||||||
name := Name
|
name := Name
|
||||||
version := GitBucketVersion
|
version := GitBucketVersion
|
||||||
scalaVersion := "2.11.7"
|
scalaVersion := "2.11.8"
|
||||||
|
|
||||||
// dependency settings
|
// dependency settings
|
||||||
resolvers ++= Seq(
|
resolvers ++= Seq(
|
||||||
Classpaths.typesafeReleases,
|
Classpaths.typesafeReleases,
|
||||||
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
|
"amateras" at "http://amateras.sourceforge.jp/mvn/",
|
||||||
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/",
|
||||||
|
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
|
||||||
)
|
)
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
|
"org.scala-lang.modules" %% "scala-java8-compat" % "0.7.0",
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
|
||||||
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
|
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
|
||||||
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
"org.scalatra" %% "scalatra" % ScalatraVersion,
|
||||||
@@ -26,7 +28,8 @@ libraryDependencies ++= Seq(
|
|||||||
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
"org.json4s" %% "json4s-jackson" % "3.3.0",
|
||||||
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
|
||||||
"commons-io" % "commons-io" % "2.4",
|
"commons-io" % "commons-io" % "2.4",
|
||||||
"io.github.gitbucket" % "markedj" % "1.0.6",
|
"io.github.gitbucket" % "solidbase" % "1.0.0",
|
||||||
|
"io.github.gitbucket" % "markedj" % "1.0.9-SNAPSHOT",
|
||||||
"org.apache.commons" % "commons-compress" % "1.10",
|
"org.apache.commons" % "commons-compress" % "1.10",
|
||||||
"org.apache.commons" % "commons-email" % "1.4",
|
"org.apache.commons" % "commons-email" % "1.4",
|
||||||
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
|
||||||
@@ -35,29 +38,38 @@ libraryDependencies ++= Seq(
|
|||||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||||
"com.h2database" % "h2" % "1.4.190",
|
"com.h2database" % "h2" % "1.4.190",
|
||||||
|
"mysql" % "mysql-connector-java" % "5.1.38",
|
||||||
|
"org.postgresql" % "postgresql" % "9.4.1208",
|
||||||
"ch.qos.logback" % "logback-classic" % "1.1.1",
|
"ch.qos.logback" % "logback-classic" % "1.1.1",
|
||||||
"com.mchange" % "c3p0" % "0.9.5.2",
|
"com.zaxxer" % "HikariCP" % "2.4.5",
|
||||||
"com.typesafe" % "config" % "1.3.0",
|
"com.typesafe" % "config" % "1.3.0",
|
||||||
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
|
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
|
||||||
|
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0",
|
||||||
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
|
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
|
||||||
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
|
||||||
"junit" % "junit" % "4.12" % "test",
|
"junit" % "junit" % "4.12" % "test",
|
||||||
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
|
||||||
"org.scalaz" %% "scalaz-core" % "7.2.0" % "test"
|
"org.scalaz" %% "scalaz-core" % "7.2.0" % "test",
|
||||||
|
"com.wix" % "wix-embedded-mysql" % "1.0.3" % "test",
|
||||||
|
"ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Twirl settings
|
// Twirl settings
|
||||||
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
|
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
|
||||||
|
|
||||||
// Compiler settings
|
// Compiler settings
|
||||||
scalacOptions := Seq("-deprecation", "-language:postfixOps")
|
scalacOptions := Seq("-deprecation", "-language:postfixOps", "-Ybackend:GenBCode", "-Ydelambdafy:method", "-target:jvm-1.8")
|
||||||
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
|
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
|
||||||
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
|
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
|
||||||
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
|
|
||||||
|
// Test settings
|
||||||
|
//testOptions in Test += Tests.Argument("-l", "ExternalDBTest")
|
||||||
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
|
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
|
||||||
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() )
|
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() )
|
||||||
fork in Test := true
|
fork in Test := true
|
||||||
|
|
||||||
|
// Packaging options
|
||||||
packageOptions += Package.MainClass("JettyLauncher")
|
packageOptions += Package.MainClass("JettyLauncher")
|
||||||
|
|
||||||
// Assembly settings
|
// Assembly settings
|
||||||
@@ -72,46 +84,47 @@ assemblyMergeStrategy in assembly := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// JRebel
|
// JRebel
|
||||||
|
Seq(jrebelSettings: _*)
|
||||||
|
|
||||||
jrebel.webLinks += (target in webappPrepare).value
|
jrebel.webLinks += (target in webappPrepare).value
|
||||||
jrebel.enabled := System.getenv().get("JREBEL") != null
|
jrebel.enabled := System.getenv().get("JREBEL") != null
|
||||||
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
|
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
|
||||||
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
|
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
|
||||||
}
|
}
|
||||||
jrebelSettings
|
|
||||||
|
|
||||||
// Create executable war file
|
// Create executable war file
|
||||||
val executableConfig = config("executable").hide
|
val executableConfig = config("executable").hide
|
||||||
Keys.ivyConfigurations += executableConfig
|
Keys.ivyConfigurations += executableConfig
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
|
||||||
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
|
||||||
)
|
)
|
||||||
|
|
||||||
val executableKey = TaskKey[File]("executable")
|
val executableKey = TaskKey[File]("executable")
|
||||||
executableKey := {
|
executableKey := {
|
||||||
import org.apache.ivy.util.ChecksumHelper
|
import org.apache.ivy.util.ChecksumHelper
|
||||||
import java.util.jar.{ Manifest => JarManifest }
|
import java.util.jar.{ Manifest => JarManifest }
|
||||||
import java.util.jar.Attributes.{ Name => AttrName }
|
import java.util.jar.Attributes.{ Name => AttrName }
|
||||||
|
|
||||||
val workDir = Keys.target.value / "executable"
|
val workDir = Keys.target.value / "executable"
|
||||||
val warName = Keys.name.value + ".war"
|
val warName = Keys.name.value + ".war"
|
||||||
|
|
||||||
val log = streams.value.log
|
val log = streams.value.log
|
||||||
log info s"building executable webapp in ${workDir}"
|
log info s"building executable webapp in ${workDir}"
|
||||||
|
|
||||||
// initialize temp directory
|
// initialize temp directory
|
||||||
val temp = workDir / "webapp"
|
val temp = workDir / "webapp"
|
||||||
IO delete temp
|
IO delete temp
|
||||||
|
|
||||||
// include jetty classes
|
// include jetty classes
|
||||||
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
|
||||||
jettyJars foreach { jar =>
|
jettyJars foreach { jar =>
|
||||||
IO unzip (jar, temp, (name:String) =>
|
IO unzip (jar, temp, (name:String) =>
|
||||||
(name startsWith "javax/") ||
|
(name startsWith "javax/") ||
|
||||||
@@ -120,31 +133,34 @@ executableKey := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// include original war file
|
// include original war file
|
||||||
val warFile = (Keys.`package`).value
|
val warFile = (Keys.`package`).value
|
||||||
IO unzip (warFile, temp)
|
IO unzip (warFile, temp)
|
||||||
|
|
||||||
// include launcher classes
|
// include launcher classes
|
||||||
val classDir = (Keys.classDirectory in Compile).value
|
val classDir = (Keys.classDirectory in Compile).value
|
||||||
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
|
||||||
launchClasses foreach { name =>
|
launchClasses foreach { name =>
|
||||||
IO copyFile (classDir / name, temp / name)
|
IO copyFile (classDir / name, temp / name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// zip it up
|
// zip it up
|
||||||
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
IO delete (temp / "META-INF" / "MANIFEST.MF")
|
||||||
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
|
||||||
val manifest = new JarManifest
|
val manifest = new JarManifest
|
||||||
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
|
||||||
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
|
||||||
val outputFile = workDir / warName
|
val outputFile = workDir / warName
|
||||||
IO jar (contentMappings, outputFile, manifest)
|
IO jar (contentMappings, outputFile, manifest)
|
||||||
|
|
||||||
// generate checksums
|
// generate checksums
|
||||||
Seq("md5", "sha1") foreach { algorithm =>
|
Seq(
|
||||||
IO.write(
|
"md5" -> "MD5",
|
||||||
workDir / (warName + "." + algorithm),
|
"sha1" -> "SHA-1",
|
||||||
ChecksumHelper computeAsString (outputFile, algorithm)
|
"sha256" -> "SHA-256"
|
||||||
)
|
)
|
||||||
|
.foreach { case (extension, algorithm) =>
|
||||||
|
val checksumFile = workDir / (warName + "." + extension)
|
||||||
|
Checksums generate (outputFile, checksumFile, algorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// done
|
// done
|
||||||
@@ -153,7 +169,7 @@ executableKey := {
|
|||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
Keys.artifact in (Compile, executableKey) ~= {
|
Keys.artifact in (Compile, executableKey) ~= {
|
||||||
_ copy (`type` = "war", extension = "war"))
|
_ copy (`type` = "war", extension = "war"))
|
||||||
}
|
}
|
||||||
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,3 +32,11 @@ $ sbt executable
|
|||||||
```
|
```
|
||||||
|
|
||||||
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.
|
||||||
|
|
||||||
|
Run tests spec
|
||||||
|
---------
|
||||||
|
To run the full serie of tests, run the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
sbt test
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,28 +6,29 @@ Update version number
|
|||||||
|
|
||||||
Note to update version number in files below:
|
Note to update version number in files below:
|
||||||
|
|
||||||
### project/build.scala
|
### build.sbt
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
object MyBuild extends Build {
|
val Organization = "gitbucket"
|
||||||
val Organization = "gitbucket"
|
val Name = "gitbucket"
|
||||||
val Name = "gitbucket"
|
val GitBucketVersion = "4.0.0" // <---- update version!!
|
||||||
val Version = "3.3.0" // <---- update version!!
|
val ScalatraVersion = "2.4.0"
|
||||||
val ScalaVersion = "2.11.6"
|
val JettyVersion = "9.3.6.v20151106"
|
||||||
val ScalatraVersion = "2.3.1"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
|
### src/main/scala/gitbucket/core/GitBucketCoreModule.scala
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
object AutoUpdate {
|
object GitBucketCoreModule extends Module("gitbucket-core",
|
||||||
|
// add new version definition
|
||||||
/**
|
new Version("4.1.0",
|
||||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
new LiquibaseMigration("update/gitbucket-core_4.1.xml")
|
||||||
*/
|
),
|
||||||
val versions = Seq(
|
new Version("4.0.0",
|
||||||
new Version(3, 3), // <---- add this line!!
|
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
|
||||||
new Version(3, 2),
|
new SqlMigration("update/gitbucket-core_4.0.sql")
|
||||||
|
)
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate release files
|
Generate release files
|
||||||
|
|||||||
34
project/Checksums.scala
Normal file
34
project/Checksums.scala
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import java.security.MessageDigest;
|
||||||
|
import scala.annotation._
|
||||||
|
import sbt._
|
||||||
|
import sbt.Using._
|
||||||
|
|
||||||
|
object Checksums {
|
||||||
|
private val bufferSize = 2048
|
||||||
|
|
||||||
|
def generate(source:File, target:File, algorithm:String):Unit =
|
||||||
|
IO write (target, compute(source, algorithm))
|
||||||
|
|
||||||
|
def compute(file:File, algorithm:String):String =
|
||||||
|
hex(raw(file, algorithm))
|
||||||
|
|
||||||
|
def raw(file:File, algorithm:String):Array[Byte] =
|
||||||
|
(Using fileInputStream file) { is =>
|
||||||
|
val md = MessageDigest getInstance algorithm
|
||||||
|
val buf = new Array[Byte](bufferSize)
|
||||||
|
md.reset()
|
||||||
|
@tailrec
|
||||||
|
def loop() {
|
||||||
|
val len = is read buf
|
||||||
|
if (len != -1) {
|
||||||
|
md update (buf, 0, len)
|
||||||
|
loop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop()
|
||||||
|
md.digest()
|
||||||
|
}
|
||||||
|
|
||||||
|
def hex(bytes:Array[Byte]):String =
|
||||||
|
bytes map { it => "%02x" format (it.toInt & 0xff) } mkString ""
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<extension>
|
<extension>
|
||||||
<groupId>org.apache.maven.wagon</groupId>
|
<groupId>org.apache.maven.wagon</groupId>
|
||||||
<artifactId>wagon-ssh</artifactId>
|
<artifactId>wagon-ssh</artifactId>
|
||||||
<version>1.0-beta-6</version>
|
<version>2.10</version>
|
||||||
</extension>
|
</extension>
|
||||||
</extensions>
|
</extensions>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
2
sbt.bat
2
sbt.bat
@@ -1,2 +1,2 @@
|
|||||||
set SCRIPT_DIR=%~dp0
|
set SCRIPT_DIR=%~dp0
|
||||||
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %*
|
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %*
|
||||||
|
|||||||
2
sbt.sh
2
sbt.sh
@@ -1,2 +1,2 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@"
|
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@"
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ public class JettyLauncher {
|
|||||||
WebAppContext context = new WebAppContext();
|
WebAppContext context = new WebAppContext();
|
||||||
|
|
||||||
File tmpDir = new File(getGitBucketHome(), "tmp");
|
File tmpDir = new File(getGitBucketHome(), "tmp");
|
||||||
if(tmpDir.exists()){
|
if(!tmpDir.exists()){
|
||||||
deleteDirectory(tmpDir);
|
tmpDir.mkdirs();
|
||||||
}
|
}
|
||||||
tmpDir.mkdirs();
|
|
||||||
context.setTempDirectory(tmpDir);
|
context.setTempDirectory(tmpDir);
|
||||||
|
|
||||||
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
|
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
|
||||||
|
|||||||
52
src/main/java/org/postgresql/Driver2.java
Normal file
52
src/main/java/org/postgresql/Driver2.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package org.postgresql;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps the PostgreSQL JDBC driver to convert the returning column names to lower case.
|
||||||
|
*/
|
||||||
|
public class Driver2 extends Driver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public java.sql.Connection connect(String url, Properties info) throws SQLException {
|
||||||
|
Connection conn = super.connect(url, info);
|
||||||
|
|
||||||
|
Object proxy = Proxy.newProxyInstance(
|
||||||
|
conn.getClass().getClassLoader(),
|
||||||
|
new Class[]{ Connection.class },
|
||||||
|
new ConnectionProxyHandler(conn)
|
||||||
|
);
|
||||||
|
|
||||||
|
return Connection.class.cast(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class ConnectionProxyHandler implements InvocationHandler {
|
||||||
|
|
||||||
|
private Connection conn;
|
||||||
|
|
||||||
|
public ConnectionProxyHandler(Connection conn){
|
||||||
|
this.conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||||
|
if(method.getName().equals("prepareStatement")){
|
||||||
|
if(args != null && args.length == 2 && args[1].getClass().isArray()){
|
||||||
|
String[] keys = (String[]) args[1];
|
||||||
|
for(int i = 0; i < keys.length; i++){
|
||||||
|
keys[i] = keys[i].toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return method.invoke(conn, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
db {
|
|
||||||
driver = "org.h2.Driver"
|
|
||||||
url = "jdbc:h2:${DatabaseHome};MVCC=true"
|
|
||||||
user = "sa"
|
|
||||||
password = "sa"
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<!--
|
<!--
|
||||||
<logger name="service.WebHookService" level="DEBUG" />
|
<logger name="service.WebHookService" level="DEBUG" />
|
||||||
<logger name="servlet" level="DEBUG" />
|
<logger name="servlet" level="DEBUG" />
|
||||||
-->
|
|
||||||
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
|
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
|
||||||
|
-->
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
c3p0 {
|
|
||||||
privilegeSpawnedThreads=true
|
|
||||||
contextClassLoaderSource=library
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
CREATE TABLE ACCOUNT(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
MAIL_ADDRESS VARCHAR(100) NOT NULL,
|
|
||||||
PASSWORD VARCHAR(40) NOT NULL,
|
|
||||||
ADMINISTRATOR BOOLEAN NOT NULL,
|
|
||||||
URL VARCHAR(200),
|
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
|
||||||
LAST_LOGIN_DATE TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE REPOSITORY(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
PRIVATE BOOLEAN NOT NULL,
|
|
||||||
DESCRIPTION TEXT,
|
|
||||||
DEFAULT_BRANCH VARCHAR(100),
|
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
|
||||||
LAST_ACTIVITY_DATE TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE COLLABORATOR(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
COLLABORATOR_NAME VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE ISSUE(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ISSUE_ID INT NOT NULL,
|
|
||||||
OPENED_USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
MILESTONE_ID INT,
|
|
||||||
ASSIGNED_USER_NAME VARCHAR(100),
|
|
||||||
TITLE TEXT NOT NULL,
|
|
||||||
CONTENT TEXT,
|
|
||||||
CLOSED BOOLEAN NOT NULL,
|
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE ISSUE_ID(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ISSUE_ID INT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE ISSUE_COMMENT(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ISSUE_ID INT NOT NULL,
|
|
||||||
COMMENT_ID INT AUTO_INCREMENT,
|
|
||||||
ACTION VARCHAR(10),
|
|
||||||
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
CONTENT TEXT NOT NULL,
|
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE LABEL(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
LABEL_ID INT AUTO_INCREMENT,
|
|
||||||
LABEL_NAME VARCHAR(100) NOT NULL,
|
|
||||||
COLOR CHAR(6) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE ISSUE_LABEL(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ISSUE_ID INT NOT NULL,
|
|
||||||
LABEL_ID INT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE MILESTONE(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
MILESTONE_ID INT AUTO_INCREMENT,
|
|
||||||
TITLE VARCHAR(100) NOT NULL,
|
|
||||||
DESCRIPTION TEXT,
|
|
||||||
DUE_DATE TIMESTAMP,
|
|
||||||
CLOSED_DATE TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_PK PRIMARY KEY (USER_NAME);
|
|
||||||
ALTER TABLE ACCOUNT ADD CONSTRAINT IDX_ACCOUNT_1 UNIQUE (MAIL_ADDRESS);
|
|
||||||
|
|
||||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE REPOSITORY ADD CONSTRAINT IDX_REPOSITORY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
|
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_PK PRIMARY KEY (ISSUE_ID, USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK1 FOREIGN KEY (OPENED_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
ALTER TABLE ISSUE ADD CONSTRAINT IDX_ISSUE_FK2 FOREIGN KEY (MILESTONE_ID) REFERENCES MILESTONE (MILESTONE_ID);
|
|
||||||
|
|
||||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE ISSUE_ID ADD CONSTRAINT IDX_ISSUE_ID_FK1 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
|
|
||||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
|
||||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, COMMENT_ID);
|
|
||||||
ALTER TABLE ISSUE_COMMENT ADD CONSTRAINT IDX_ISSUE_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
|
||||||
|
|
||||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, LABEL_ID);
|
|
||||||
ALTER TABLE LABEL ADD CONSTRAINT IDX_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
|
|
||||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID);
|
|
||||||
ALTER TABLE ISSUE_LABEL ADD CONSTRAINT IDX_ISSUE_LABEL_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
|
||||||
|
|
||||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, MILESTONE_ID);
|
|
||||||
ALTER TABLE MILESTONE ADD CONSTRAINT IDX_MILESTONE_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
|
|
||||||
INSERT INTO ACCOUNT (
|
|
||||||
USER_NAME,
|
|
||||||
MAIL_ADDRESS,
|
|
||||||
PASSWORD,
|
|
||||||
ADMINISTRATOR,
|
|
||||||
URL,
|
|
||||||
REGISTERED_DATE,
|
|
||||||
UPDATED_DATE,
|
|
||||||
LAST_LOGIN_DATE
|
|
||||||
) VALUES (
|
|
||||||
'root',
|
|
||||||
'root@localhost',
|
|
||||||
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
|
|
||||||
true,
|
|
||||||
'https://github.com/gitbucket/gitbucket',
|
|
||||||
SYSDATE,
|
|
||||||
SYSDATE,
|
|
||||||
NULL
|
|
||||||
);
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- Fix COLLABORATOR constraints
|
|
||||||
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK1 IF EXISTS;
|
|
||||||
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_FK0 IF EXISTS;
|
|
||||||
ALTER TABLE COLLABORATOR DROP CONSTRAINT IDX_COLLABORATOR_PK IF EXISTS;
|
|
||||||
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COLLABORATOR_NAME);
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE COLLABORATOR ADD CONSTRAINT IDX_COLLABORATOR_FK1 FOREIGN KEY (COLLABORATOR_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
ALTER TABLE GROUP_MEMBER ADD COLUMN MANAGER BOOLEAN DEFAULT FALSE;
|
|
||||||
|
|
||||||
CREATE TABLE SSH_KEY (
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
SSH_KEY_ID INT AUTO_INCREMENT,
|
|
||||||
TITLE VARCHAR(100) NOT NULL,
|
|
||||||
PUBLIC_KEY TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_PK PRIMARY KEY (USER_NAME, SSH_KEY_ID);
|
|
||||||
ALTER TABLE SSH_KEY ADD CONSTRAINT IDX_SSH_KEY_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
DROP TABLE COMMIT_LOG;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
CREATE TABLE ACTIVITY(
|
|
||||||
ACTIVITY_ID INT AUTO_INCREMENT,
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ACTIVITY_USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ACTIVITY_TYPE VARCHAR(100) NOT NULL,
|
|
||||||
MESSAGE TEXT NOT NULL,
|
|
||||||
ADDITIONAL_INFO TEXT,
|
|
||||||
ACTIVITY_DATE TIMESTAMP NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE COMMIT_LOG (
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
COMMIT_ID VARCHAR(40) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_PK PRIMARY KEY (ACTIVITY_ID);
|
|
||||||
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE ACTIVITY ADD CONSTRAINT IDX_ACTIVITY_FK1 FOREIGN KEY (ACTIVITY_USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
|
|
||||||
ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, COMMIT_ID);
|
|
||||||
ALTER TABLE COMMIT_LOG ADD CONSTRAINT IDX_COMMIT_LOG_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
ALTER TABLE ACCOUNT ADD COLUMN IMAGE VARCHAR(100);
|
|
||||||
|
|
||||||
UPDATE ISSUE_COMMENT SET ACTION = 'comment' WHERE ACTION IS NULL;
|
|
||||||
|
|
||||||
ALTER TABLE ISSUE_COMMENT ALTER COLUMN ACTION VARCHAR(20) NOT NULL;
|
|
||||||
|
|
||||||
UPDATE ISSUE_COMMENT SET ACTION = 'close_comment' WHERE ACTION = 'close';
|
|
||||||
UPDATE ISSUE_COMMENT SET ACTION = 'reopen_comment' WHERE ACTION = 'reopen';
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
CREATE TABLE GROUP_MEMBER(
|
|
||||||
GROUP_NAME VARCHAR(100) NOT NULL,
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_PK PRIMARY KEY (GROUP_NAME, USER_NAME);
|
|
||||||
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK0 FOREIGN KEY (GROUP_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
ALTER TABLE GROUP_MEMBER ADD CONSTRAINT IDX_GROUP_MEMBER_FK1 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME);
|
|
||||||
|
|
||||||
ALTER TABLE ACCOUNT ADD COLUMN GROUP_ACCOUNT BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
|
||||||
SELECT
|
|
||||||
A.USER_NAME,
|
|
||||||
A.REPOSITORY_NAME,
|
|
||||||
A.ISSUE_ID,
|
|
||||||
NVL(B.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
|
||||||
FROM ISSUE A
|
|
||||||
LEFT OUTER JOIN (
|
|
||||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
|
||||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
|
||||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
|
||||||
) B
|
|
||||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_USER_NAME VARCHAR(100);
|
|
||||||
ALTER TABLE REPOSITORY ADD COLUMN ORIGIN_REPOSITORY_NAME VARCHAR(100);
|
|
||||||
ALTER TABLE REPOSITORY ADD COLUMN PARENT_USER_NAME VARCHAR(100);
|
|
||||||
ALTER TABLE REPOSITORY ADD COLUMN PARENT_REPOSITORY_NAME VARCHAR(100);
|
|
||||||
|
|
||||||
CREATE TABLE PULL_REQUEST(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
ISSUE_ID INT NOT NULL,
|
|
||||||
BRANCH VARCHAR(100) NOT NULL,
|
|
||||||
REQUEST_USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REQUEST_REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REQUEST_BRANCH VARCHAR(100) NOT NULL,
|
|
||||||
COMMIT_ID_FROM VARCHAR(40) NOT NULL,
|
|
||||||
COMMIT_ID_TO VARCHAR(40) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
|
||||||
ALTER TABLE PULL_REQUEST ADD CONSTRAINT IDX_PULL_REQUEST_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, ISSUE_ID) REFERENCES ISSUE (USER_NAME, REPOSITORY_NAME, ISSUE_ID);
|
|
||||||
|
|
||||||
ALTER TABLE ISSUE ADD COLUMN PULL_REQUEST BOOLEAN NOT NULL DEFAULT FALSE;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
CREATE TABLE WEB_HOOK (
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
URL VARCHAR(200) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL);
|
|
||||||
ALTER TABLE WEB_HOOK ADD CONSTRAINT IDX_WEB_HOOK_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE ACCOUNT ADD COLUMN FULL_NAME VARCHAR(100);
|
|
||||||
|
|
||||||
UPDATE ACCOUNT SET FULL_NAME = USER_NAME WHERE FULL_NAME IS NULL;
|
|
||||||
|
|
||||||
ALTER TABLE ACCOUNT ALTER COLUMN FULL_NAME SET NOT NULL;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE PLUGIN (
|
|
||||||
PLUGIN_ID VARCHAR(100) NOT NULL,
|
|
||||||
VERSION VARCHAR(100) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID);
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
CREATE TABLE COMMIT_COMMENT (
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
COMMIT_ID VARCHAR(100) NOT NULL,
|
|
||||||
COMMENT_ID INT AUTO_INCREMENT,
|
|
||||||
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
CONTENT TEXT NOT NULL,
|
|
||||||
FILE_NAME NVARCHAR(100),
|
|
||||||
OLD_LINE_NUMBER INT,
|
|
||||||
NEW_LINE_NUMBER INT,
|
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL,
|
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL,
|
|
||||||
PULL_REQUEST BOOLEAN NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID);
|
|
||||||
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
|
|
||||||
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260);
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS ACCESS_TOKEN;
|
|
||||||
|
|
||||||
CREATE TABLE ACCESS_TOKEN (
|
|
||||||
ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
TOKEN_HASH VARCHAR(40) NOT NULL,
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
NOTE TEXT NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID);
|
|
||||||
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH);
|
|
||||||
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS COMMIT_STATUS;
|
|
||||||
CREATE TABLE COMMIT_STATUS(
|
|
||||||
COMMIT_STATUS_ID INT AUTO_INCREMENT,
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
COMMIT_ID VARCHAR(40) NOT NULL,
|
|
||||||
CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters)
|
|
||||||
STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure
|
|
||||||
TARGET_URL VARCHAR(200),
|
|
||||||
DESCRIPTION TEXT,
|
|
||||||
CREATOR VARCHAR(100) NOT NULL,
|
|
||||||
REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT
|
|
||||||
UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT
|
|
||||||
);
|
|
||||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID);
|
|
||||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1
|
|
||||||
UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT);
|
|
||||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1
|
|
||||||
FOREIGN KEY (USER_NAME, REPOSITORY_NAME)
|
|
||||||
REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2
|
|
||||||
FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3
|
|
||||||
FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS PROTECTED_BRANCH;
|
|
||||||
|
|
||||||
CREATE TABLE PROTECTED_BRANCH(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
BRANCH VARCHAR(100) NOT NULL,
|
|
||||||
STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH);
|
|
||||||
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT;
|
|
||||||
CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
BRANCH VARCHAR(100) NOT NULL,
|
|
||||||
CONTEXT VARCHAR(255) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT);
|
|
||||||
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
DROP TABLE IF EXISTS WEB_HOOK_EVENT;
|
|
||||||
|
|
||||||
CREATE TABLE WEB_HOOK_EVENT(
|
|
||||||
USER_NAME VARCHAR(100) NOT NULL,
|
|
||||||
REPOSITORY_NAME VARCHAR(100) NOT NULL,
|
|
||||||
URL VARCHAR(200) NOT NULL,
|
|
||||||
EVENT VARCHAR(30) NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL, EVENT);
|
|
||||||
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, URL) REFERENCES WEB_HOOK (USER_NAME, REPOSITORY_NAME, URL)
|
|
||||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
CREATE TEMPORARY TABLE TMP_EVENTS (EVENT VARCHAR(30));
|
|
||||||
|
|
||||||
INSERT INTO TMP_EVENTS VALUES ('push'),('issue_comment'),('issues'),('pull_request');
|
|
||||||
|
|
||||||
INSERT INTO WEB_HOOK_EVENT (USER_NAME, REPOSITORY_NAME, URL, EVENT)
|
|
||||||
SELECT USER_NAME, REPOSITORY_NAME, URL, EVENT
|
|
||||||
FROM WEB_HOOK, TMP_EVENTS;
|
|
||||||
|
|
||||||
DROP TABLE TMP_EVENTS;
|
|
||||||
|
|
||||||
ALTER TABLE COMMIT_COMMENT ADD COLUMN ISSUE_ID INT;
|
|
||||||
|
|
||||||
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
|
||||||
SELECT
|
|
||||||
A.USER_NAME,
|
|
||||||
A.REPOSITORY_NAME,
|
|
||||||
A.ISSUE_ID,
|
|
||||||
NVL(B.COMMENT_COUNT, 0) + NVL(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
|
||||||
FROM ISSUE A
|
|
||||||
LEFT OUTER JOIN (
|
|
||||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
|
||||||
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
|
||||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
|
||||||
) B
|
|
||||||
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
|
||||||
LEFT OUTER JOIN (
|
|
||||||
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
|
||||||
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
|
||||||
) C
|
|
||||||
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);
|
|
||||||
|
|
||||||
|
|
||||||
UPDATE COMMIT_COMMENT C SET (ISSUE_ID) = (
|
|
||||||
SELECT MAX(P.ISSUE_ID)
|
|
||||||
FROM PULL_REQUEST P
|
|
||||||
WHERE
|
|
||||||
C.USER_NAME = P.USER_NAME AND
|
|
||||||
C.REPOSITORY_NAME = P.REPOSITORY_NAME AND
|
|
||||||
C.COMMIT_ID = P.COMMIT_ID_TO
|
|
||||||
);
|
|
||||||
|
|
||||||
ALTER TABLE COMMIT_COMMENT DROP COLUMN PULL_REQUEST;
|
|
||||||
18
src/main/resources/update/gitbucket-core_4.0.sql
Normal file
18
src/main/resources/update/gitbucket-core_4.0.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
|
||||||
|
SELECT
|
||||||
|
A.USER_NAME,
|
||||||
|
A.REPOSITORY_NAME,
|
||||||
|
A.ISSUE_ID,
|
||||||
|
COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
|
||||||
|
FROM ISSUE A
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
|
||||||
|
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
|
||||||
|
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||||
|
) B
|
||||||
|
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
|
||||||
|
LEFT OUTER JOIN (
|
||||||
|
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
|
||||||
|
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
|
||||||
|
) C
|
||||||
|
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);
|
||||||
351
src/main/resources/update/gitbucket-core_4.0.xml
Normal file
351
src/main/resources/update/gitbucket-core_4.0.xml
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<changeSet>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ACCOUNT -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ACCOUNT">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="MAIL_ADDRESS" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="PASSWORD" type="varchar(40)" nullable="false"/>
|
||||||
|
<column name="ADMINISTRATOR" type="boolean" nullable="false"/>
|
||||||
|
<column name="URL" type="varchar(200)" nullable="true"/>
|
||||||
|
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="LAST_LOGIN_DATE" type="datetime" nullable="true"/>
|
||||||
|
<column name="IMAGE" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="GROUP_ACCOUNT" type="boolean" nullable="false"/>
|
||||||
|
<column name="FULL_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REMOVED" type="boolean" nullable="true"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ACCOUNT_PK" tableName="ACCOUNT" columnNames="USER_NAME"/>
|
||||||
|
<addUniqueConstraint constraintName="IDX_ACCOUNT_1" tableName="ACCOUNT" columnNames="MAIL_ADDRESS"/>
|
||||||
|
|
||||||
|
<insert tableName="ACCOUNT">
|
||||||
|
<column name="USER_NAME" value="root"/>
|
||||||
|
<column name="FULL_NAME" value="root"/>
|
||||||
|
<column name="MAIL_ADDRESS" value="root@localhost"/>
|
||||||
|
<column name="PASSWORD" value="dc76e9f0c0006e8f919e0c515c66dbba3982f785"/>
|
||||||
|
<column name="ADMINISTRATOR" valueBoolean="true"/>
|
||||||
|
<column name="URL" value="https://github.com/gitbucket/gitbucket"/>
|
||||||
|
<column name="GROUP_ACCOUNT" valueBoolean="false"/>
|
||||||
|
<column name="REMOVED" valueBoolean="false"/>
|
||||||
|
<column name="REGISTERED_DATE" valueDate="${currentDateTime}"/>
|
||||||
|
<column name="UPDATED_DATE" valueDate="${currentDateTime}"/>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- REPOSITORY -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="REPOSITORY">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="PRIVATE" type="boolean" nullable="false"/>
|
||||||
|
<column name="DESCRIPTION" type="text" nullable="true"/>
|
||||||
|
<column name="DEFAULT_BRANCH" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="LAST_ACTIVITY_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="ORIGIN_USER_NAME" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="ORIGIN_REPOSITORY_NAME" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="PARENT_USER_NAME" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="PARENT_REPOSITORY_NAME" type="varchar(100)" nullable="true"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_REPOSITORY_PK" tableName="REPOSITORY" columnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_REPOSITORY_FK0" baseTableName="REPOSITORY" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ACCESS_TOKEN -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ACCESS_TOKEN">
|
||||||
|
<column name="ACCESS_TOKEN_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="TOKEN_HASH" type="varchar(40)" nullable="false"/>
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="NOTE" type="text" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ACCESS_TOKEN_PK" tableName="ACCESS_TOKEN" columnNames="ACCESS_TOKEN_ID"/>
|
||||||
|
<addUniqueConstraint constraintName="IDX_ACCESS_TOKEN_TOKEN_HASH" tableName="ACCESS_TOKEN" columnNames="TOKEN_HASH"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ACCESS_TOKEN_FK0" baseTableName="ACCESS_TOKEN" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ACTIVITY -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ACTIVITY">
|
||||||
|
<column name="ACTIVITY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ACTIVITY_USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ACTIVITY_TYPE" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="MESSAGE" type="text" nullable="false"/>
|
||||||
|
<column name="ADDITIONAL_INFO" type="text" nullable="true"/>
|
||||||
|
<column name="ACTIVITY_DATE" type="datetime" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ACTIVITY_PK" tableName="ACTIVITY" columnNames="ACTIVITY_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ACTIVITY_FK1" baseTableName="ACTIVITY" baseColumnNames="ACTIVITY_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ACTIVITY_FK0" baseTableName="ACTIVITY" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- COLLABORATOR -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="COLLABORATOR">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="COLLABORATOR_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_COLLABORATOR_PK" tableName="COLLABORATOR" columnNames="USER_NAME, REPOSITORY_NAME, COLLABORATOR_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_COLLABORATOR_FK1" baseTableName="COLLABORATOR" baseColumnNames="COLLABORATOR_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_COLLABORATOR_FK0" baseTableName="COLLABORATOR" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- COMMIT_COMMENT -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="COMMIT_COMMENT">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="COMMIT_ID" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="COMMENTED_USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="CONTENT" type="text" nullable="false"/>
|
||||||
|
<column name="FILE_NAME" type="varchar(260)" nullable="true"/>
|
||||||
|
<column name="OLD_LINE_NUMBER" type="int" nullable="true"/>
|
||||||
|
<column name="NEW_LINE_NUMBER" type="int" nullable="true"/>
|
||||||
|
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="ISSUE_ID" type="int" nullable="true"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_COMMIT_COMMENT_PK" tableName="COMMIT_COMMENT" columnNames="COMMENT_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_COMMIT_COMMENT_FK0" baseTableName="COMMIT_COMMENT" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- COMMIT_STATUS -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="COMMIT_STATUS">
|
||||||
|
<column name="COMMIT_STATUS_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="COMMIT_ID" type="varchar(40)" nullable="false"/>
|
||||||
|
<column name="CONTEXT" type="varchar(255)" nullable="false"/>
|
||||||
|
<column name="STATE" type="varchar(10)" nullable="false"/>
|
||||||
|
<column name="TARGET_URL" type="varchar(200)" nullable="true"/>
|
||||||
|
<column name="DESCRIPTION" type="text" nullable="true"/>
|
||||||
|
<column name="CREATOR" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_COMMIT_STATUS_PK" tableName="COMMIT_STATUS" columnNames="COMMIT_STATUS_ID"/>
|
||||||
|
<addUniqueConstraint constraintName="IDX_COMMIT_STATUS_1" tableName="COMMIT_STATUS" columnNames="USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK3" baseTableName="COMMIT_STATUS" baseColumnNames="CREATOR" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK2" baseTableName="COMMIT_STATUS" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_COMMIT_STATUS_FK1" baseTableName="COMMIT_STATUS" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- GROUP_MEMBER -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="GROUP_MEMBER">
|
||||||
|
<column name="GROUP_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="MANAGER" type="boolean" nullable="true"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_GROUP_MEMBER_PK" tableName="GROUP_MEMBER" columnNames="GROUP_NAME, USER_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_GROUP_MEMBER_FK1" baseTableName="GROUP_MEMBER" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_GROUP_MEMBER_FK0" baseTableName="GROUP_MEMBER" baseColumnNames="GROUP_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- LABEL -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="LABEL">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="LABEL_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="LABEL_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="COLOR" type="char(6)" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_LABEL_PK" tableName="LABEL" columnNames="USER_NAME, REPOSITORY_NAME, LABEL_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_LABEL_FK0" baseTableName="LABEL" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- MILESTONE -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="MILESTONE">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="MILESTONE_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="TITLE" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="DESCRIPTION" type="text" nullable="true"/>
|
||||||
|
<column name="DUE_DATE" type="datetime" nullable="true"/>
|
||||||
|
<column name="CLOSED_DATE" type="datetime" nullable="true"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_MILESTONE_PK" tableName="MILESTONE" columnNames="USER_NAME, REPOSITORY_NAME, MILESTONE_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_MILESTONE_FK0" baseTableName="MILESTONE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ISSUE -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ISSUE">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ISSUE_ID" type="int" nullable="false"/>
|
||||||
|
<column name="OPENED_USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="MILESTONE_ID" type="int" nullable="true"/>
|
||||||
|
<column name="ASSIGNED_USER_NAME" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="TITLE" type="text" nullable="false"/>
|
||||||
|
<column name="CONTENT" type="text" nullable="true"/>
|
||||||
|
<column name="CLOSED" type="boolean" nullable="false"/>
|
||||||
|
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="PULL_REQUEST" type="boolean" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ISSUE_PK" tableName="ISSUE" columnNames="ISSUE_ID, USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK0" baseTableName="ISSUE" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK2" baseTableName="ISSUE" baseColumnNames="MILESTONE_ID" referencedTableName="MILESTONE" referencedColumnNames="MILESTONE_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_FK1" baseTableName="ISSUE" baseColumnNames="OPENED_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ISSUE_COMMENT -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ISSUE_COMMENT">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ISSUE_ID" type="int" nullable="false"/>
|
||||||
|
<column name="COMMENT_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="ACTION" type="varchar(20)" nullable="false"/>
|
||||||
|
<column name="COMMENTED_USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="CONTENT" type="text" nullable="false"/>
|
||||||
|
<column name="REGISTERED_DATE" type="datetime" nullable="false"/>
|
||||||
|
<column name="UPDATED_DATE" type="datetime" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ISSUE_COMMENT_PK" tableName="ISSUE_COMMENT" columnNames="COMMENT_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_COMMENT_FK0" baseTableName="ISSUE_COMMENT" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ISSUE_ID -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ISSUE_ID">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ISSUE_ID" type="int" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ISSUE_ID_PK" tableName="ISSUE_ID" columnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_ID_FK1" baseTableName="ISSUE_ID" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- ISSUE_ID -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="ISSUE_LABEL">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ISSUE_ID" type="int" nullable="false"/>
|
||||||
|
<column name="LABEL_ID" type="int" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_ISSUE_LABEL_PK" tableName="ISSUE_LABEL" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, LABEL_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_ISSUE_LABEL_FK0" baseTableName="ISSUE_LABEL" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- PLUGIN -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="PLUGIN">
|
||||||
|
<column name="PLUGIN_ID" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="VERSION" type="varchar(100)" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_PLUGIN_PK" tableName="PLUGIN" columnNames="PLUGIN_ID"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- PULL_REQUEST -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="PULL_REQUEST">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="ISSUE_ID" type="int" nullable="false"/>
|
||||||
|
<column name="BRANCH" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REQUEST_USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REQUEST_REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REQUEST_BRANCH" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="COMMIT_ID_FROM" type="varchar(40)" nullable="false"/>
|
||||||
|
<column name="COMMIT_ID_TO" type="varchar(40)" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_PULL_REQUEST_PK" tableName="PULL_REQUEST" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_PULL_REQUEST_FK0" baseTableName="PULL_REQUEST" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- SSH_KEY -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="SSH_KEY">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="SSH_KEY_ID" type="int" nullable="false" autoIncrement="true" unique="true"/>
|
||||||
|
<column name="TITLE" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="PUBLIC_KEY" type="text" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_SSH_KEY_PK" tableName="SSH_KEY" columnNames="USER_NAME, SSH_KEY_ID"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_SSH_KEY_FK0" baseTableName="SSH_KEY" baseColumnNames="USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- WEB_HOOK -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="WEB_HOOK">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="URL" type="varchar(200)" nullable="false"/>
|
||||||
|
<column name="TOKEN" type="varchar(100)" nullable="true"/>
|
||||||
|
<column name="CTYPE" type="varchar(10)" nullable="true"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_WEB_HOOK_PK" 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"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- WEB_HOOK_EVENT -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="WEB_HOOK_EVENT">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="URL" type="varchar(200)" nullable="false"/>
|
||||||
|
<column name="EVENT" type="varchar(30)" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_WEB_HOOK_EVENT_PK" tableName="WEB_HOOK_EVENT" columnNames="USER_NAME, REPOSITORY_NAME, URL, EVENT"/>
|
||||||
|
<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"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- PROTECTED_BRANCH -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="PROTECTED_BRANCH">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="BRANCH" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="STATUS_CHECK_ADMIN" type="boolean" nullable="false" defaultValueBoolean="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_PK" tableName="PROTECTED_BRANCH" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_FK0" baseTableName="PROTECTED_BRANCH" baseColumnNames="USER_NAME, REPOSITORY_NAME" referencedTableName="REPOSITORY" referencedColumnNames="USER_NAME, REPOSITORY_NAME" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<!-- PROTECTED_BRANCH_REQUIRE_CONTEXT -->
|
||||||
|
<!--================================================================================================-->
|
||||||
|
<createTable tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT">
|
||||||
|
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="BRANCH" type="varchar(100)" nullable="false"/>
|
||||||
|
<column name="CONTEXT" type="varchar(255)" nullable="false"/>
|
||||||
|
</createTable>
|
||||||
|
|
||||||
|
<addPrimaryKey constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK" tableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" columnNames="USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT"/>
|
||||||
|
<addForeignKeyConstraint constraintName="IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0" baseTableName="PROTECTED_BRANCH_REQUIRE_CONTEXT" baseColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" referencedTableName="PROTECTED_BRANCH" referencedColumnNames="USER_NAME, REPOSITORY_NAME, BRANCH" onDelete="CASCADE" onUpdate="CASCADE"/>
|
||||||
|
|
||||||
|
</changeSet>
|
||||||
@@ -27,10 +27,10 @@ class ScalatraBootstrap extends LifeCycle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.mount(new IndexController, "/")
|
context.mount(new IndexController, "/")
|
||||||
|
context.mount(new ApiController, "/api/v3")
|
||||||
context.mount(new FileUploadController, "/upload")
|
context.mount(new FileUploadController, "/upload")
|
||||||
|
context.mount(new SystemSettingsController, "/admin")
|
||||||
context.mount(new DashboardController, "/*")
|
context.mount(new DashboardController, "/*")
|
||||||
context.mount(new UserManagementController, "/*")
|
|
||||||
context.mount(new SystemSettingsController, "/*")
|
|
||||||
context.mount(new AccountController, "/*")
|
context.mount(new AccountController, "/*")
|
||||||
context.mount(new RepositoryViewerController, "/*")
|
context.mount(new RepositoryViewerController, "/*")
|
||||||
context.mount(new WikiController, "/*")
|
context.mount(new WikiController, "/*")
|
||||||
|
|||||||
12
src/main/scala/gitbucket/core/GitBucketCoreModule.scala
Normal file
12
src/main/scala/gitbucket/core/GitBucketCoreModule.scala
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package gitbucket.core
|
||||||
|
|
||||||
|
import io.github.gitbucket.solidbase.migration.{SqlMigration, LiquibaseMigration}
|
||||||
|
import io.github.gitbucket.solidbase.model.{Version, Module}
|
||||||
|
|
||||||
|
object GitBucketCoreModule extends Module("gitbucket-core",
|
||||||
|
new Version("4.1.0"),
|
||||||
|
new Version("4.0.0",
|
||||||
|
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
|
||||||
|
new SqlMigration("update/gitbucket-core_4.0.sql")
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -14,7 +14,7 @@ object ApiBranchProtection{
|
|||||||
|
|
||||||
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
|
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
|
||||||
enabled = info.enabled,
|
enabled = info.enabled,
|
||||||
required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts)))
|
required_status_checks = Some(Status(EnforcementLevel(info.enabled && info.contexts.nonEmpty, info.includeAdministrators), info.contexts)))
|
||||||
val statusNone = Status(Off, Seq.empty)
|
val statusNone = Status(Off, Seq.empty)
|
||||||
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
|
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
|
||||||
sealed class EnforcementLevel(val name: String)
|
sealed class EnforcementLevel(val name: String)
|
||||||
@@ -44,4 +44,3 @@ object ApiBranchProtection{
|
|||||||
}
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ case class ApiIssue(
|
|||||||
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
body: String)(repositoryName: RepositoryName, isPullRequest: Boolean){
|
||||||
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
|
||||||
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${number}")
|
||||||
|
val pull_request = if (isPullRequest) {
|
||||||
|
Some(Map(
|
||||||
|
"url" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${number}"),
|
||||||
|
"html_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}")
|
||||||
|
// "diff_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.diff"),
|
||||||
|
// "patch_url" -> ApiPath(s"/${repositoryName.fullName}/pull/${number}.patch")
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ApiIssue{
|
object ApiIssue{
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ case class ApiPullRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
object ApiPullRequest{
|
object ApiPullRequest{
|
||||||
def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest = ApiPullRequest(
|
def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest =
|
||||||
|
ApiPullRequest(
|
||||||
number = issue.issueId,
|
number = issue.issueId,
|
||||||
updated_at = issue.updatedDate,
|
updated_at = issue.updatedDate,
|
||||||
created_at = issue.registeredDate,
|
created_at = issue.registeredDate,
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ case class ApiRepository(
|
|||||||
`private`: Boolean,
|
`private`: Boolean,
|
||||||
default_branch: String,
|
default_branch: String,
|
||||||
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
|
owner: ApiUser)(urlIsHtmlUrl: Boolean) {
|
||||||
val forks_count = forks
|
val forks_count = forks
|
||||||
val watchers_count = watchers
|
val watchers_count = watchers
|
||||||
val url = if(urlIsHtmlUrl){
|
val url = if(urlIsHtmlUrl){
|
||||||
ApiPath(s"/${full_name}")
|
ApiPath(s"/${full_name}")
|
||||||
}else{
|
} else {
|
||||||
ApiPath(s"/api/v3/repos/${full_name}")
|
ApiPath(s"/api/v3/repos/${full_name}")
|
||||||
}
|
}
|
||||||
val http_url = ApiPath(s"/git/${full_name}.git")
|
val http_url = ApiPath(s"/git/${full_name}.git")
|
||||||
@@ -34,14 +34,14 @@ object ApiRepository{
|
|||||||
watchers: Int = 0,
|
watchers: Int = 0,
|
||||||
urlIsHtmlUrl: Boolean = false): ApiRepository =
|
urlIsHtmlUrl: Boolean = false): ApiRepository =
|
||||||
ApiRepository(
|
ApiRepository(
|
||||||
name = repository.repositoryName,
|
name = repository.repositoryName,
|
||||||
full_name = s"${repository.userName}/${repository.repositoryName}",
|
full_name = s"${repository.userName}/${repository.repositoryName}",
|
||||||
description = repository.description.getOrElse(""),
|
description = repository.description.getOrElse(""),
|
||||||
watchers = 0,
|
watchers = 0,
|
||||||
forks = forkedCount,
|
forks = forkedCount,
|
||||||
`private` = repository.isPrivate,
|
`private` = repository.isPrivate,
|
||||||
default_branch = repository.defaultBranch,
|
default_branch = repository.defaultBranch,
|
||||||
owner = owner
|
owner = owner
|
||||||
)(urlIsHtmlUrl)
|
)(urlIsHtmlUrl)
|
||||||
|
|
||||||
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.account.html
|
import gitbucket.core.account.html
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.helper
|
import gitbucket.core.helper
|
||||||
import gitbucket.core.model.GroupMember
|
import gitbucket.core.model.GroupMember
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
@@ -14,22 +13,19 @@ import gitbucket.core.util._
|
|||||||
|
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import org.eclipse.jgit.dircache.DirCache
|
|
||||||
import org.eclipse.jgit.lib.{FileMode, Constants}
|
|
||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
|
|
||||||
class AccountController extends AccountControllerBase
|
class AccountController extends AccountControllerBase
|
||||||
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
with AccessTokenService with WebHookService
|
with AccessTokenService with WebHookService with RepositoryCreationService
|
||||||
|
|
||||||
|
|
||||||
trait AccountControllerBase extends AccountManagementControllerBase {
|
trait AccountControllerBase extends AccountManagementControllerBase {
|
||||||
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
|
||||||
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
|
||||||
with AccessTokenService with WebHookService =>
|
with AccessTokenService with WebHookService with RepositoryCreationService =>
|
||||||
|
|
||||||
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
|
||||||
url: Option[String], fileId: Option[String])
|
url: Option[String], fileId: Option[String])
|
||||||
@@ -156,29 +152,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/users/#get-a-single-user
|
|
||||||
*/
|
|
||||||
get("/api/v3/users/:userName") {
|
|
||||||
getAccountByUserName(params("userName")).map { account =>
|
|
||||||
JsonFormat(ApiUser(account))
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/users/#get-the-authenticated-user
|
|
||||||
*/
|
|
||||||
get("/api/v3/user") {
|
|
||||||
context.loginAccount.map { account =>
|
|
||||||
JsonFormat(ApiUser(account))
|
|
||||||
} getOrElse Unauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get("/:userName/_edit")(oneselfOnly {
|
get("/:userName/_edit")(oneselfOnly {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
getAccountByUserName(userName).map { x =>
|
getAccountByUserName(userName).map { x =>
|
||||||
html.edit(x, flash.get("info"))
|
html.edit(x, flash.get("info"), flash.get("error"))
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -201,7 +178,11 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
get("/:userName/_delete")(oneselfOnly {
|
get("/:userName/_delete")(oneselfOnly {
|
||||||
val userName = params("userName")
|
val userName = params("userName")
|
||||||
|
|
||||||
getAccountByUserName(userName, true).foreach { account =>
|
getAccountByUserName(userName, true).map { account =>
|
||||||
|
if(isLastAdministrator(account)){
|
||||||
|
flash += "error" -> "Account can't be removed because this is last one administrator."
|
||||||
|
redirect(s"/${userName}/_edit")
|
||||||
|
} else {
|
||||||
// // Remove repositories
|
// // Remove repositories
|
||||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||||
// deleteRepository(userName, repositoryName)
|
// deleteRepository(userName, repositoryName)
|
||||||
@@ -210,14 +191,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||||
// }
|
// }
|
||||||
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
// removeUserRelatedData(userName)
|
removeUserRelatedData(userName)
|
||||||
|
updateAccount(account.copy(isRemoved = true))
|
||||||
removeUserRelatedData(userName)
|
session.invalidate
|
||||||
updateAccount(account.copy(isRemoved = true))
|
redirect("/")
|
||||||
}
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
session.invalidate
|
|
||||||
redirect("/")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
get("/:userName/_ssh")(oneselfOnly {
|
get("/:userName/_ssh")(oneselfOnly {
|
||||||
@@ -367,7 +346,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
post("/new", newRepositoryForm)(usersOnly { form =>
|
post("/new", newRepositoryForm)(usersOnly { form =>
|
||||||
LockUtil.lock(s"${form.owner}/${form.name}"){
|
LockUtil.lock(s"${form.owner}/${form.name}"){
|
||||||
if(getRepository(form.owner, form.name).isEmpty){
|
if(getRepository(form.owner, form.name).isEmpty){
|
||||||
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
createRepository(context.loginAccount.get, form.owner, form.name, form.description, form.isPrivate, form.createReadme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to the repository
|
// redirect to the repository
|
||||||
@@ -375,54 +354,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Create user repository
|
|
||||||
* https://developer.github.com/v3/repos/#create
|
|
||||||
*/
|
|
||||||
post("/api/v3/user/repos")(usersOnly {
|
|
||||||
val owner = context.loginAccount.get.userName
|
|
||||||
(for {
|
|
||||||
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(s"${owner}/${data.name}") {
|
|
||||||
if(getRepository(owner, data.name).isEmpty){
|
|
||||||
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
|
|
||||||
val repository = getRepository(owner, data.name).get
|
|
||||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
|
||||||
} else {
|
|
||||||
ApiError(
|
|
||||||
"A repository with this name already exists on this account",
|
|
||||||
Some("https://developer.github.com/v3/repos/#create")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create group repository
|
|
||||||
* https://developer.github.com/v3/repos/#create
|
|
||||||
*/
|
|
||||||
post("/api/v3/orgs/:org/repos")(managersOnly {
|
|
||||||
val groupName = params("org")
|
|
||||||
(for {
|
|
||||||
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(s"${groupName}/${data.name}") {
|
|
||||||
if(getRepository(groupName, data.name).isEmpty){
|
|
||||||
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
|
|
||||||
val repository = getRepository(groupName, data.name).get
|
|
||||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
|
||||||
} else {
|
|
||||||
ApiError(
|
|
||||||
"A repository with this name already exists for this group",
|
|
||||||
Some("https://developer.github.com/v3/repos/#create")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
|
||||||
val loginAccount = context.loginAccount.get
|
val loginAccount = context.loginAccount.get
|
||||||
val loginUserName = loginAccount.userName
|
val loginUserName = loginAccount.userName
|
||||||
@@ -456,7 +387,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
|
||||||
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
val originRepositoryName = repository.repository.originRepositoryName.getOrElse(repository.name)
|
||||||
|
|
||||||
createRepository(
|
insertRepository(
|
||||||
repositoryName = repository.name,
|
repositoryName = repository.name,
|
||||||
userName = accountName,
|
userName = accountName,
|
||||||
description = repository.repository.description,
|
description = repository.repository.description,
|
||||||
@@ -496,68 +427,6 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
|
|
||||||
val ownerAccount = getAccountByUserName(owner).get
|
|
||||||
val loginAccount = context.loginAccount.get
|
|
||||||
val loginUserName = loginAccount.userName
|
|
||||||
|
|
||||||
// Insert to the database at first
|
|
||||||
createRepository(name, owner, description, isPrivate)
|
|
||||||
|
|
||||||
// Add collaborators for group repository
|
|
||||||
if(ownerAccount.isGroupAccount){
|
|
||||||
getGroupMembers(owner).foreach { member =>
|
|
||||||
addCollaborator(owner, name, member.userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert default labels
|
|
||||||
insertDefaultLabels(owner, name)
|
|
||||||
|
|
||||||
// Create the actual repository
|
|
||||||
val gitdir = getRepositoryDir(owner, name)
|
|
||||||
JGitUtil.initRepository(gitdir)
|
|
||||||
|
|
||||||
if(createReadme){
|
|
||||||
using(Git.open(gitdir)){ git =>
|
|
||||||
val builder = DirCache.newInCore.builder()
|
|
||||||
val inserter = git.getRepository.newObjectInserter()
|
|
||||||
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
|
||||||
val content = if(description.nonEmpty){
|
|
||||||
name + "\n" +
|
|
||||||
"===============\n" +
|
|
||||||
"\n" +
|
|
||||||
description.get
|
|
||||||
} else {
|
|
||||||
name + "\n" +
|
|
||||||
"===============\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
|
||||||
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
|
||||||
builder.finish()
|
|
||||||
|
|
||||||
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
|
||||||
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Wiki repository
|
|
||||||
createWikiRepository(loginAccount, owner, name)
|
|
||||||
|
|
||||||
// Record activity
|
|
||||||
recordCreateRepositoryActivity(owner, name, loginUserName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
|
|
||||||
createLabel(userName, repositoryName, "bug", "fc2929")
|
|
||||||
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
|
||||||
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
|
||||||
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
|
||||||
createLabel(userName, repositoryName, "question", "cc317c")
|
|
||||||
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
|
||||||
}
|
|
||||||
|
|
||||||
private def existsAccount: Constraint = new Constraint(){
|
private def existsAccount: Constraint = new Constraint(){
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
if(getAccountByUserName(value).isEmpty) Some("User or group does not exist.") else None
|
||||||
@@ -580,8 +449,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
|
|||||||
|
|
||||||
private def validPublicKey: Constraint = new Constraint(){
|
private def validPublicKey: Constraint = new Constraint(){
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
|
override def validate(name: String, value: String, messages: Messages): Option[String] = SshUtil.str2PublicKey(value) match {
|
||||||
case Some(_) => None
|
case Some(_) if !getAllKeys().exists(_.publicKey == value) => None
|
||||||
case None => Some("Key is invalid.")
|
case _ => Some("Key is invalid.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
401
src/main/scala/gitbucket/core/controller/ApiController.scala
Normal file
401
src/main/scala/gitbucket/core/controller/ApiController.scala
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.api._
|
||||||
|
import gitbucket.core.model._
|
||||||
|
import gitbucket.core.service.IssuesService.IssueSearchCondition
|
||||||
|
import gitbucket.core.service.PullRequestService._
|
||||||
|
import gitbucket.core.service._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
import gitbucket.core.util._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.scalatra.{NoContent, UnprocessableEntity, Created}
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
|
class ApiController extends ApiControllerBase
|
||||||
|
with RepositoryService
|
||||||
|
with AccountService
|
||||||
|
with ProtectedBranchService
|
||||||
|
with IssuesService
|
||||||
|
with LabelsService
|
||||||
|
with PullRequestService
|
||||||
|
with CommitStatusService
|
||||||
|
with RepositoryCreationService
|
||||||
|
with HandleCommentService
|
||||||
|
with WebHookService
|
||||||
|
with WebHookPullRequestService
|
||||||
|
with WebHookIssueCommentService
|
||||||
|
with WikiService
|
||||||
|
with ActivityService
|
||||||
|
with OwnerAuthenticator
|
||||||
|
with UsersAuthenticator
|
||||||
|
with GroupManagerAuthenticator
|
||||||
|
with ReferrerAuthenticator
|
||||||
|
with ReadableUsersAuthenticator
|
||||||
|
with CollaboratorsAuthenticator
|
||||||
|
|
||||||
|
trait ApiControllerBase extends ControllerBase {
|
||||||
|
self: RepositoryService
|
||||||
|
with AccountService
|
||||||
|
with ProtectedBranchService
|
||||||
|
with IssuesService
|
||||||
|
with LabelsService
|
||||||
|
with PullRequestService
|
||||||
|
with CommitStatusService
|
||||||
|
with RepositoryCreationService
|
||||||
|
with HandleCommentService
|
||||||
|
with OwnerAuthenticator
|
||||||
|
with UsersAuthenticator
|
||||||
|
with GroupManagerAuthenticator
|
||||||
|
with ReferrerAuthenticator
|
||||||
|
with ReadableUsersAuthenticator
|
||||||
|
with CollaboratorsAuthenticator =>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/users/#get-a-single-user
|
||||||
|
*/
|
||||||
|
get("/api/v3/users/:userName") {
|
||||||
|
getAccountByUserName(params("userName")).map { account =>
|
||||||
|
JsonFormat(ApiUser(account))
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||||
|
*/
|
||||||
|
get("/api/v3/user") {
|
||||||
|
context.loginAccount.map { account =>
|
||||||
|
JsonFormat(ApiUser(account))
|
||||||
|
} getOrElse Unauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create user repository
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
*/
|
||||||
|
post("/api/v3/user/repos")(usersOnly {
|
||||||
|
val owner = context.loginAccount.get.userName
|
||||||
|
(for {
|
||||||
|
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(s"${owner}/${data.name}") {
|
||||||
|
if(getRepository(owner, data.name).isEmpty){
|
||||||
|
createRepository(context.loginAccount.get, owner, data.name, data.description, data.`private`, data.auto_init)
|
||||||
|
val repository = getRepository(owner, data.name).get
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
|
||||||
|
} else {
|
||||||
|
ApiError(
|
||||||
|
"A repository with this name already exists on this account",
|
||||||
|
Some("https://developer.github.com/v3/repos/#create")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create group repository
|
||||||
|
* https://developer.github.com/v3/repos/#create
|
||||||
|
*/
|
||||||
|
post("/api/v3/orgs/:org/repos")(managersOnly {
|
||||||
|
val groupName = params("org")
|
||||||
|
(for {
|
||||||
|
data <- extractFromJsonBody[CreateARepository] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(s"${groupName}/${data.name}") {
|
||||||
|
if(getRepository(groupName, data.name).isEmpty){
|
||||||
|
createRepository(context.loginAccount.get, groupName, data.name, data.description, data.`private`, data.auto_init)
|
||||||
|
val repository = getRepository(groupName, data.name).get
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
|
||||||
|
} else {
|
||||||
|
ApiError(
|
||||||
|
"A repository with this name already exists for this group",
|
||||||
|
Some("https://developer.github.com/v3/repos/#create")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
|
||||||
|
*/
|
||||||
|
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
|
||||||
|
import gitbucket.core.api._
|
||||||
|
(for{
|
||||||
|
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
|
||||||
|
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
|
||||||
|
} yield {
|
||||||
|
if(protection.enabled){
|
||||||
|
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
|
||||||
|
} else {
|
||||||
|
disableBranchProtection(repository.owner, repository.name, branch)
|
||||||
|
}
|
||||||
|
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
|
||||||
|
* but not enabled.
|
||||||
|
*/
|
||||||
|
get("/api/v3/rate_limit"){
|
||||||
|
contentType = formats("json")
|
||||||
|
// this message is same as github enterprise...
|
||||||
|
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
||||||
|
}).getOrElse(NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
issue <- getIssue(repository.owner, repository.name, issueId.toString)
|
||||||
|
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
|
||||||
|
action = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||||
|
(issue, id) <- handleComment(issue, Some(body), repository, action)
|
||||||
|
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all labels for this repository
|
||||||
|
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
|
||||||
|
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
|
||||||
|
ApiLabel(label, RepositoryName(repository))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#get-a-single-label
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
|
||||||
|
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||||
|
JsonFormat(ApiLabel(label, RepositoryName(repository)))
|
||||||
|
} getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#create-a-label
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||||
|
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
||||||
|
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
|
||||||
|
getLabel(repository.owner, repository.name, labelId).map { label =>
|
||||||
|
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
|
||||||
|
} getOrElse NotFound()
|
||||||
|
} else {
|
||||||
|
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
||||||
|
UnprocessableEntity(ApiError(
|
||||||
|
"Validation Failed",
|
||||||
|
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#update-a-label
|
||||||
|
*/
|
||||||
|
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
||||||
|
} yield {
|
||||||
|
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||||
|
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||||
|
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
||||||
|
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
|
||||||
|
JsonFormat(ApiLabel(
|
||||||
|
getLabel(repository.owner, repository.name, label.labelId).get,
|
||||||
|
RepositoryName(repository)))
|
||||||
|
} else {
|
||||||
|
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
||||||
|
UnprocessableEntity(ApiError(
|
||||||
|
"Validation Failed",
|
||||||
|
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
|
||||||
|
}
|
||||||
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
|
}) getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a label
|
||||||
|
* https://developer.github.com/v3/issues/labels/#delete-a-label
|
||||||
|
*/
|
||||||
|
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
||||||
|
LockUtil.lock(RepositoryName(repository).fullName) {
|
||||||
|
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
||||||
|
deleteLabel(repository.owner, repository.name, label.labelId)
|
||||||
|
NoContent()
|
||||||
|
} getOrElse NotFound()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#list-pull-requests
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||||
|
val page = IssueSearchCondition.page(request)
|
||||||
|
// TODO: more api spec condition
|
||||||
|
val condition = IssueSearchCondition(request)
|
||||||
|
val baseOwner = getAccountByUserName(repository.owner).get
|
||||||
|
|
||||||
|
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account)] =
|
||||||
|
searchPullRequestByApi(
|
||||||
|
condition = condition,
|
||||||
|
offset = (page - 1) * PullRequestLimit,
|
||||||
|
limit = PullRequestLimit,
|
||||||
|
repos = repository.owner -> repository.name
|
||||||
|
)
|
||||||
|
|
||||||
|
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
|
||||||
|
ApiPullRequest(
|
||||||
|
issue,
|
||||||
|
pullRequest,
|
||||||
|
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||||
|
ApiRepository(repository, ApiUser(baseOwner)),
|
||||||
|
ApiUser(issueUser)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
issueId <- params("id").toIntOpt
|
||||||
|
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
||||||
|
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
|
||||||
|
baseOwner <- users.get(repository.owner)
|
||||||
|
headOwner <- users.get(pullRequest.requestUserName)
|
||||||
|
issueUser <- users.get(issue.openedUserName)
|
||||||
|
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiPullRequest(
|
||||||
|
issue,
|
||||||
|
pullRequest,
|
||||||
|
ApiRepository(headRepo, ApiUser(headOwner)),
|
||||||
|
ApiRepository(repository, ApiUser(baseOwner)),
|
||||||
|
ApiUser(issueUser)))
|
||||||
|
}).getOrElse(NotFound)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
|
||||||
|
val owner = repository.owner
|
||||||
|
val name = repository.name
|
||||||
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
|
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
||||||
|
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
||||||
|
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
|
||||||
|
val newId = git.getRepository.resolve(pullreq.commitIdTo)
|
||||||
|
val repoFullName = RepositoryName(repository)
|
||||||
|
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
|
||||||
|
JsonFormat(commits)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/#get
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
|
||||||
|
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
||||||
|
*/
|
||||||
|
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("sha")
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
data <- extractFromJsonBody[CreateAStatus] if data.isValid
|
||||||
|
creator <- context.loginAccount
|
||||||
|
state <- CommitState.valueOf(data.state)
|
||||||
|
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
|
||||||
|
state, data.target_url, data.description, new java.util.Date(), creator)
|
||||||
|
status <- getCommitStatus(repository.owner, repository.name, statusId)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||||
|
*/
|
||||||
|
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("ref")
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
} yield {
|
||||||
|
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
|
||||||
|
ApiCommitStatus(status, ApiUser(creator))
|
||||||
|
})
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* legacy route
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
|
||||||
|
listStatusesRoute.action()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
||||||
|
*
|
||||||
|
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
||||||
|
*/
|
||||||
|
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
|
||||||
|
(for{
|
||||||
|
ref <- params.get("ref")
|
||||||
|
owner <- getAccountByUserName(repository.owner)
|
||||||
|
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
||||||
|
} yield {
|
||||||
|
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
||||||
|
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
||||||
|
}) getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
|
||||||
|
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -83,9 +83,9 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
|
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
|
||||||
|
|
||||||
filter match {
|
filter match {
|
||||||
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
|
case "assigned" => condition.copy(assigned = Some(Some(userName)), author = None, mentioned = None)
|
||||||
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
|
case "mentioned" => condition.copy(assigned = None, author = None, mentioned = Some(userName))
|
||||||
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
|
case _ => condition.copy(assigned = None, author = Some(userName), mentioned = None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +103,14 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
||||||
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
||||||
filter match {
|
filter match {
|
||||||
case "assigned" => condition.copy(assigned = Some(userName))
|
case "assigned" => condition.copy(assigned = Some(Some(userName)))
|
||||||
case "mentioned" => condition.copy(mentioned = Some(userName))
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
case _ => condition.copy(author = Some(userName))
|
case _ => condition.copy(author = Some(userName))
|
||||||
},
|
},
|
||||||
filter,
|
filter,
|
||||||
getGroupNames(userName))
|
getGroupNames(userName),
|
||||||
|
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
|
||||||
|
getUserRepositories(userName, withoutPhysicalInfo = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchPullRequests(filter: String) = {
|
private def searchPullRequests(filter: String) = {
|
||||||
@@ -126,12 +128,14 @@ trait DashboardControllerBase extends ControllerBase {
|
|||||||
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
||||||
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
||||||
filter match {
|
filter match {
|
||||||
case "assigned" => condition.copy(assigned = Some(userName))
|
case "assigned" => condition.copy(assigned = Some(Some(userName)))
|
||||||
case "mentioned" => condition.copy(mentioned = Some(userName))
|
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||||
case _ => condition.copy(author = Some(userName))
|
case _ => condition.copy(author = Some(userName))
|
||||||
},
|
},
|
||||||
filter,
|
filter,
|
||||||
getGroupNames(userName))
|
getGroupNames(userName),
|
||||||
|
getVisibleRepositories(context.loginAccount, withoutPhysicalInfo = true),
|
||||||
|
getUserRepositories(userName, withoutPhysicalInfo = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.util.{Keys, FileUtil}
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.{AccountService, RepositoryService}
|
||||||
|
import gitbucket.core.servlet.Database
|
||||||
|
import gitbucket.core.util._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
|
import org.scalatra
|
||||||
import org.scalatra._
|
import org.scalatra._
|
||||||
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides Ajax based file upload functionality.
|
* Provides Ajax based file upload functionality.
|
||||||
*
|
*
|
||||||
* This servlet saves uploaded file.
|
* This servlet saves uploaded file.
|
||||||
*/
|
*/
|
||||||
class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
class FileUploadController extends ScalatraServlet with FileUploadSupport with RepositoryService with AccountService {
|
||||||
|
|
||||||
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
|
||||||
|
|
||||||
@@ -31,6 +39,69 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
|
|||||||
}, FileUtil.isUploadableType)
|
}, FileUtil.isUploadableType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
post("/wiki/:owner/:repository"){
|
||||||
|
// Don't accept not logged-in users
|
||||||
|
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account =>
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
|
||||||
|
// Check whether logged-in user is collaborator
|
||||||
|
collaboratorsOnly(owner, repository, loginAccount){
|
||||||
|
execute({ (file, fileId) =>
|
||||||
|
val fileName = file.getName
|
||||||
|
LockUtil.lock(s"${owner}/${repository}/wiki") {
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))) { git =>
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
|
||||||
|
if(headId != null){
|
||||||
|
JGitUtil.processTree(git, headId){ (path, tree) =>
|
||||||
|
if(path != fileName){
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bytes = IOUtils.toByteArray(file.getInputStream)
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry(fileName, FileMode.REGULAR_FILE, inserter.insert(Constants.OBJ_BLOB, bytes)))
|
||||||
|
builder.finish()
|
||||||
|
|
||||||
|
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||||
|
Constants.HEAD, loginAccount.userName, loginAccount.mailAddress, s"Uploaded ${fileName}")
|
||||||
|
|
||||||
|
fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, FileUtil.isUploadableType)
|
||||||
|
}
|
||||||
|
} getOrElse BadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/import") {
|
||||||
|
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
|
||||||
|
execute({ (file, fileId) =>
|
||||||
|
if(file.getName.endsWith(".xml")){
|
||||||
|
import JDBCUtil._
|
||||||
|
val conn = request2Session(request).conn
|
||||||
|
conn.importAsXML(file.getInputStream)
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Import is available for only the XML file.")
|
||||||
|
}
|
||||||
|
}, _ => true)
|
||||||
|
}
|
||||||
|
redirect("/admin/data")
|
||||||
|
}
|
||||||
|
|
||||||
|
private def collaboratorsOnly(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = {
|
||||||
|
implicit val session = Database.getSession(request)
|
||||||
|
loginAccount match {
|
||||||
|
case x if(x.isAdmin) => action
|
||||||
|
case x if(getCollaborators(owner, repository).contains(x.userName)) => action
|
||||||
|
case _ => BadRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
|
||||||
case Some(file) if(mimeTypeChcker(file.name)) =>
|
case Some(file) if(mimeTypeChcker(file.name)) =>
|
||||||
defining(FileUtil.generateFileId){ fileId =>
|
defining(FileUtil.generateFileId){ fileId =>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.helper.xml
|
import gitbucket.core.helper.xml
|
||||||
import gitbucket.core.model.Account
|
import gitbucket.core.model.Account
|
||||||
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService}
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
|
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
|
||||||
@@ -118,19 +117,11 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
* JSON API for checking user existence.
|
* JSON API for checking user existence.
|
||||||
*/
|
*/
|
||||||
post("/_user/existence")(usersOnly {
|
post("/_user/existence")(usersOnly {
|
||||||
getAccountByUserName(params("userName")).isDefined
|
getAccountByUserName(params("userName")).map { account =>
|
||||||
|
if(params.get("userOnly").isDefined) !account.isGroupAccount else true
|
||||||
|
} getOrElse false
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
|
|
||||||
* but not enabled.
|
|
||||||
*/
|
|
||||||
get("/api/v3/rate_limit"){
|
|
||||||
contentType = formats("json")
|
|
||||||
// this message is same as github enterprise...
|
|
||||||
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Move to RepositoryViwerController?
|
// TODO Move to RepositoryViwerController?
|
||||||
post("/search", searchForm){ form =>
|
post("/search", searchForm){ form =>
|
||||||
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
|
||||||
@@ -148,13 +139,21 @@ trait IndexControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
target.toLowerCase match {
|
target.toLowerCase match {
|
||||||
case "issue" => gitbucket.core.search.html.issues(
|
case "issue" => gitbucket.core.search.html.issues(
|
||||||
searchIssues(repository.owner, repository.name, query),
|
|
||||||
countFiles(repository.owner, repository.name, query),
|
countFiles(repository.owner, repository.name, query),
|
||||||
|
searchIssues(repository.owner, repository.name, query),
|
||||||
|
countWikiPages(repository.owner, repository.name, query),
|
||||||
|
query, page, repository)
|
||||||
|
|
||||||
|
case "wiki" => gitbucket.core.search.html.wiki(
|
||||||
|
countFiles(repository.owner, repository.name, query),
|
||||||
|
countIssues(repository.owner, repository.name, query),
|
||||||
|
searchWikiPages(repository.owner, repository.name, query),
|
||||||
query, page, repository)
|
query, page, repository)
|
||||||
|
|
||||||
case _ => gitbucket.core.search.html.code(
|
case _ => gitbucket.core.search.html.code(
|
||||||
searchFiles(repository.owner, repository.name, query),
|
searchFiles(repository.owner, repository.name, query),
|
||||||
countIssues(repository.owner, repository.name, query),
|
countIssues(repository.owner, repository.name, query),
|
||||||
|
countWikiPages(repository.owner, repository.name, query),
|
||||||
query, page, repository)
|
query, page, repository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.issues.html
|
import gitbucket.core.issues.html
|
||||||
import gitbucket.core.model.Issue
|
|
||||||
import gitbucket.core.service.IssuesService._
|
import gitbucket.core.service.IssuesService._
|
||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
@@ -16,11 +14,11 @@ import org.scalatra.Ok
|
|||||||
|
|
||||||
|
|
||||||
class IssuesController extends IssuesControllerBase
|
class IssuesController extends IssuesControllerBase
|
||||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
|
||||||
|
|
||||||
trait IssuesControllerBase extends ControllerBase {
|
trait IssuesControllerBase extends ControllerBase {
|
||||||
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with HandleCommentService
|
||||||
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
|
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
|
||||||
|
|
||||||
case class IssueCreateForm(title: String, content: Option[String],
|
case class IssueCreateForm(title: String, content: Option[String],
|
||||||
@@ -78,18 +76,6 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
issueId <- params("id").toIntOpt
|
|
||||||
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
|
|
||||||
}).getOrElse(NotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
|
||||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
html.create(
|
html.create(
|
||||||
@@ -128,7 +114,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
getIssue(owner, name, issueId.toString).foreach { issue =>
|
getIssue(owner, name, issueId.toString).foreach { issue =>
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
|
||||||
|
|
||||||
// call web hooks
|
// call web hooks
|
||||||
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
|
||||||
@@ -150,7 +136,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
// update issue
|
// update issue
|
||||||
updateIssue(owner, name, issue.issueId, title, issue.content)
|
updateIssue(owner, name, issue.issueId, title, issue.content)
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue.copy(title = title), title)
|
createReferComment(owner, name, issue.copy(title = title), title, context.loginAccount.get)
|
||||||
|
|
||||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -165,7 +151,7 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
// update issue
|
// update issue
|
||||||
updateIssue(owner, name, issue.issueId, issue.title, content)
|
updateIssue(owner, name, issue.issueId, issue.title, content)
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue, content.getOrElse(""))
|
createReferComment(owner, name, issue, content.getOrElse(""), context.loginAccount.get)
|
||||||
|
|
||||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||||
} else Unauthorized
|
} else Unauthorized
|
||||||
@@ -174,30 +160,22 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) =>
|
||||||
handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) =>
|
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/${
|
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
handleComment(issue, Some(form.content), repository, actionOpt) map { case (issue, id) =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/issues/comments/#create-a-comment
|
|
||||||
*/
|
|
||||||
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
issueId <- params("id").toIntOpt
|
|
||||||
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
|
|
||||||
(issue, id) <- handleComment(issueId, Some(body), repository)()
|
|
||||||
issueComment <- getComment(repository.owner, repository.name, id.toString())
|
|
||||||
} yield {
|
|
||||||
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get), issue.isPullRequest))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
|
||||||
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
|
getIssue(repository.owner, repository.name, form.issueId.toString).flatMap { issue =>
|
||||||
redirect(s"/${repository.owner}/${repository.name}/${
|
val actionOpt = params.get("action").filter(_ => isEditable(issue.userName, issue.repositoryName, issue.openedUserName))
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
handleComment(issue, form.content, repository, actionOpt) map { case (issue, id) =>
|
||||||
|
redirect(s"/${repository.owner}/${repository.name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}")
|
||||||
|
}
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -315,8 +293,16 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||||
defining(params.get("value")){ action =>
|
defining(params.get("value")){ action =>
|
||||||
action match {
|
action match {
|
||||||
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
|
case Some("open") => executeBatch(repository) { issueId =>
|
||||||
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
|
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||||
|
handleComment(issue, None, repository, Some("reopen"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Some("close") => executeBatch(repository) { issueId =>
|
||||||
|
getIssue(repository.owner, repository.name, issueId.toString).foreach { issue =>
|
||||||
|
handleComment(issue, None, repository, Some("close"))
|
||||||
|
}
|
||||||
|
}
|
||||||
case _ => // TODO BadRequest
|
case _ => // TODO BadRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,99 +359,6 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Same method exists in PullRequestController. Should it moved to IssueService?
|
|
||||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
|
||||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
|
||||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
// Not add if refer comment already exist.
|
|
||||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
|
||||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
|
||||||
*/
|
|
||||||
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
|
|
||||||
(getAction: Issue => Option[String] =
|
|
||||||
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
|
|
||||||
|
|
||||||
defining(repository.owner, repository.name){ case (owner, name) =>
|
|
||||||
val userName = context.loginAccount.get.userName
|
|
||||||
|
|
||||||
getIssue(owner, name, issueId.toString) flatMap { issue =>
|
|
||||||
val (action, recordActivity) =
|
|
||||||
getAction(issue)
|
|
||||||
.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(recordReopenIssueActivity _))
|
|
||||||
}
|
|
||||||
.map { case (closed, t) =>
|
|
||||||
updateClosed(owner, name, issueId, closed)
|
|
||||||
t
|
|
||||||
}
|
|
||||||
.getOrElse(None -> None)
|
|
||||||
|
|
||||||
val commentId = (content, action) match {
|
|
||||||
case (None, None) => None
|
|
||||||
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
|
|
||||||
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
|
||||||
}
|
|
||||||
|
|
||||||
// record comment activity if comment is entered
|
|
||||||
content foreach {
|
|
||||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
|
||||||
(owner, name, userName, issueId, _)
|
|
||||||
}
|
|
||||||
recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) )
|
|
||||||
|
|
||||||
// extract references and create refer comment
|
|
||||||
content.map { content =>
|
|
||||||
createReferComment(owner, name, issue, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// call web hooks
|
|
||||||
action match {
|
|
||||||
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
|
||||||
case Some(act) => val webHookAction = act match {
|
|
||||||
case "open" => "opened"
|
|
||||||
case "reopen" => "reopened"
|
|
||||||
case "close" => "closed"
|
|
||||||
case _ => act
|
|
||||||
}
|
|
||||||
if(issue.isPullRequest){
|
|
||||||
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
|
||||||
} else {
|
|
||||||
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// notifications
|
|
||||||
Notifier() match {
|
|
||||||
case f =>
|
|
||||||
content foreach {
|
|
||||||
f.toNotify(repository, issue, _){
|
|
||||||
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
|
||||||
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
action foreach {
|
|
||||||
f.toNotify(repository, issue, _){
|
|
||||||
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
commentId.map( issue -> _ )
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
|
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
|
||||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||||
val page = IssueSearchCondition.page(request)
|
val page = IssueSearchCondition.page(request)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
|
|
||||||
import gitbucket.core.issues.labels.html
|
import gitbucket.core.issues.labels.html
|
||||||
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
|
||||||
import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator}
|
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
|
import org.scalatra.Ok
|
||||||
|
|
||||||
class LabelsController extends LabelsControllerBase
|
class LabelsController extends LabelsControllerBase
|
||||||
with LabelsService with IssuesService with RepositoryService with AccountService
|
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||||
@@ -24,6 +23,7 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
"labelColor" -> trim(label("Color", text(required, color)))
|
"labelColor" -> trim(label("Color", text(required, color)))
|
||||||
)(LabelForm.apply)
|
)(LabelForm.apply)
|
||||||
|
|
||||||
|
|
||||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||||
html.list(
|
html.list(
|
||||||
getLabels(repository.owner, repository.name),
|
getLabels(repository.owner, repository.name),
|
||||||
@@ -32,26 +32,6 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* List all labels for this repository
|
|
||||||
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
|
|
||||||
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
|
|
||||||
ApiLabel(label, RepositoryName(repository))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#get-a-single-label
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
|
|
||||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
|
||||||
JsonFormat(ApiLabel(label, RepositoryName(repository)))
|
|
||||||
} getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
||||||
html.edit(None, repository)
|
html.edit(None, repository)
|
||||||
})
|
})
|
||||||
@@ -66,31 +46,6 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#create-a-label
|
|
||||||
*/
|
|
||||||
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
|
|
||||||
(for{
|
|
||||||
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
|
||||||
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
|
||||||
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
|
|
||||||
getLabel(repository.owner, repository.name, labelId).map { label =>
|
|
||||||
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
|
|
||||||
} getOrElse NotFound()
|
|
||||||
} else {
|
|
||||||
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
|
||||||
UnprocessableEntity(ApiError(
|
|
||||||
"Validation Failed",
|
|
||||||
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
||||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||||
html.edit(Some(label), repository)
|
html.edit(Some(label), repository)
|
||||||
@@ -107,50 +62,11 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#update-a-label
|
|
||||||
*/
|
|
||||||
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
|
||||||
(for{
|
|
||||||
data <- extractFromJsonBody[CreateALabel] if data.isValid
|
|
||||||
} yield {
|
|
||||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
|
||||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
|
||||||
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
|
|
||||||
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
|
|
||||||
JsonFormat(ApiLabel(
|
|
||||||
getLabel(repository.owner, repository.name, label.labelId).get,
|
|
||||||
RepositoryName(repository)))
|
|
||||||
} else {
|
|
||||||
// TODO ApiError should support errors field to enhance compatibility of GitHub API
|
|
||||||
UnprocessableEntity(ApiError(
|
|
||||||
"Validation Failed",
|
|
||||||
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
|
|
||||||
}
|
|
||||||
} getOrElse NotFound()
|
|
||||||
}
|
|
||||||
}) getOrElse NotFound()
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
||||||
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
||||||
Ok()
|
Ok()
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a label
|
|
||||||
* https://developer.github.com/v3/issues/labels/#delete-a-label
|
|
||||||
*/
|
|
||||||
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
|
|
||||||
LockUtil.lock(RepositoryName(repository).fullName) {
|
|
||||||
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
|
|
||||||
deleteLabel(repository.owner, repository.name, label.labelId)
|
|
||||||
NoContent()
|
|
||||||
} getOrElse NotFound()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constraint for the identifier such as user name, repository name or page name.
|
* Constraint for the identifier such as user name, repository name or page name.
|
||||||
*/
|
*/
|
||||||
@@ -169,7 +85,11 @@ trait LabelsControllerBase extends ControllerBase {
|
|||||||
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
||||||
val owner = params("owner")
|
val owner = params("owner")
|
||||||
val repository = params("repository")
|
val repository = params("repository")
|
||||||
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
|
params.get("labelId").map { labelId =>
|
||||||
|
getLabel(owner, repository, value).filter(_.labelId != labelId.toInt).map(_ => "Name has already been taken.")
|
||||||
|
}.getOrElse {
|
||||||
|
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
import gitbucket.core.api._
|
import gitbucket.core.model.WebHook
|
||||||
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
|
|
||||||
import gitbucket.core.pulls.html
|
import gitbucket.core.pulls.html
|
||||||
import gitbucket.core.service.CommitStatusService
|
import gitbucket.core.service.CommitStatusService
|
||||||
import gitbucket.core.service.MergeService
|
import gitbucket.core.service.MergeService
|
||||||
@@ -82,24 +81,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/pulls/#list-pull-requests
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
|
|
||||||
val page = IssueSearchCondition.page(request)
|
|
||||||
// TODO: more api spec condition
|
|
||||||
val condition = IssueSearchCondition(request)
|
|
||||||
val baseOwner = getAccountByUserName(repository.owner).get
|
|
||||||
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
|
|
||||||
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
|
|
||||||
ApiPullRequest(
|
|
||||||
issue,
|
|
||||||
pullRequest,
|
|
||||||
ApiRepository(headRepo, ApiUser(headOwner)),
|
|
||||||
ApiRepository(repository, ApiUser(baseOwner)),
|
|
||||||
ApiUser(issueUser)) })
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
@@ -126,47 +107,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
issueId <- params("id").toIntOpt
|
|
||||||
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
|
|
||||||
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
|
|
||||||
baseOwner <- users.get(repository.owner)
|
|
||||||
headOwner <- users.get(pullRequest.requestUserName)
|
|
||||||
issueUser <- users.get(issue.openedUserName)
|
|
||||||
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(ApiPullRequest(
|
|
||||||
issue,
|
|
||||||
pullRequest,
|
|
||||||
ApiRepository(headRepo, ApiUser(headOwner)),
|
|
||||||
ApiRepository(repository, ApiUser(baseOwner)),
|
|
||||||
ApiUser(issueUser)))
|
|
||||||
}).getOrElse(NotFound)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
|
|
||||||
val owner = repository.owner
|
|
||||||
val name = repository.name
|
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
|
||||||
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
|
|
||||||
using(Git.open(getRepositoryDir(owner, name))){ git =>
|
|
||||||
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
|
|
||||||
val newId = git.getRepository.resolve(pullreq.commitIdTo)
|
|
||||||
val repoFullName = RepositoryName(repository)
|
|
||||||
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
|
|
||||||
JsonFormat(commits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
|
||||||
params("id").toIntOpt.flatMap{ issueId =>
|
params("id").toIntOpt.flatMap{ issueId =>
|
||||||
val owner = repository.owner
|
val owner = repository.owner
|
||||||
@@ -238,7 +178,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
|
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
|
||||||
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
|
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
|
||||||
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
s"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
|
||||||
case None => // conflict
|
case None => // conflict
|
||||||
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
|
||||||
case Some(oldId) =>
|
case Some(oldId) =>
|
||||||
@@ -262,7 +202,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
// close issue by commit message
|
// close issue by commit message
|
||||||
if(pullreq.requestBranch == repository.repository.defaultBranch){
|
if(pullreq.requestBranch == repository.repository.defaultBranch){
|
||||||
commits.map{ commit =>
|
commits.map { commit =>
|
||||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,10 +255,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
commits.flatten.foreach { commit =>
|
commits.flatten.foreach { commit =>
|
||||||
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
|
||||||
}
|
}
|
||||||
issue.content match {
|
closeIssuesFromMessage(issue.title + " " + issue.content.getOrElse(""), loginAccount.userName, owner, name)
|
||||||
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
|
|
||||||
case _ =>
|
|
||||||
}
|
|
||||||
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +351,15 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
originRepository.owner, originRepository.name, oldId.getName,
|
originRepository.owner, originRepository.name, oldId.getName,
|
||||||
forkedRepository.owner, forkedRepository.name, newId.getName)
|
forkedRepository.owner, forkedRepository.name, newId.getName)
|
||||||
|
|
||||||
|
val title = if(commits.flatten.length == 1){
|
||||||
|
commits.flatten.head.shortMessage
|
||||||
|
} else {
|
||||||
|
val text = forkedId.replaceAll("[\\-_]", " ")
|
||||||
|
text.substring(0, 1).toUpperCase + text.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
html.compare(
|
html.compare(
|
||||||
|
title,
|
||||||
commits,
|
commits,
|
||||||
diffs,
|
diffs,
|
||||||
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
|
||||||
@@ -523,7 +468,7 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
getIssue(owner, name, issueId.toString) foreach { issue =>
|
getIssue(owner, name, issueId.toString) foreach { issue =>
|
||||||
// extract references and create refer comment
|
// extract references and create refer comment
|
||||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""), context.loginAccount.get)
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
|
||||||
@@ -535,19 +480,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO Same method exists in IssueController. Should it moved to IssueService?
|
|
||||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
|
||||||
StringUtil.extractIssueId(message).foreach { issueId =>
|
|
||||||
val content = fromIssue.issueId + ":" + fromIssue.title
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
// Not add if refer comment already exist.
|
|
||||||
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
|
||||||
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses branch identifier and extracts owner and branch name as tuple.
|
* Parses branch identifier and extracts owner and branch name as tuple.
|
||||||
*
|
*
|
||||||
@@ -611,14 +543,4 @@ trait PullRequestsControllerBase extends ControllerBase {
|
|||||||
hasWritePermission(owner, repoName, context.loginAccount))
|
hasWritePermission(owner, repoName, context.loginAccount))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
|
|
||||||
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
|
|
||||||
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
|
||||||
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.scalatra.i18n.Messages
|
|||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
import org.eclipse.jgit.lib.Constants
|
import org.eclipse.jgit.lib.Constants
|
||||||
import org.eclipse.jgit.lib.ObjectId
|
import org.eclipse.jgit.lib.ObjectId
|
||||||
|
import gitbucket.core.model.WebHookContentType
|
||||||
|
|
||||||
|
|
||||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||||
@@ -49,12 +50,16 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
)(CollaboratorForm.apply)
|
)(CollaboratorForm.apply)
|
||||||
|
|
||||||
// for web hook url addition
|
// for web hook url addition
|
||||||
case class WebHookForm(url: String, events: Set[WebHook.Event])
|
case class WebHookForm(url: String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])
|
||||||
|
|
||||||
def webHookForm(update:Boolean) = mapping(
|
def webHookForm(update:Boolean) = mapping(
|
||||||
"url" -> trim(label("url", text(required, webHook(update)))),
|
"url" -> trim(label("url", text(required, webHook(update)))),
|
||||||
"events" -> webhookEvents
|
"events" -> webhookEvents,
|
||||||
)(WebHookForm.apply)
|
"ctype" -> label("ctype", text()),
|
||||||
|
"token" -> optional(trim(label("token", text(maxlength(100)))))
|
||||||
|
)(
|
||||||
|
(url, events, ctype, token) => WebHookForm(url, events, WebHookContentType.valueOf(ctype), token)
|
||||||
|
)
|
||||||
|
|
||||||
// for transfer ownership
|
// for transfer ownership
|
||||||
case class TransferOwnerShipForm(newOwner: String)
|
case class TransferOwnerShipForm(newOwner: String)
|
||||||
@@ -141,22 +146,6 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
|
|
||||||
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
|
|
||||||
import gitbucket.core.api._
|
|
||||||
(for{
|
|
||||||
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
|
|
||||||
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
|
|
||||||
} yield {
|
|
||||||
if(protection.enabled){
|
|
||||||
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
|
|
||||||
} else {
|
|
||||||
disableBranchProtection(repository.owner, repository.name, branch)
|
|
||||||
}
|
|
||||||
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the Collaborators page.
|
* Display the Collaborators page.
|
||||||
*/
|
*/
|
||||||
@@ -198,7 +187,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Display the web hook edit page.
|
* Display the web hook edit page.
|
||||||
*/
|
*/
|
||||||
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
|
||||||
val webhook = WebHook(repository.owner, repository.name, "")
|
val webhook = WebHook(repository.owner, repository.name, "", WebHookContentType.FORM, None)
|
||||||
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -206,7 +195,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Add the web hook URL.
|
* Add the web hook URL.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
|
||||||
addWebHook(repository.owner, repository.name, form.url, form.events)
|
addWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token)
|
||||||
flash += "info" -> s"Webhook ${form.url} created"
|
flash += "info" -> s"Webhook ${form.url} created"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
@@ -235,7 +224,9 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
import scala.concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
||||||
val url = params("url")
|
val url = params("url")
|
||||||
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
|
val token = Some(params("token"))
|
||||||
|
val ctype = WebHookContentType.valueOf(params("ctype"))
|
||||||
|
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url, ctype, token)
|
||||||
val dummyPayload = {
|
val dummyPayload = {
|
||||||
val ownerAccount = getAccountByUserName(repository.owner).get
|
val ownerAccount = getAccountByUserName(repository.owner).get
|
||||||
val commits = if(repository.commitCount == 0) List.empty else git.log
|
val commits = if(repository.commitCount == 0) List.empty else git.log
|
||||||
@@ -294,7 +285,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
|||||||
* Update web hook settings.
|
* Update web hook settings.
|
||||||
*/
|
*/
|
||||||
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
|
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
|
||||||
updateWebHook(repository.owner, repository.name, form.url, form.events)
|
updateWebHook(repository.owner, repository.name, form.url, form.events, form.ctype, form.token)
|
||||||
flash += "info" -> s"webhook ${form.url} updated"
|
flash += "info" -> s"webhook ${form.url} updated"
|
||||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package gitbucket.core.controller
|
|||||||
|
|
||||||
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
|
||||||
|
|
||||||
import gitbucket.core.api._
|
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
import gitbucket.core.repo.html
|
import gitbucket.core.repo.html
|
||||||
import gitbucket.core.helper
|
import gitbucket.core.helper
|
||||||
@@ -13,7 +12,7 @@ import gitbucket.core.util.StringUtil._
|
|||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.model.{Account, CommitState, WebHook}
|
import gitbucket.core.model.{Account, WebHook}
|
||||||
import gitbucket.core.service.WebHookService._
|
import gitbucket.core.service.WebHookService._
|
||||||
import gitbucket.core.view
|
import gitbucket.core.view
|
||||||
import gitbucket.core.view.helpers
|
import gitbucket.core.view.helpers
|
||||||
@@ -122,13 +121,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
fileList(_)
|
fileList(_)
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/#get
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
|
|
||||||
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the file list of the specified path and branch.
|
* Displays the file list of the specified path and branch.
|
||||||
*/
|
*/
|
||||||
@@ -160,65 +152,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#create-a-status
|
|
||||||
*/
|
|
||||||
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
|
|
||||||
(for{
|
|
||||||
ref <- params.get("sha")
|
|
||||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
|
||||||
data <- extractFromJsonBody[CreateAStatus] if data.isValid
|
|
||||||
creator <- context.loginAccount
|
|
||||||
state <- CommitState.valueOf(data.state)
|
|
||||||
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
|
|
||||||
state, data.target_url, data.description, new java.util.Date(), creator)
|
|
||||||
status <- getCommitStatus(repository.owner, repository.name, statusId)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
|
||||||
*
|
|
||||||
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
|
||||||
*/
|
|
||||||
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
ref <- params.get("ref")
|
|
||||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
|
||||||
} yield {
|
|
||||||
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
|
|
||||||
ApiCommitStatus(status, ApiUser(creator))
|
|
||||||
})
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
|
|
||||||
*
|
|
||||||
* legacy route
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
|
|
||||||
listStatusesRoute.action()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
|
|
||||||
*
|
|
||||||
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
|
|
||||||
*/
|
|
||||||
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
|
|
||||||
(for{
|
|
||||||
ref <- params.get("ref")
|
|
||||||
owner <- getAccountByUserName(repository.owner)
|
|
||||||
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
|
|
||||||
} yield {
|
|
||||||
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
|
|
||||||
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
|
|
||||||
}) getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
|
||||||
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
val (branch, path) = splitPath(repository, multiParams("splat").head)
|
||||||
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
|
||||||
@@ -564,6 +497,10 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
getForkedRepositories(
|
getForkedRepositories(
|
||||||
repository.repository.originUserName.getOrElse(repository.owner),
|
repository.repository.originUserName.getOrElse(repository.owner),
|
||||||
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
repository.repository.originRepositoryName.getOrElse(repository.name)),
|
||||||
|
context.loginAccount match {
|
||||||
|
case None => List()
|
||||||
|
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
||||||
|
}, // groups of current user
|
||||||
repository)
|
repository)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -574,13 +511,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||||
val ref = multiParams("splat").head
|
val ref = multiParams("splat").head
|
||||||
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
JGitUtil.getTreeId(git, ref).map{ treeId =>
|
||||||
html.find(ref,
|
html.find(ref, treeId, repository)
|
||||||
treeId,
|
|
||||||
repository,
|
|
||||||
context.loginAccount match {
|
|
||||||
case None => List()
|
|
||||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
|
||||||
})
|
|
||||||
} getOrElse NotFound
|
} getOrElse NotFound
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -642,10 +573,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
html.files(revision, repository,
|
html.files(revision, repository,
|
||||||
if(path == ".") Nil else path.split("/").toList, // current path
|
if(path == ".") Nil else path.split("/").toList, // current path
|
||||||
context.loginAccount match {
|
|
||||||
case None => List()
|
|
||||||
case account: Option[Account] => getGroupsByUserName(account.get.userName)
|
|
||||||
}, // groups of current user
|
|
||||||
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
|
||||||
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
|
||||||
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
package gitbucket.core.controller
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
import gitbucket.core.admin.html
|
import gitbucket.core.admin.html
|
||||||
import gitbucket.core.service.{AccountService, SystemSettingsService}
|
import gitbucket.core.service.{AccountService, SystemSettingsService, RepositoryService}
|
||||||
import gitbucket.core.util.AdminAuthenticator
|
import gitbucket.core.util.AdminAuthenticator
|
||||||
import gitbucket.core.ssh.SshServer
|
import gitbucket.core.ssh.SshServer
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
import SystemSettingsService._
|
import SystemSettingsService._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.StringUtil._
|
||||||
import io.github.gitbucket.scalatra.forms._
|
import io.github.gitbucket.scalatra.forms._
|
||||||
|
import org.apache.commons.io.{IOUtils, FileUtils}
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class SystemSettingsController extends SystemSettingsControllerBase
|
class SystemSettingsController extends SystemSettingsControllerBase
|
||||||
with AccountService with AdminAuthenticator
|
with AccountService with RepositoryService with AdminAuthenticator
|
||||||
|
|
||||||
trait SystemSettingsControllerBase extends ControllerBase {
|
trait SystemSettingsControllerBase extends AccountManagementControllerBase {
|
||||||
self: AccountService with AdminAuthenticator =>
|
self: AccountService with RepositoryService with AdminAuthenticator =>
|
||||||
|
|
||||||
private val form = mapping(
|
private val form = mapping(
|
||||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||||
@@ -68,6 +76,62 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
|
|
||||||
case class PluginForm(pluginIds: List[String])
|
case class PluginForm(pluginIds: List[String])
|
||||||
|
|
||||||
|
case class DataExportForm(tableNames: List[String])
|
||||||
|
|
||||||
|
case class NewUserForm(userName: String, password: String, fullName: String,
|
||||||
|
mailAddress: String, isAdmin: Boolean,
|
||||||
|
url: Option[String], fileId: Option[String])
|
||||||
|
|
||||||
|
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
||||||
|
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
||||||
|
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
||||||
|
|
||||||
|
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
|
members: String)
|
||||||
|
|
||||||
|
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
||||||
|
members: String, clearImage: Boolean, isRemoved: Boolean)
|
||||||
|
|
||||||
|
|
||||||
|
val newUserForm = mapping(
|
||||||
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
||||||
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
||||||
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text())))
|
||||||
|
)(NewUserForm.apply)
|
||||||
|
|
||||||
|
val editUserForm = mapping(
|
||||||
|
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
||||||
|
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
||||||
|
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
||||||
|
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
||||||
|
"isAdmin" -> trim(label("User Type" ,boolean())),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
||||||
|
)(EditUserForm.apply)
|
||||||
|
|
||||||
|
val newGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members)))
|
||||||
|
)(NewGroupForm.apply)
|
||||||
|
|
||||||
|
val editGroupForm = mapping(
|
||||||
|
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
||||||
|
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
||||||
|
"fileId" -> trim(label("File ID" ,optional(text()))),
|
||||||
|
"members" -> trim(label("Members" ,text(required, members))),
|
||||||
|
"clearImage" -> trim(label("Clear image" ,boolean())),
|
||||||
|
"removed" -> trim(label("Disable" ,boolean()))
|
||||||
|
)(EditGroupForm.apply)
|
||||||
|
|
||||||
|
|
||||||
get("/admin/system")(adminOnly {
|
get("/admin/system")(adminOnly {
|
||||||
html.system(flash.get("info"))
|
html.system(flash.get("info"))
|
||||||
})
|
})
|
||||||
@@ -92,4 +156,167 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
|||||||
html.plugins(PluginRegistry().getPlugins())
|
html.plugins(PluginRegistry().getPlugins())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
get("/admin/users")(adminOnly {
|
||||||
|
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
||||||
|
val users = getAllUsers(includeRemoved)
|
||||||
|
val members = users.collect { case account if(account.isGroupAccount) =>
|
||||||
|
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
html.userlist(users, members, includeRemoved)
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/_newuser")(adminOnly {
|
||||||
|
html.user(None)
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
||||||
|
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
||||||
|
updateImage(form.userName, form.fileId, false)
|
||||||
|
redirect("/admin/users")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/:userName/_edituser")(adminOnly {
|
||||||
|
val userName = params("userName")
|
||||||
|
html.user(getAccountByUserName(userName, true), flash.get("error"))
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
||||||
|
val userName = params("userName")
|
||||||
|
getAccountByUserName(userName, true).map { account =>
|
||||||
|
if(account.isAdmin && (form.isRemoved || !form.isAdmin) && isLastAdministrator(account)){
|
||||||
|
flash += "error" -> "Account can't be turned off because this is last one administrator."
|
||||||
|
redirect(s"/admin/users/${userName}/_edituser")
|
||||||
|
} else {
|
||||||
|
if(form.isRemoved){
|
||||||
|
// Remove repositories
|
||||||
|
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
||||||
|
// deleteRepository(userName, repositoryName)
|
||||||
|
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
||||||
|
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
||||||
|
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
||||||
|
// }
|
||||||
|
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
||||||
|
removeUserRelatedData(userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccount(account.copy(
|
||||||
|
password = form.password.map(sha1).getOrElse(account.password),
|
||||||
|
fullName = form.fullName,
|
||||||
|
mailAddress = form.mailAddress,
|
||||||
|
isAdmin = form.isAdmin,
|
||||||
|
url = form.url,
|
||||||
|
isRemoved = form.isRemoved))
|
||||||
|
|
||||||
|
updateImage(userName, form.fileId, form.clearImage)
|
||||||
|
redirect("/admin/users")
|
||||||
|
}
|
||||||
|
} getOrElse NotFound
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/_newgroup")(adminOnly {
|
||||||
|
html.usergroup(None, Nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
||||||
|
createGroup(form.groupName, form.url)
|
||||||
|
updateGroupMembers(form.groupName, form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList)
|
||||||
|
updateImage(form.groupName, form.fileId, false)
|
||||||
|
redirect("/admin/users")
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
||||||
|
defining(params("groupName")){ groupName =>
|
||||||
|
html.usergroup(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
||||||
|
defining(params("groupName"), form.members.split(",").map {
|
||||||
|
_.split(":") match {
|
||||||
|
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
||||||
|
}
|
||||||
|
}.toList){ case (groupName, members) =>
|
||||||
|
getAccountByUserName(groupName, true).map { account =>
|
||||||
|
updateGroup(groupName, form.url, form.isRemoved)
|
||||||
|
|
||||||
|
if(form.isRemoved){
|
||||||
|
// Remove from GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, Nil)
|
||||||
|
// Remove repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
deleteRepository(groupName, repositoryName)
|
||||||
|
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
||||||
|
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Update GROUP_MEMBER
|
||||||
|
updateGroupMembers(form.groupName, members)
|
||||||
|
// Update COLLABORATOR for group repositories
|
||||||
|
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
||||||
|
removeCollaborators(form.groupName, repositoryName)
|
||||||
|
members.foreach { case (userName, isManager) =>
|
||||||
|
addCollaborator(form.groupName, repositoryName, userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateImage(form.groupName, form.fileId, form.clearImage)
|
||||||
|
redirect("/admin/users")
|
||||||
|
|
||||||
|
} getOrElse NotFound
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/admin/data")(adminOnly {
|
||||||
|
import gitbucket.core.util.JDBCUtil._
|
||||||
|
val session = request2Session(request)
|
||||||
|
html.data(session.conn.allTableNames())
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/admin/export")(adminOnly {
|
||||||
|
import gitbucket.core.util.JDBCUtil._
|
||||||
|
val session = request2Session(request)
|
||||||
|
val file = if(params("type") == "sql"){
|
||||||
|
session.conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
|
||||||
|
} else {
|
||||||
|
session.conn.exportAsXML(request.getParameterValues("tableNames").toSeq)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)
|
||||||
|
response.setContentLength(file.length.toInt)
|
||||||
|
|
||||||
|
using(new FileInputStream(file)){ in =>
|
||||||
|
IOUtils.copy(in, response.outputStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
()
|
||||||
|
})
|
||||||
|
|
||||||
|
private def members: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
if(value.split(",").exists {
|
||||||
|
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
||||||
|
}) None else Some("Must select one manager at least.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
||||||
|
params.get(paramName).flatMap { userName =>
|
||||||
|
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
||||||
|
Some("You can't disable your account yourself")
|
||||||
|
else
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
package gitbucket.core.controller
|
|
||||||
|
|
||||||
import gitbucket.core.service.{RepositoryService, AccountService}
|
|
||||||
import gitbucket.core.admin.users.html
|
|
||||||
import gitbucket.core.util._
|
|
||||||
import gitbucket.core.util.ControlUtil._
|
|
||||||
import gitbucket.core.util.StringUtil._
|
|
||||||
import gitbucket.core.util.Implicits._
|
|
||||||
import gitbucket.core.util.Directory._
|
|
||||||
import io.github.gitbucket.scalatra.forms._
|
|
||||||
import org.scalatra.i18n.Messages
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
|
|
||||||
class UserManagementController extends UserManagementControllerBase
|
|
||||||
with AccountService with RepositoryService with AdminAuthenticator
|
|
||||||
|
|
||||||
trait UserManagementControllerBase extends AccountManagementControllerBase {
|
|
||||||
self: AccountService with RepositoryService with AdminAuthenticator =>
|
|
||||||
|
|
||||||
case class NewUserForm(userName: String, password: String, fullName: String,
|
|
||||||
mailAddress: String, isAdmin: Boolean,
|
|
||||||
url: Option[String], fileId: Option[String])
|
|
||||||
|
|
||||||
case class EditUserForm(userName: String, password: Option[String], fullName: String,
|
|
||||||
mailAddress: String, isAdmin: Boolean, url: Option[String],
|
|
||||||
fileId: Option[String], clearImage: Boolean, isRemoved: Boolean)
|
|
||||||
|
|
||||||
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
|
||||||
members: String)
|
|
||||||
|
|
||||||
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String],
|
|
||||||
members: String, clearImage: Boolean, isRemoved: Boolean)
|
|
||||||
|
|
||||||
val newUserForm = mapping(
|
|
||||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
|
||||||
"password" -> trim(label("Password" ,text(required, maxlength(20)))),
|
|
||||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
|
||||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
|
|
||||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text())))
|
|
||||||
)(NewUserForm.apply)
|
|
||||||
|
|
||||||
val editUserForm = mapping(
|
|
||||||
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier))),
|
|
||||||
"password" -> trim(label("Password" ,optional(text(maxlength(20))))),
|
|
||||||
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
|
|
||||||
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress("userName")))),
|
|
||||||
"isAdmin" -> trim(label("User Type" ,boolean())),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
|
||||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
|
||||||
"removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
|
|
||||||
)(EditUserForm.apply)
|
|
||||||
|
|
||||||
val newGroupForm = mapping(
|
|
||||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
|
||||||
"members" -> trim(label("Members" ,text(required, members)))
|
|
||||||
)(NewGroupForm.apply)
|
|
||||||
|
|
||||||
val editGroupForm = mapping(
|
|
||||||
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier))),
|
|
||||||
"url" -> trim(label("URL" ,optional(text(maxlength(200))))),
|
|
||||||
"fileId" -> trim(label("File ID" ,optional(text()))),
|
|
||||||
"members" -> trim(label("Members" ,text(required, members))),
|
|
||||||
"clearImage" -> trim(label("Clear image" ,boolean())),
|
|
||||||
"removed" -> trim(label("Disable" ,boolean()))
|
|
||||||
)(EditGroupForm.apply)
|
|
||||||
|
|
||||||
get("/admin/users")(adminOnly {
|
|
||||||
val includeRemoved = params.get("includeRemoved").map(_.toBoolean).getOrElse(false)
|
|
||||||
val users = getAllUsers(includeRemoved)
|
|
||||||
val members = users.collect { case account if(account.isGroupAccount) =>
|
|
||||||
account.userName -> getGroupMembers(account.userName).map(_.userName)
|
|
||||||
}.toMap
|
|
||||||
|
|
||||||
html.list(users, members, includeRemoved)
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/_newuser")(adminOnly {
|
|
||||||
html.user(None)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_newuser", newUserForm)(adminOnly { form =>
|
|
||||||
createAccount(form.userName, sha1(form.password), form.fullName, form.mailAddress, form.isAdmin, form.url)
|
|
||||||
updateImage(form.userName, form.fileId, false)
|
|
||||||
redirect("/admin/users")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/:userName/_edituser")(adminOnly {
|
|
||||||
val userName = params("userName")
|
|
||||||
html.user(getAccountByUserName(userName, true))
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
|
|
||||||
val userName = params("userName")
|
|
||||||
getAccountByUserName(userName, true).map { account =>
|
|
||||||
|
|
||||||
if(form.isRemoved){
|
|
||||||
// Remove repositories
|
|
||||||
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
|
|
||||||
// deleteRepository(userName, repositoryName)
|
|
||||||
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
|
|
||||||
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
|
|
||||||
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
|
|
||||||
// }
|
|
||||||
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
|
|
||||||
removeUserRelatedData(userName)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAccount(account.copy(
|
|
||||||
password = form.password.map(sha1).getOrElse(account.password),
|
|
||||||
fullName = form.fullName,
|
|
||||||
mailAddress = form.mailAddress,
|
|
||||||
isAdmin = form.isAdmin,
|
|
||||||
url = form.url,
|
|
||||||
isRemoved = form.isRemoved))
|
|
||||||
|
|
||||||
updateImage(userName, form.fileId, form.clearImage)
|
|
||||||
redirect("/admin/users")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/_newgroup")(adminOnly {
|
|
||||||
html.group(None, Nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/_newgroup", newGroupForm)(adminOnly { form =>
|
|
||||||
createGroup(form.groupName, form.url)
|
|
||||||
updateGroupMembers(form.groupName, form.members.split(",").map {
|
|
||||||
_.split(":") match {
|
|
||||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
|
||||||
}
|
|
||||||
}.toList)
|
|
||||||
updateImage(form.groupName, form.fileId, false)
|
|
||||||
redirect("/admin/users")
|
|
||||||
})
|
|
||||||
|
|
||||||
get("/admin/users/:groupName/_editgroup")(adminOnly {
|
|
||||||
defining(params("groupName")){ groupName =>
|
|
||||||
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
post("/admin/users/:groupName/_editgroup", editGroupForm)(adminOnly { form =>
|
|
||||||
defining(params("groupName"), form.members.split(",").map {
|
|
||||||
_.split(":") match {
|
|
||||||
case Array(userName, isManager) => (userName, isManager.toBoolean)
|
|
||||||
}
|
|
||||||
}.toList){ case (groupName, members) =>
|
|
||||||
getAccountByUserName(groupName, true).map { account =>
|
|
||||||
updateGroup(groupName, form.url, form.isRemoved)
|
|
||||||
|
|
||||||
if(form.isRemoved){
|
|
||||||
// Remove from GROUP_MEMBER
|
|
||||||
updateGroupMembers(form.groupName, Nil)
|
|
||||||
// Remove repositories
|
|
||||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
|
||||||
deleteRepository(groupName, repositoryName)
|
|
||||||
FileUtils.deleteDirectory(getRepositoryDir(groupName, repositoryName))
|
|
||||||
FileUtils.deleteDirectory(getWikiRepositoryDir(groupName, repositoryName))
|
|
||||||
FileUtils.deleteDirectory(getTemporaryDir(groupName, repositoryName))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Update GROUP_MEMBER
|
|
||||||
updateGroupMembers(form.groupName, members)
|
|
||||||
// Update COLLABORATOR for group repositories
|
|
||||||
getRepositoryNamesOfUser(form.groupName).foreach { repositoryName =>
|
|
||||||
removeCollaborators(form.groupName, repositoryName)
|
|
||||||
members.foreach { case (userName, isManager) =>
|
|
||||||
addCollaborator(form.groupName, repositoryName, userName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateImage(form.groupName, form.fileId, form.clearImage)
|
|
||||||
redirect("/admin/users")
|
|
||||||
|
|
||||||
} getOrElse NotFound
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
private def members: Constraint = new Constraint(){
|
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
|
||||||
if(value.split(",").exists {
|
|
||||||
_.split(":") match { case Array(userName, isManager) => isManager.toBoolean }
|
|
||||||
}) None else Some("Must select one manager at least.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
|
|
||||||
override def validate(name: String, value: String, messages: Messages): Option[String] = {
|
|
||||||
params.get(paramName).flatMap { userName =>
|
|
||||||
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
|
|
||||||
Some("You can't disable your account yourself")
|
|
||||||
else
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,8 @@ import org.eclipse.jgit.api.Git
|
|||||||
import org.scalatra.i18n.Messages
|
import org.scalatra.i18n.Messages
|
||||||
|
|
||||||
class WikiController extends WikiControllerBase
|
class WikiController extends WikiControllerBase
|
||||||
with WikiService with RepositoryService with AccountService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator
|
with WikiService with RepositoryService with AccountService with ActivityService
|
||||||
|
with CollaboratorsAuthenticator with ReferrerAuthenticator
|
||||||
|
|
||||||
trait WikiControllerBase extends ControllerBase {
|
trait WikiControllerBase extends ControllerBase {
|
||||||
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
self: WikiService with RepositoryService with ActivityService with CollaboratorsAuthenticator with ReferrerAuthenticator =>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package gitbucket.core.model
|
|
||||||
|
|
||||||
trait PluginComponent extends TemplateComponent { self: Profile =>
|
|
||||||
import profile.simple._
|
|
||||||
import self._
|
|
||||||
|
|
||||||
lazy val Plugins = TableQuery[Plugins]
|
|
||||||
|
|
||||||
class Plugins(tag: Tag) extends Table[Plugin](tag, "PLUGIN"){
|
|
||||||
val pluginId = column[String]("PLUGIN_ID", O PrimaryKey)
|
|
||||||
val version = column[String]("VERSION")
|
|
||||||
def * = (pluginId, version) <> (Plugin.tupled, Plugin.unapply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case class Plugin(
|
|
||||||
pluginId: String,
|
|
||||||
version: String
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package gitbucket.core.model
|
package gitbucket.core.model
|
||||||
|
|
||||||
|
import gitbucket.core.util.DatabaseConfig
|
||||||
|
|
||||||
trait Profile {
|
trait Profile {
|
||||||
val profile: slick.driver.JdbcProfile
|
val profile: slick.driver.JdbcProfile
|
||||||
@@ -28,7 +29,9 @@ trait Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trait ProfileProvider { self: Profile =>
|
trait ProfileProvider { self: Profile =>
|
||||||
val profile = slick.driver.H2Driver
|
|
||||||
|
lazy val profile = DatabaseConfig.slickDriver
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trait CoreProfile extends ProfileProvider with Profile
|
trait CoreProfile extends ProfileProvider with Profile
|
||||||
@@ -49,7 +52,6 @@ trait CoreProfile extends ProfileProvider with Profile
|
|||||||
with SshKeyComponent
|
with SshKeyComponent
|
||||||
with WebHookComponent
|
with WebHookComponent
|
||||||
with WebHookEventComponent
|
with WebHookEventComponent
|
||||||
with PluginComponent
|
|
||||||
with ProtectedBranchComponent
|
with ProtectedBranchComponent
|
||||||
|
|
||||||
object Profile extends CoreProfile
|
object Profile extends CoreProfile
|
||||||
|
|||||||
@@ -3,20 +3,43 @@ package gitbucket.core.model
|
|||||||
trait WebHookComponent extends TemplateComponent { self: Profile =>
|
trait WebHookComponent extends TemplateComponent { self: Profile =>
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
|
|
||||||
|
implicit val whContentTypeColumnType = MappedColumnType.base[WebHookContentType, String](whct => whct.code , code => WebHookContentType.valueOf(code))
|
||||||
|
|
||||||
lazy val WebHooks = TableQuery[WebHooks]
|
lazy val WebHooks = TableQuery[WebHooks]
|
||||||
|
|
||||||
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
|
||||||
val url = column[String]("URL")
|
val url = column[String]("URL")
|
||||||
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
val token = column[Option[String]]("TOKEN", O.Nullable)
|
||||||
|
val ctype = column[WebHookContentType]("CTYPE", O.NotNull)
|
||||||
|
def * = (userName, repositoryName, url, ctype, token) <> ((WebHook.apply _).tupled, WebHook.unapply)
|
||||||
|
|
||||||
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class WebHookContentType(val code: String, val ctype: String)
|
||||||
|
|
||||||
|
object WebHookContentType {
|
||||||
|
object JSON extends WebHookContentType("json", "application/json")
|
||||||
|
|
||||||
|
object FORM extends WebHookContentType("form", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
val values: Vector[WebHookContentType] = Vector(JSON, FORM)
|
||||||
|
|
||||||
|
private val map: Map[String, WebHookContentType] = values.map(enum => enum.code -> enum).toMap
|
||||||
|
|
||||||
|
def apply(code: String): WebHookContentType = map(code)
|
||||||
|
|
||||||
|
def valueOf(code: String): WebHookContentType = map(code)
|
||||||
|
def valueOpt(code: String): Option[WebHookContentType] = map.get(code)
|
||||||
|
}
|
||||||
|
|
||||||
case class WebHook(
|
case class WebHook(
|
||||||
userName: String,
|
userName: String,
|
||||||
repositoryName: String,
|
repositoryName: String,
|
||||||
url: String
|
url: String,
|
||||||
|
ctype: WebHookContentType,
|
||||||
|
token: Option[String]
|
||||||
)
|
)
|
||||||
|
|
||||||
object WebHook {
|
object WebHook {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package gitbucket.core.plugin
|
package gitbucket.core.plugin
|
||||||
|
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import gitbucket.core.controller.ControllerBase
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Version
|
import io.github.gitbucket.solidbase.model.Version
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trait for define plugin interface.
|
* Trait for define plugin interface.
|
||||||
* To provide plugin, put Plugin class which mixed in this trait into the package root.
|
* To provide a plugin, put a Plugin class which extends this class into the package root.
|
||||||
*/
|
*/
|
||||||
trait Plugin {
|
abstract class Plugin {
|
||||||
|
|
||||||
val pluginId: String
|
val pluginId: String
|
||||||
val pluginName: String
|
val pluginName: String
|
||||||
@@ -77,6 +79,76 @@ trait Plugin {
|
|||||||
*/
|
*/
|
||||||
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
|
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add global menus.
|
||||||
|
*/
|
||||||
|
val globalMenus: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add global menus.
|
||||||
|
*/
|
||||||
|
def globalMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository menus.
|
||||||
|
*/
|
||||||
|
val repositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository menus.
|
||||||
|
*/
|
||||||
|
def repositoryMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository setting tabs.
|
||||||
|
*/
|
||||||
|
val repositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add repository setting tabs.
|
||||||
|
*/
|
||||||
|
def repositorySettingTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add profile tabs.
|
||||||
|
*/
|
||||||
|
val profileTabs: Seq[(Account, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add profile tabs.
|
||||||
|
*/
|
||||||
|
def profileTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Account, Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add system setting menus.
|
||||||
|
*/
|
||||||
|
val systemSettingMenus: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add system setting menus.
|
||||||
|
*/
|
||||||
|
def systemSettingMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add account setting menus.
|
||||||
|
*/
|
||||||
|
val accountSettingMenus: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add account setting menus.
|
||||||
|
*/
|
||||||
|
def accountSettingMenus(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add dashboard tabs.
|
||||||
|
*/
|
||||||
|
val dashboardTabs: Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override to add dashboard tabs.
|
||||||
|
*/
|
||||||
|
def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is invoked in initialization of plugin system.
|
* This method is invoked in initialization of plugin system.
|
||||||
* Register plugin functionality to PluginRegistry.
|
* Register plugin functionality to PluginRegistry.
|
||||||
@@ -100,6 +172,27 @@ trait Plugin {
|
|||||||
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
|
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
|
||||||
registry.addReceiveHook(receiveHook)
|
registry.addReceiveHook(receiveHook)
|
||||||
}
|
}
|
||||||
|
(globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
|
||||||
|
registry.addGlobalMenu(globalMenu)
|
||||||
|
}
|
||||||
|
(repositoryMenus ++ repositoryMenus(registry, context, settings)).foreach { repositoryMenu =>
|
||||||
|
registry.addRepositoryMenu(repositoryMenu)
|
||||||
|
}
|
||||||
|
(repositorySettingTabs ++ repositorySettingTabs(registry, context, settings)).foreach { repositorySettingTab =>
|
||||||
|
registry.addRepositorySettingTab(repositorySettingTab)
|
||||||
|
}
|
||||||
|
(profileTabs ++ profileTabs(registry, context, settings)).foreach { profileTab =>
|
||||||
|
registry.addProfileTab(profileTab)
|
||||||
|
}
|
||||||
|
(systemSettingMenus ++ systemSettingMenus(registry, context, settings)).foreach { systemSettingMenu =>
|
||||||
|
registry.addSystemSettingMenu(systemSettingMenu)
|
||||||
|
}
|
||||||
|
(accountSettingMenus ++ accountSettingMenus(registry, context, settings)).foreach { accountSettingMenu =>
|
||||||
|
registry.addAccountSettingMenu(accountSettingMenu)
|
||||||
|
}
|
||||||
|
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
|
||||||
|
registry.addDashboardTab(dashboardTab)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ package gitbucket.core.plugin
|
|||||||
import java.io.{File, FilenameFilter, InputStream}
|
import java.io.{File, FilenameFilter, InputStream}
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import javax.servlet.ServletContext
|
import javax.servlet.ServletContext
|
||||||
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
|
|
||||||
|
|
||||||
import gitbucket.core.controller.{Context, ControllerBase}
|
import gitbucket.core.controller.{Context, ControllerBase}
|
||||||
|
import gitbucket.core.model.Account
|
||||||
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
import gitbucket.core.service.SystemSettingsService.SystemSettings
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.DatabaseConfig
|
||||||
import gitbucket.core.util.Directory._
|
import gitbucket.core.util.Directory._
|
||||||
import gitbucket.core.util.JDBCUtil._
|
import io.github.gitbucket.solidbase.Solidbase
|
||||||
import gitbucket.core.util.{Version, Versions}
|
import io.github.gitbucket.solidbase.model.Module
|
||||||
|
import liquibase.database.core.H2Database
|
||||||
import org.apache.commons.codec.binary.{Base64, StringUtils}
|
import org.apache.commons.codec.binary.{Base64, StringUtils}
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
@@ -33,6 +35,14 @@ class PluginRegistry {
|
|||||||
private val receiveHooks = new ListBuffer[ReceiveHook]
|
private val receiveHooks = new ListBuffer[ReceiveHook]
|
||||||
receiveHooks += new ProtectedBranchReceiveHook()
|
receiveHooks += new ProtectedBranchReceiveHook()
|
||||||
|
|
||||||
|
private val globalMenus = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
|
||||||
|
private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
|
||||||
|
private val profileTabs = new ListBuffer[(Account, Context) => Option[Link]]
|
||||||
|
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
|
||||||
|
|
||||||
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
def addPlugin(pluginInfo: PluginInfo): Unit = {
|
||||||
plugins += pluginInfo
|
plugins += pluginInfo
|
||||||
}
|
}
|
||||||
@@ -107,17 +117,47 @@ class PluginRegistry {
|
|||||||
|
|
||||||
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
|
||||||
|
|
||||||
private case class GlobalAction(
|
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = {
|
||||||
method: String,
|
globalMenus += globalMenu
|
||||||
path: String,
|
}
|
||||||
function: (HttpServletRequest, HttpServletResponse, Context) => Any
|
|
||||||
)
|
|
||||||
|
|
||||||
private case class RepositoryAction(
|
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
|
||||||
method: String,
|
|
||||||
path: String,
|
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = {
|
||||||
function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
|
repositoryMenus += repositoryMenu
|
||||||
)
|
}
|
||||||
|
|
||||||
|
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
|
||||||
|
|
||||||
|
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = {
|
||||||
|
repositorySettingTabs += repositorySettingTab
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
|
||||||
|
|
||||||
|
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = {
|
||||||
|
profileTabs += profileTab
|
||||||
|
}
|
||||||
|
|
||||||
|
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
|
||||||
|
|
||||||
|
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = {
|
||||||
|
systemSettingMenus += systemSettingMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
|
||||||
|
|
||||||
|
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = {
|
||||||
|
accountSettingMenus += accountSettingMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
|
||||||
|
|
||||||
|
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = {
|
||||||
|
dashboardTabs += dashboardTab
|
||||||
|
}
|
||||||
|
|
||||||
|
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,30 +189,15 @@ object PluginRegistry {
|
|||||||
val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin]
|
val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin]
|
||||||
|
|
||||||
// Migration
|
// Migration
|
||||||
val headVersion = plugin.versions.head
|
val solidbase = new Solidbase()
|
||||||
val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match {
|
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
|
||||||
case Some(x) => {
|
|
||||||
val dim = x.split("\\.")
|
|
||||||
Version(dim(0).toInt, dim(1).toInt)
|
|
||||||
}
|
|
||||||
case None => Version(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn =>
|
|
||||||
currentVersion.versionString match {
|
|
||||||
case "0.0" =>
|
|
||||||
conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString)
|
|
||||||
case _ =>
|
|
||||||
conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
plugin.initialize(instance, context, settings)
|
plugin.initialize(instance, context, settings)
|
||||||
instance.addPlugin(PluginInfo(
|
instance.addPlugin(PluginInfo(
|
||||||
pluginId = plugin.pluginId,
|
pluginId = plugin.pluginId,
|
||||||
pluginName = plugin.pluginName,
|
pluginName = plugin.pluginName,
|
||||||
version = plugin.versions.head.versionString,
|
version = plugin.versions.head.getVersion,
|
||||||
description = plugin.description,
|
description = plugin.description,
|
||||||
pluginClass = plugin
|
pluginClass = plugin
|
||||||
))
|
))
|
||||||
@@ -201,6 +226,8 @@ object PluginRegistry {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class Link(id: String, label: String, path: String, icon: Option[String] = None)
|
||||||
|
|
||||||
case class PluginInfo(
|
case class PluginInfo(
|
||||||
pluginId: String,
|
pluginId: String,
|
||||||
pluginName: String,
|
pluginName: String,
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ trait AccountService {
|
|||||||
Accounts filter (_.removed === false.bind) sortBy(_.userName) list
|
Accounts filter (_.removed === false.bind) sortBy(_.userName) list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def isLastAdministrator(account: Account)(implicit s: Session): Boolean = {
|
||||||
|
if(account.isAdmin){
|
||||||
|
(Accounts filter (_.removed === false.bind) filter (_.isAdmin === true.bind) map (_.userName.length)).first == 1
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
|
def createAccount(userName: String, password: String, fullName: String, mailAddress: String, isAdmin: Boolean, url: Option[String])
|
||||||
(implicit s: Session): Unit =
|
(implicit s: Session): Unit =
|
||||||
Accounts insert Account(
|
Accounts insert Account(
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.controller.Context
|
||||||
|
import gitbucket.core.model.Issue
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import gitbucket.core.util.Notifier
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
trait HandleCommentService {
|
||||||
|
self: RepositoryService with IssuesService with ActivityService
|
||||||
|
with WebHookService with WebHookIssueCommentService with WebHookPullRequestService =>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
|
||||||
|
*/
|
||||||
|
def handleComment(issue: Issue, content: Option[String], repository: RepositoryService.RepositoryInfo, actionOpt: Option[String])
|
||||||
|
(implicit context: Context, s: Session) = {
|
||||||
|
|
||||||
|
defining(repository.owner, repository.name){ case (owner, name) =>
|
||||||
|
val userName = context.loginAccount.get.userName
|
||||||
|
|
||||||
|
val (action, recordActivity) = 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(recordReopenIssueActivity _))
|
||||||
|
}
|
||||||
|
.map { case (closed, t) =>
|
||||||
|
updateClosed(owner, name, issue.issueId, closed)
|
||||||
|
t
|
||||||
|
}
|
||||||
|
.getOrElse(None -> None)
|
||||||
|
|
||||||
|
val commentId = (content, action) match {
|
||||||
|
case (None, None) => None
|
||||||
|
case (None, Some(action)) => Some(createComment(owner, name, userName, issue.issueId, action.capitalize, action))
|
||||||
|
case (Some(content), _) => Some(createComment(owner, name, userName, issue.issueId, content, action.map(_+ "_comment").getOrElse("comment")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// record comment activity if comment is entered
|
||||||
|
content foreach {
|
||||||
|
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||||
|
(owner, name, userName, issue.issueId, _)
|
||||||
|
}
|
||||||
|
recordActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
|
||||||
|
|
||||||
|
// extract references and create refer comment
|
||||||
|
content.map { content =>
|
||||||
|
createReferComment(owner, name, issue, content, context.loginAccount.get)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call web hooks
|
||||||
|
action match {
|
||||||
|
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
|
||||||
|
case Some(act) => val webHookAction = act match {
|
||||||
|
case "open" => "opened"
|
||||||
|
case "reopen" => "reopened"
|
||||||
|
case "close" => "closed"
|
||||||
|
case _ => act
|
||||||
|
}
|
||||||
|
if(issue.isPullRequest){
|
||||||
|
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
|
||||||
|
} else {
|
||||||
|
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifications
|
||||||
|
Notifier() match {
|
||||||
|
case f =>
|
||||||
|
content foreach {
|
||||||
|
f.toNotify(repository, issue, _){
|
||||||
|
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
|
||||||
|
if(issue.isPullRequest) "pull" else "issues"}/${issue.issueId}#comment-${commentId.get}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action foreach {
|
||||||
|
f.toNotify(repository, issue, _){
|
||||||
|
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issue.issueId}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commentId.map( issue -> _ )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
|
import gitbucket.core.util.StringUtil
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
|
|
||||||
import gitbucket.core.util.StringUtil._
|
import gitbucket.core.util.StringUtil._
|
||||||
@@ -12,6 +14,7 @@ import Q.interpolation
|
|||||||
|
|
||||||
|
|
||||||
trait IssuesService {
|
trait IssuesService {
|
||||||
|
self: AccountService =>
|
||||||
import IssuesService._
|
import IssuesService._
|
||||||
|
|
||||||
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
def getIssue(owner: String, repository: String, issueId: String)(implicit s: Session) =
|
||||||
@@ -105,25 +108,41 @@ trait IssuesService {
|
|||||||
}
|
}
|
||||||
import gitbucket.core.model.Profile.commitStateColumnType
|
import gitbucket.core.model.Profile.commitStateColumnType
|
||||||
val query = Q.query[Seq[(String, String, Int)], (String, String, Int, Int, Int, Option[String], Option[CommitState], Option[String], Option[String])](s"""
|
val query = Q.query[Seq[(String, String, Int)], (String, String, Int, Int, Int, Option[String], Option[CommitState], Option[String], Option[String])](s"""
|
||||||
SELECT SUMM.USER_NAME, SUMM.REPOSITORY_NAME, SUMM.ISSUE_ID, CS_ALL, CS_SUCCESS
|
SELECT
|
||||||
, CSD.CONTEXT, CSD.STATE, CSD.TARGET_URL, CSD.DESCRIPTION
|
SUMM.USER_NAME,
|
||||||
FROM (SELECT
|
SUMM.REPOSITORY_NAME,
|
||||||
PR.USER_NAME
|
SUMM.ISSUE_ID,
|
||||||
, PR.REPOSITORY_NAME
|
CS_ALL,
|
||||||
, PR.ISSUE_ID
|
CS_SUCCESS,
|
||||||
, COUNT(CS.STATE) AS CS_ALL
|
CSD.CONTEXT,
|
||||||
, SUM(CS.STATE='success') AS CS_SUCCESS
|
CSD.STATE,
|
||||||
, PR.COMMIT_ID_TO AS COMMIT_ID
|
CSD.TARGET_URL,
|
||||||
|
CSD.DESCRIPTION
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
PR.USER_NAME,
|
||||||
|
PR.REPOSITORY_NAME,
|
||||||
|
PR.ISSUE_ID,
|
||||||
|
COUNT(CS.STATE) AS CS_ALL,
|
||||||
|
CSS.CS_SUCCESS AS CS_SUCCESS,
|
||||||
|
PR.COMMIT_ID_TO AS COMMIT_ID
|
||||||
FROM PULL_REQUEST PR
|
FROM PULL_REQUEST PR
|
||||||
JOIN COMMIT_STATUS CS
|
JOIN COMMIT_STATUS CS
|
||||||
ON PR.USER_NAME=CS.USER_NAME
|
ON PR.USER_NAME = CS.USER_NAME AND PR.REPOSITORY_NAME = CS.REPOSITORY_NAME AND PR.COMMIT_ID_TO = CS.COMMIT_ID
|
||||||
AND PR.REPOSITORY_NAME=CS.REPOSITORY_NAME
|
JOIN (
|
||||||
AND PR.COMMIT_ID_TO=CS.COMMIT_ID
|
SELECT
|
||||||
WHERE $issueIdQuery
|
COUNT(*) AS CS_SUCCESS,
|
||||||
GROUP BY PR.USER_NAME, PR.REPOSITORY_NAME, PR.ISSUE_ID) as SUMM
|
USER_NAME,
|
||||||
|
REPOSITORY_NAME,
|
||||||
|
COMMIT_ID
|
||||||
|
FROM COMMIT_STATUS WHERE STATE = 'success' GROUP BY USER_NAME, REPOSITORY_NAME, COMMIT_ID
|
||||||
|
) CSS ON PR.USER_NAME = CSS.USER_NAME AND PR.REPOSITORY_NAME = CSS.REPOSITORY_NAME AND PR.COMMIT_ID_TO = CSS.COMMIT_ID
|
||||||
|
WHERE $issueIdQuery
|
||||||
|
GROUP BY PR.USER_NAME, PR.REPOSITORY_NAME, PR.ISSUE_ID, CSS.CS_SUCCESS
|
||||||
|
) as SUMM
|
||||||
LEFT OUTER JOIN COMMIT_STATUS CSD
|
LEFT OUTER JOIN COMMIT_STATUS CSD
|
||||||
ON SUMM.CS_ALL = 1 AND SUMM.COMMIT_ID = CSD.COMMIT_ID""");
|
ON SUMM.CS_ALL = 1 AND SUMM.COMMIT_ID = CSD.COMMIT_ID""");
|
||||||
query(issueList).list.map{
|
query(issueList).list.map {
|
||||||
case(userName, repositoryName, issueId, count, successCount, context, state, targetUrl, description) =>
|
case(userName, repositoryName, issueId, count, successCount, context, state, targetUrl, description) =>
|
||||||
(userName, repositoryName, issueId) -> CommitStatusInfo(count, successCount, context, state, targetUrl, description)
|
(userName, repositoryName, issueId) -> CommitStatusInfo(count, successCount, context, state, targetUrl, description)
|
||||||
}.toMap
|
}.toMap
|
||||||
@@ -215,9 +234,8 @@ trait IssuesService {
|
|||||||
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||||
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||||
(t1.closed === (condition.state == "closed").bind) &&
|
(t1.closed === (condition.state == "closed").bind) &&
|
||||||
//(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) &&
|
||||||
(t1.milestoneId.? isEmpty, condition.milestone == Some(None)) &&
|
(t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) &&
|
||||||
(t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
|
|
||||||
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
|
||||||
(t1.pullRequest === pullRequest.bind) &&
|
(t1.pullRequest === pullRequest.bind) &&
|
||||||
// Milestone filter
|
// Milestone filter
|
||||||
@@ -225,6 +243,8 @@ trait IssuesService {
|
|||||||
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) &&
|
(t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) &&
|
||||||
(t2.title === condition.milestone.get.get.bind)
|
(t2.title === condition.milestone.get.get.bind)
|
||||||
} exists, condition.milestone.flatten.isDefined) &&
|
} exists, condition.milestone.flatten.isDefined) &&
|
||||||
|
// Assignee filter
|
||||||
|
(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) &&
|
||||||
// Label filter
|
// Label filter
|
||||||
(IssueLabels filter { t2 =>
|
(IssueLabels filter { t2 =>
|
||||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
|
||||||
@@ -394,6 +414,29 @@ trait IssuesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String, loginAccount: Account)(implicit s: Session) = {
|
||||||
|
StringUtil.extractIssueId(message).foreach { issueId =>
|
||||||
|
val content = fromIssue.issueId + ":" + fromIssue.title
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
// Not add if refer comment already exist.
|
||||||
|
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
|
||||||
|
createComment(owner, repository, loginAccount.userName, issueId.toInt, content, "refer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session) = {
|
||||||
|
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
||||||
|
if(getIssue(owner, repository, issueId).isDefined){
|
||||||
|
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
||||||
|
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object IssuesService {
|
object IssuesService {
|
||||||
@@ -405,7 +448,7 @@ object IssuesService {
|
|||||||
labels: Set[String] = Set.empty,
|
labels: Set[String] = Set.empty,
|
||||||
milestone: Option[Option[String]] = None,
|
milestone: Option[Option[String]] = None,
|
||||||
author: Option[String] = None,
|
author: Option[String] = None,
|
||||||
assigned: Option[String] = None,
|
assigned: Option[Option[String]] = None,
|
||||||
mentioned: Option[String] = None,
|
mentioned: Option[String] = None,
|
||||||
state: String = "open",
|
state: String = "open",
|
||||||
sort: String = "created",
|
sort: String = "created",
|
||||||
@@ -449,12 +492,15 @@ object IssuesService {
|
|||||||
def toURL: String =
|
def toURL: String =
|
||||||
"?" + List(
|
"?" + List(
|
||||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
||||||
milestone.map { _ match {
|
milestone.map {
|
||||||
case Some(x) => "milestone=" + urlEncode(x)
|
case Some(x) => "milestone=" + urlEncode(x)
|
||||||
case None => "milestone=none"
|
case None => "milestone=none"
|
||||||
}},
|
},
|
||||||
author .map(x => "author=" + urlEncode(x)),
|
author .map(x => "author=" + urlEncode(x)),
|
||||||
assigned .map(x => "assigned=" + urlEncode(x)),
|
assigned.map {
|
||||||
|
case Some(x) => "assigned=" + urlEncode(x)
|
||||||
|
case None => "assigned=none"
|
||||||
|
},
|
||||||
mentioned.map(x => "mentioned=" + urlEncode(x)),
|
mentioned.map(x => "mentioned=" + urlEncode(x)),
|
||||||
Some("state=" + urlEncode(state)),
|
Some("state=" + urlEncode(state)),
|
||||||
Some("sort=" + urlEncode(sort)),
|
Some("sort=" + urlEncode(sort)),
|
||||||
@@ -499,10 +545,14 @@ object IssuesService {
|
|||||||
conditions.get("milestone").flatMap(_.headOption) match {
|
conditions.get("milestone").flatMap(_.headOption) match {
|
||||||
case None => None
|
case None => None
|
||||||
case Some("none") => Some(None)
|
case Some("none") => Some(None)
|
||||||
case Some(x) => Some(Some(x)) //milestones.get(x).map(x => Some(x))
|
case Some(x) => Some(Some(x))
|
||||||
},
|
},
|
||||||
conditions.get("author").flatMap(_.headOption),
|
conditions.get("author").flatMap(_.headOption),
|
||||||
conditions.get("assignee").flatMap(_.headOption),
|
conditions.get("assignee").flatMap(_.headOption) match {
|
||||||
|
case None => None
|
||||||
|
case Some("none") => Some(None)
|
||||||
|
case Some(x) => Some(Some(x))
|
||||||
|
},
|
||||||
conditions.get("mentions").flatMap(_.headOption),
|
conditions.get("mentions").flatMap(_.headOption),
|
||||||
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
|
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
|
||||||
sort,
|
sort,
|
||||||
@@ -523,7 +573,10 @@ object IssuesService {
|
|||||||
case x => Some(x)
|
case x => Some(x)
|
||||||
},
|
},
|
||||||
param(request, "author"),
|
param(request, "author"),
|
||||||
param(request, "assigned"),
|
param(request, "assigned").map {
|
||||||
|
case "none" => None
|
||||||
|
case x => Some(x)
|
||||||
|
},
|
||||||
param(request, "mentioned"),
|
param(request, "mentioned"),
|
||||||
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
param(request, "state", Seq("open", "closed")).getOrElse("open"),
|
||||||
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"),
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package gitbucket.core.service
|
|
||||||
|
|
||||||
import gitbucket.core.model.Plugin
|
|
||||||
import gitbucket.core.model.Profile._
|
|
||||||
import profile.simple._
|
|
||||||
|
|
||||||
trait PluginService {
|
|
||||||
|
|
||||||
def getPlugins()(implicit s: Session): List[Plugin] =
|
|
||||||
Plugins.sortBy(_.pluginId).list
|
|
||||||
|
|
||||||
def registerPlugin(plugin: Plugin)(implicit s: Session): Unit =
|
|
||||||
Plugins.insert(plugin)
|
|
||||||
|
|
||||||
def updatePlugin(plugin: Plugin)(implicit s: Session): Unit =
|
|
||||||
Plugins.filter(_.pluginId === plugin.pluginId.bind).map(_.version).update(plugin.version)
|
|
||||||
|
|
||||||
def deletePlugin(pluginId: String)(implicit s: Session): Unit =
|
|
||||||
Plugins.filter(_.pluginId === pluginId.bind).delete
|
|
||||||
|
|
||||||
def getPlugin(pluginId: String)(implicit s: Session): Option[Plugin] =
|
|
||||||
Plugins.filter(_.pluginId === pluginId.bind).firstOption
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.util.ControlUtil._
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.JGitUtil
|
||||||
|
import gitbucket.core.model.Account
|
||||||
|
import org.eclipse.jgit.api.Git
|
||||||
|
import org.eclipse.jgit.dircache.DirCache
|
||||||
|
import org.eclipse.jgit.lib.{FileMode, Constants}
|
||||||
|
import profile.simple._
|
||||||
|
|
||||||
|
trait RepositoryCreationService {
|
||||||
|
self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService =>
|
||||||
|
|
||||||
|
def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean)
|
||||||
|
(implicit s: Session) {
|
||||||
|
val ownerAccount = getAccountByUserName(owner).get
|
||||||
|
val loginUserName = loginAccount.userName
|
||||||
|
|
||||||
|
// Insert to the database at first
|
||||||
|
insertRepository(name, owner, description, isPrivate)
|
||||||
|
|
||||||
|
// Add collaborators for group repository
|
||||||
|
if(ownerAccount.isGroupAccount){
|
||||||
|
getGroupMembers(owner).foreach { member =>
|
||||||
|
addCollaborator(owner, name, member.userName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert default labels
|
||||||
|
insertDefaultLabels(owner, name)
|
||||||
|
|
||||||
|
// Create the actual repository
|
||||||
|
val gitdir = getRepositoryDir(owner, name)
|
||||||
|
JGitUtil.initRepository(gitdir)
|
||||||
|
|
||||||
|
if(createReadme){
|
||||||
|
using(Git.open(gitdir)){ git =>
|
||||||
|
val builder = DirCache.newInCore.builder()
|
||||||
|
val inserter = git.getRepository.newObjectInserter()
|
||||||
|
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
|
||||||
|
val content = if(description.nonEmpty){
|
||||||
|
name + "\n" +
|
||||||
|
"===============\n" +
|
||||||
|
"\n" +
|
||||||
|
description.get
|
||||||
|
} else {
|
||||||
|
name + "\n" +
|
||||||
|
"===============\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
|
||||||
|
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
|
||||||
|
builder.finish()
|
||||||
|
|
||||||
|
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
|
||||||
|
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Wiki repository
|
||||||
|
createWikiRepository(loginAccount, owner, name)
|
||||||
|
|
||||||
|
// Record activity
|
||||||
|
recordCreateRepositoryActivity(owner, name, loginUserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
def insertDefaultLabels(userName: String, repositoryName: String)(implicit s: Session): Unit = {
|
||||||
|
createLabel(userName, repositoryName, "bug", "fc2929")
|
||||||
|
createLabel(userName, repositoryName, "duplicate", "cccccc")
|
||||||
|
createLabel(userName, repositoryName, "enhancement", "84b6eb")
|
||||||
|
createLabel(userName, repositoryName, "invalid", "e6e6e6")
|
||||||
|
createLabel(userName, repositoryName, "question", "cc317c")
|
||||||
|
createLabel(userName, repositoryName, "wontfix", "ffffff")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -53,7 +53,30 @@ trait RepositorySearchService { self: IssuesService =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
def countWikiPages(owner: String, repository: String, query: String): Int =
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
|
if(JGitUtil.isEmpty(git)) 0 else searchRepositoryFiles(git, query).length
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchWikiPages(owner: String, repository: String, query: String): List[FileSearchResult] =
|
||||||
|
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
|
||||||
|
if(JGitUtil.isEmpty(git)){
|
||||||
|
Nil
|
||||||
|
} else {
|
||||||
|
val files = searchRepositoryFiles(git, query)
|
||||||
|
val commits = JGitUtil.getLatestCommitFromPaths(git, files.map(_._1), "HEAD")
|
||||||
|
files.map { case (path, text) =>
|
||||||
|
val (highlightText, lineNumber) = getHighlightText(text, query)
|
||||||
|
FileSearchResult(
|
||||||
|
path.replaceFirst("\\.md$", ""),
|
||||||
|
commits(path).getCommitterIdent.getWhen,
|
||||||
|
highlightText,
|
||||||
|
lineNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchRepositoryFiles(git: Git, query: String): List[(String, String)] = {
|
||||||
val revWalk = new RevWalk(git.getRepository)
|
val revWalk = new RevWalk(git.getRepository)
|
||||||
val objectId = git.getRepository.resolve("HEAD")
|
val objectId = git.getRepository.resolve("HEAD")
|
||||||
val revCommit = revWalk.parseCommit(objectId)
|
val revCommit = revWalk.parseCommit(objectId)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ trait RepositoryService { self: AccountService =>
|
|||||||
* @param originRepositoryName specify for the forked repository. (default is None)
|
* @param originRepositoryName specify for the forked repository. (default is None)
|
||||||
* @param originUserName specify for the forked repository. (default is None)
|
* @param originUserName specify for the forked repository. (default is None)
|
||||||
*/
|
*/
|
||||||
def createRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
|
def insertRepository(repositoryName: String, userName: String, description: Option[String], isPrivate: Boolean,
|
||||||
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
|
originRepositoryName: Option[String] = None, originUserName: Option[String] = None,
|
||||||
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
|
parentRepositoryName: Option[String] = None, parentUserName: Option[String] = None)
|
||||||
(implicit s: Session): Unit = {
|
(implicit s: Session): Unit = {
|
||||||
@@ -417,9 +417,7 @@ object RepositoryService {
|
|||||||
def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git"
|
def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git"
|
||||||
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
|
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
|
||||||
if(context.settings.ssh){
|
if(context.settings.ssh){
|
||||||
context.loginAccount.flatMap { loginAccount =>
|
context.settings.sshAddress.map { x => s"ssh://${x.genericUser}@${x.host}:${x.port}/${owner}/${name}.git" }
|
||||||
context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" }
|
|
||||||
}
|
|
||||||
} else None
|
} else None
|
||||||
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
|
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ trait SshKeyService {
|
|||||||
def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
|
def getPublicKeys(userName: String)(implicit s: Session): List[SshKey] =
|
||||||
SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list
|
SshKeys.filter(_.userName === userName.bind).sortBy(_.sshKeyId).list
|
||||||
|
|
||||||
|
def getAllKeys()(implicit s: Session): List[SshKey] =
|
||||||
|
SshKeys.filter(_.publicKey.trim =!= "").list
|
||||||
|
|
||||||
def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
|
def deletePublicKey(userName: String, sshKeyId: Int)(implicit s: Session): Unit =
|
||||||
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
|
SshKeys filter (_.byPrimaryKey(userName, sshKeyId)) delete
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,11 @@ object SystemSettingsService {
|
|||||||
for {
|
for {
|
||||||
host <- sshHost if ssh
|
host <- sshHost if ssh
|
||||||
}
|
}
|
||||||
yield SshAddress(host, sshPort.getOrElse(DefaultSshPort))
|
yield SshAddress(
|
||||||
|
host,
|
||||||
|
sshPort.getOrElse(DefaultSshPort),
|
||||||
|
"git"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Ldap(
|
case class Ldap(
|
||||||
@@ -169,7 +173,8 @@ object SystemSettingsService {
|
|||||||
|
|
||||||
case class SshAddress(
|
case class SshAddress(
|
||||||
host:String,
|
host:String,
|
||||||
port:Int)
|
port:Int,
|
||||||
|
genericUser:String)
|
||||||
|
|
||||||
val DefaultSshPort = 29418
|
val DefaultSshPort = 29418
|
||||||
val DefaultSmtpPort = 25
|
val DefaultSmtpPort = 25
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package gitbucket.core.service
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import fr.brouillard.oss.security.xhub.XHub
|
||||||
|
import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter}
|
||||||
import gitbucket.core.api._
|
import gitbucket.core.api._
|
||||||
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
|
||||||
import gitbucket.core.model.Profile._
|
import gitbucket.core.model.Profile._
|
||||||
|
import org.apache.http.client.utils.URLEncodedUtils
|
||||||
import profile.simple._
|
import profile.simple._
|
||||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
import gitbucket.core.util.JGitUtil.CommitInfo
|
||||||
import gitbucket.core.util.RepositoryName
|
import gitbucket.core.util.RepositoryName
|
||||||
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
import gitbucket.core.service.RepositoryService.RepositoryInfo
|
||||||
|
|
||||||
import org.apache.http.NameValuePair
|
import org.apache.http.NameValuePair
|
||||||
import org.apache.http.client.entity.UrlEncodedFormEntity
|
import org.apache.http.client.entity.UrlEncodedFormEntity
|
||||||
import org.apache.http.message.BasicNameValuePair
|
import org.apache.http.message.BasicNameValuePair
|
||||||
@@ -17,6 +20,8 @@ import org.slf4j.LoggerFactory
|
|||||||
import scala.concurrent._
|
import scala.concurrent._
|
||||||
import org.apache.http.HttpRequest
|
import org.apache.http.HttpRequest
|
||||||
import org.apache.http.HttpResponse
|
import org.apache.http.HttpResponse
|
||||||
|
import gitbucket.core.model.WebHookContentType
|
||||||
|
import org.apache.http.client.entity.EntityBuilder
|
||||||
|
|
||||||
|
|
||||||
trait WebHookService {
|
trait WebHookService {
|
||||||
@@ -33,8 +38,11 @@ trait WebHookService {
|
|||||||
|
|
||||||
/** get All WebHook informations of repository event */
|
/** get All WebHook informations of repository event */
|
||||||
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
|
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
|
||||||
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind)
|
WebHooks.filter(_.byRepository(owner, repository))
|
||||||
.list.map(t => WebHook(t.userName, t.repositoryName, t.url))
|
.innerJoin(WebHookEvents).on { (wh, whe) => whe.byWebHook(wh) }
|
||||||
|
.filter{ case (wh, whe) => whe.event === event.bind}
|
||||||
|
.map{ case (wh, whe) => wh }
|
||||||
|
.list.distinct
|
||||||
|
|
||||||
/** get All WebHook information from repository to url */
|
/** get All WebHook information from repository to url */
|
||||||
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
|
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
|
||||||
@@ -44,14 +52,15 @@ trait WebHookService {
|
|||||||
.map{ case (w,t) => w -> t.event }
|
.map{ case (w,t) => w -> t.event }
|
||||||
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
|
||||||
|
|
||||||
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
|
||||||
WebHooks insert WebHook(owner, repository, url)
|
WebHooks insert WebHook(owner, repository, url, ctype, token)
|
||||||
events.toSet.map{ event: WebHook.Event =>
|
events.toSet.map{ event: WebHook.Event =>
|
||||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
|
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event], ctype: WebHookContentType, token: Option[String])(implicit s: Session): Unit = {
|
||||||
|
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).map(w => (w.ctype, w.token)).update((ctype, token))
|
||||||
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
|
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
|
||||||
events.toSet.map{ event: WebHook.Event =>
|
events.toSet.map{ event: WebHook.Event =>
|
||||||
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
WebHookEvents insert WebHookEvent(owner, repository, url, event)
|
||||||
@@ -69,17 +78,17 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload)
|
def callWebHook(event: WebHook.Event, webHooks: List[WebHook], payload: WebHookPayload)
|
||||||
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
|
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
|
||||||
import org.apache.http.impl.client.HttpClientBuilder
|
import org.apache.http.impl.client.HttpClientBuilder
|
||||||
import ExecutionContext.Implicits.global
|
import ExecutionContext.Implicits.global
|
||||||
import org.apache.http.protocol.HttpContext
|
import org.apache.http.protocol.HttpContext
|
||||||
import org.apache.http.client.methods.HttpPost
|
import org.apache.http.client.methods.HttpPost
|
||||||
|
|
||||||
if(webHookURLs.nonEmpty){
|
if(webHooks.nonEmpty){
|
||||||
val json = JsonFormat(payload)
|
val json = JsonFormat(payload)
|
||||||
|
|
||||||
webHookURLs.map { webHookUrl =>
|
webHooks.map { webHook =>
|
||||||
val reqPromise = Promise[HttpRequest]
|
val reqPromise = Promise[HttpRequest]
|
||||||
val f = Future {
|
val f = Future {
|
||||||
val itcp = new org.apache.http.HttpRequestInterceptor{
|
val itcp = new org.apache.http.HttpRequestInterceptor{
|
||||||
@@ -88,20 +97,37 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
try{
|
try{
|
||||||
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
|
val httpClient = HttpClientBuilder.create.useSystemProperties.addInterceptorLast(itcp).build
|
||||||
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
|
logger.debug(s"start web hook invocation for ${webHook.url}")
|
||||||
val httpPost = new HttpPost(webHookUrl.url)
|
val httpPost = new HttpPost(webHook.url)
|
||||||
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
|
logger.info(s"Content-Type: ${webHook.ctype.ctype}")
|
||||||
|
httpPost.addHeader("Content-Type", webHook.ctype.ctype)
|
||||||
httpPost.addHeader("X-Github-Event", event.name)
|
httpPost.addHeader("X-Github-Event", event.name)
|
||||||
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
|
||||||
|
|
||||||
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
webHook.ctype match {
|
||||||
params.add(new BasicNameValuePair("payload", json))
|
case WebHookContentType.FORM => {
|
||||||
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
|
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
|
||||||
|
params.add(new BasicNameValuePair("payload", json))
|
||||||
|
def postContent = new UrlEncodedFormEntity(params, "UTF-8")
|
||||||
|
httpPost.setEntity(postContent)
|
||||||
|
if (webHook.token.exists(_.trim.nonEmpty)) {
|
||||||
|
// TODO find a better way and see how to extract content from postContent
|
||||||
|
val contentAsBytes = URLEncodedUtils.format(params, "UTF-8").getBytes("UTF-8")
|
||||||
|
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.get, contentAsBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case WebHookContentType.JSON => {
|
||||||
|
httpPost.setEntity(EntityBuilder.create().setText(json).build())
|
||||||
|
if (!webHook.token.isEmpty) {
|
||||||
|
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, json.getBytes("UTF-8")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val res = httpClient.execute(httpPost)
|
val res = httpClient.execute(httpPost)
|
||||||
httpPost.releaseConnection()
|
httpPost.releaseConnection()
|
||||||
logger.debug(s"end web hook invocation for ${webHookUrl}")
|
logger.debug(s"end web hook invocation for ${webHook}")
|
||||||
res
|
res
|
||||||
}catch{
|
}catch{
|
||||||
case e:Throwable => {
|
case e:Throwable => {
|
||||||
@@ -113,12 +139,12 @@ trait WebHookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.onSuccess {
|
f.onSuccess {
|
||||||
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
|
case s => logger.debug(s"Success: web hook request to ${webHook.url}")
|
||||||
}
|
}
|
||||||
f.onFailure {
|
f.onFailure {
|
||||||
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
|
case t => logger.error(s"Failed: web hook request to ${webHook.url}", t)
|
||||||
}
|
}
|
||||||
(webHookUrl, json, reqPromise.future, f)
|
(webHook, json, reqPromise.future, f)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Nil
|
Nil
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
package gitbucket.core.servlet
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
import java.sql.{DriverManager, Connection}
|
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
|
||||||
import gitbucket.core.service.SystemSettingsService
|
|
||||||
import gitbucket.core.util._
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import javax.servlet.{ServletContextListener, ServletContextEvent}
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import Directory._
|
|
||||||
import ControlUtil._
|
|
||||||
import JDBCUtil._
|
|
||||||
import org.eclipse.jgit.api.Git
|
|
||||||
import gitbucket.core.util.Versions
|
|
||||||
import gitbucket.core.util.Directory
|
|
||||||
|
|
||||||
object AutoUpdate {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The history of versions. A head of this sequence is the current GitBucket version.
|
|
||||||
*/
|
|
||||||
val versions = Seq(
|
|
||||||
new Version(3, 12),
|
|
||||||
new Version(3, 11),
|
|
||||||
new Version(3, 10),
|
|
||||||
new Version(3, 9),
|
|
||||||
new Version(3, 8),
|
|
||||||
new Version(3, 7) with SystemSettingsService {
|
|
||||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
|
||||||
super.update(conn, cl)
|
|
||||||
val settings = loadSystemSettings()
|
|
||||||
if(settings.notification){
|
|
||||||
saveSystemSettings(settings.copy(useSMTP = true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Version(3, 6),
|
|
||||||
new Version(3, 5),
|
|
||||||
new Version(3, 4),
|
|
||||||
new Version(3, 3),
|
|
||||||
new Version(3, 2),
|
|
||||||
new Version(3, 1),
|
|
||||||
new Version(3, 0),
|
|
||||||
new Version(2, 8),
|
|
||||||
new Version(2, 7) {
|
|
||||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
|
||||||
super.update(conn, cl)
|
|
||||||
conn.select("SELECT * FROM REPOSITORY"){ rs =>
|
|
||||||
// Rename attached files directory from /issues to /comments
|
|
||||||
val userName = rs.getString("USER_NAME")
|
|
||||||
val repoName = rs.getString("REPOSITORY_NAME")
|
|
||||||
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
|
|
||||||
val oldDir = new File(newDir.getParentFile, "issues")
|
|
||||||
if(oldDir.exists && oldDir.isDirectory){
|
|
||||||
oldDir.renameTo(newDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
|
|
||||||
val originalUserName = rs.getString("ORIGIN_USER_NAME")
|
|
||||||
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
|
|
||||||
if(originalUserName != null && originalRepoName != null){
|
|
||||||
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
|
||||||
originalUserName, originalRepoName) == 0){
|
|
||||||
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
|
|
||||||
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
|
|
||||||
val parentUserName = rs.getString("PARENT_USER_NAME")
|
|
||||||
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
|
|
||||||
if(parentUserName != null && parentRepoName != null){
|
|
||||||
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
|
|
||||||
parentUserName, parentRepoName) == 0){
|
|
||||||
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
|
|
||||||
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Version(2, 6),
|
|
||||||
new Version(2, 5),
|
|
||||||
new Version(2, 4),
|
|
||||||
new Version(2, 3) {
|
|
||||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
|
||||||
super.update(conn, cl)
|
|
||||||
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
|
|
||||||
val curInfo = rs.getString("ADDITIONAL_INFO")
|
|
||||||
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
|
|
||||||
if (curInfo != newInfo) {
|
|
||||||
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ignore {
|
|
||||||
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
|
|
||||||
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new Version(2, 2),
|
|
||||||
new Version(2, 1),
|
|
||||||
new Version(2, 0){
|
|
||||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
|
||||||
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
|
|
||||||
|
|
||||||
val mimeUtil = new MimeUtil2()
|
|
||||||
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
|
|
||||||
|
|
||||||
super.update(conn, cl)
|
|
||||||
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
|
||||||
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
|
|
||||||
if(dir.exists && dir.isDirectory){
|
|
||||||
dir.listFiles.foreach { file =>
|
|
||||||
if(file.getName.indexOf('.') < 0){
|
|
||||||
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
|
|
||||||
if(mimeType.startsWith("image/")){
|
|
||||||
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Version(1, 13),
|
|
||||||
Version(1, 12),
|
|
||||||
Version(1, 11),
|
|
||||||
Version(1, 10),
|
|
||||||
Version(1, 9),
|
|
||||||
Version(1, 8),
|
|
||||||
Version(1, 7),
|
|
||||||
Version(1, 6),
|
|
||||||
Version(1, 5),
|
|
||||||
Version(1, 4),
|
|
||||||
new Version(1, 3){
|
|
||||||
override def update(conn: Connection, cl: ClassLoader): Unit = {
|
|
||||||
super.update(conn, cl)
|
|
||||||
// Fix wiki repository configuration
|
|
||||||
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
|
|
||||||
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
|
|
||||||
defining(git.getRepository.getConfig){ config =>
|
|
||||||
if(!config.getBoolean("http", "receivepack", false)){
|
|
||||||
config.setBoolean("http", null, "receivepack", true)
|
|
||||||
config.save
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Version(1, 2),
|
|
||||||
Version(1, 1),
|
|
||||||
Version(1, 0),
|
|
||||||
Version(0, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The head version of GitBucket.
|
|
||||||
*/
|
|
||||||
val headVersion = versions.head
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The version file (GITBUCKET_HOME/version).
|
|
||||||
*/
|
|
||||||
lazy val versionFile = new File(GitBucketHome, "version")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current version from the version file.
|
|
||||||
*/
|
|
||||||
def getCurrentVersion(): Version = {
|
|
||||||
if(versionFile.exists){
|
|
||||||
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
|
|
||||||
case Array(majorVersion, minorVersion) => {
|
|
||||||
versions.find { v =>
|
|
||||||
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
|
|
||||||
}.getOrElse(Version(0, 0))
|
|
||||||
}
|
|
||||||
case _ => Version(0, 0)
|
|
||||||
}
|
|
||||||
} else Version(0, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import gitbucket.core.service.WebHookService._
|
|||||||
import gitbucket.core.service._
|
import gitbucket.core.service._
|
||||||
import gitbucket.core.util.ControlUtil._
|
import gitbucket.core.util.ControlUtil._
|
||||||
import gitbucket.core.util.Implicits._
|
import gitbucket.core.util.Implicits._
|
||||||
import gitbucket.core.util.JGitUtil.CommitInfo
|
|
||||||
import gitbucket.core.util._
|
import gitbucket.core.util._
|
||||||
|
|
||||||
import org.eclipse.jgit.api.Git
|
import org.eclipse.jgit.api.Git
|
||||||
@@ -168,7 +167,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
|
||||||
if (issueCount > 0) {
|
if (issueCount > 0) {
|
||||||
pushedIds.add(commit.id)
|
pushedIds.add(commit.id)
|
||||||
createIssueComment(commit)
|
createIssueComment(owner, repository, commit)
|
||||||
// close issues
|
// close issues
|
||||||
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
if(refName(1) == "heads" && branchName == defaultBranch && command.getType == ReceiveCommand.Type.UPDATE){
|
||||||
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
closeIssuesFromMessage(commit.fullMessage, pusher, owner, repository)
|
||||||
@@ -230,13 +229,4 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def createIssueComment(commit: CommitInfo) = {
|
|
||||||
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
|
|
||||||
if(getIssue(owner, repository, issueId).isDefined){
|
|
||||||
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
|
|
||||||
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
package gitbucket.core.servlet
|
package gitbucket.core.servlet
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
import akka.event.Logging
|
import akka.event.Logging
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import gitbucket.core.GitBucketCoreModule
|
||||||
import gitbucket.core.plugin.PluginRegistry
|
import gitbucket.core.plugin.PluginRegistry
|
||||||
import gitbucket.core.service.{ActivityService, SystemSettingsService}
|
import gitbucket.core.service.{ActivityService, SystemSettingsService}
|
||||||
import org.apache.commons.io.FileUtils
|
import gitbucket.core.util.DatabaseConfig
|
||||||
|
import gitbucket.core.util.Directory._
|
||||||
|
import gitbucket.core.util.JDBCUtil._
|
||||||
|
import io.github.gitbucket.solidbase.Solidbase
|
||||||
|
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
|
||||||
import javax.servlet.{ServletContextListener, ServletContextEvent}
|
import javax.servlet.{ServletContextListener, ServletContextEvent}
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import gitbucket.core.util.Versions
|
|
||||||
import akka.actor.{Actor, Props, ActorSystem}
|
import akka.actor.{Actor, Props, ActorSystem}
|
||||||
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
|
||||||
import AutoUpdate._
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize GitBucket system.
|
* Initialize GitBucket system.
|
||||||
@@ -30,14 +36,49 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
|
|||||||
Database() withTransaction { session =>
|
Database() withTransaction { session =>
|
||||||
val conn = session.conn
|
val conn = session.conn
|
||||||
|
|
||||||
// Migration
|
// Check version
|
||||||
logger.debug("Start schema update")
|
val versionFile = new File(GitBucketHome, "version")
|
||||||
Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
|
|
||||||
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
|
if(versionFile.exists()){
|
||||||
|
val version = FileUtils.readFileToString(versionFile, "UTF-8")
|
||||||
|
if(version == "3.14"){
|
||||||
|
// Initialization for GitBucket 3.14
|
||||||
|
logger.info("Migration to GitBucket 4.x start")
|
||||||
|
|
||||||
|
// Backup current data
|
||||||
|
val dataMvFile = new File(GitBucketHome, "data.mv.db")
|
||||||
|
if(dataMvFile.exists) {
|
||||||
|
FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
|
||||||
|
}
|
||||||
|
val dataTraceFile = new File(GitBucketHome, "data.trace.db")
|
||||||
|
if(dataTraceFile.exists) {
|
||||||
|
FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change form
|
||||||
|
val manager = new JDBCVersionManager(conn)
|
||||||
|
manager.initialize()
|
||||||
|
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0")
|
||||||
|
conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
|
||||||
|
manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
|
||||||
|
}
|
||||||
|
conn.update("DROP TABLE PLUGIN")
|
||||||
|
versionFile.delete()
|
||||||
|
|
||||||
|
logger.info("Migration to GitBucket 4.x completed")
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run normal migration
|
||||||
|
logger.info("Start schema update")
|
||||||
|
val solidbase = new Solidbase()
|
||||||
|
solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
|
||||||
|
|
||||||
// Load plugins
|
// Load plugins
|
||||||
logger.debug("Initialize plugins")
|
logger.info("Initialize plugins")
|
||||||
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
|
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package gitbucket.core.servlet
|
|||||||
|
|
||||||
import javax.servlet._
|
import javax.servlet._
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
import com.mchange.v2.c3p0.ComboPooledDataSource
|
import com.zaxxer.hikari._
|
||||||
import gitbucket.core.util.DatabaseConfig
|
import gitbucket.core.util.DatabaseConfig
|
||||||
import org.scalatra.ScalatraBase
|
import org.scalatra.ScalatraBase
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@@ -46,14 +46,14 @@ object Database {
|
|||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(Database.getClass)
|
private val logger = LoggerFactory.getLogger(Database.getClass)
|
||||||
|
|
||||||
private val dataSource: ComboPooledDataSource = {
|
private val dataSource: HikariDataSource = {
|
||||||
val ds = new ComboPooledDataSource
|
val config = new HikariConfig()
|
||||||
ds.setDriverClass(DatabaseConfig.driver)
|
config.setDriverClassName(DatabaseConfig.jdbcDriver)
|
||||||
ds.setJdbcUrl(DatabaseConfig.url)
|
config.setJdbcUrl(DatabaseConfig.url)
|
||||||
ds.setUser(DatabaseConfig.user)
|
config.setUsername(DatabaseConfig.user)
|
||||||
ds.setPassword(DatabaseConfig.password)
|
config.setPassword(DatabaseConfig.password)
|
||||||
logger.debug("load database connection pool")
|
logger.debug("load database connection pool")
|
||||||
ds
|
new HikariDataSource(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val db: SlickDatabase = {
|
private val db: SlickDatabase = {
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
|
|||||||
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
|
||||||
import gitbucket.core.servlet.{Database, CommitLogHook}
|
import gitbucket.core.servlet.{Database, CommitLogHook}
|
||||||
import gitbucket.core.util.{Directory, ControlUtil}
|
import gitbucket.core.util.{Directory, ControlUtil}
|
||||||
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command}
|
import org.apache.sshd.server.{CommandFactory, Environment, ExitCallback, Command, SessionAware}
|
||||||
|
import org.apache.sshd.server.session.ServerSession
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.io.{File, InputStream, OutputStream}
|
import java.io.{File, InputStream, OutputStream}
|
||||||
import ControlUtil._
|
import ControlUtil._
|
||||||
@@ -20,37 +21,44 @@ object GitCommand {
|
|||||||
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
|
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GitCommand() extends Command {
|
abstract class GitCommand extends Command with SessionAware {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
|
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
|
||||||
@volatile protected var err: OutputStream = null
|
@volatile protected var err: OutputStream = null
|
||||||
@volatile protected var in: InputStream = null
|
@volatile protected var in: InputStream = null
|
||||||
@volatile protected var out: OutputStream = null
|
@volatile protected var out: OutputStream = null
|
||||||
@volatile protected var callback: ExitCallback = null
|
@volatile protected var callback: ExitCallback = null
|
||||||
|
@volatile private var authUser:Option[String] = None
|
||||||
|
|
||||||
protected def runTask(user: String)(implicit session: Session): Unit
|
protected def runTask(authUser: String)(implicit session: Session): Unit
|
||||||
|
|
||||||
private def newTask(user: String): Runnable = new Runnable {
|
private def newTask(): Runnable = new Runnable {
|
||||||
override def run(): Unit = {
|
override def run(): Unit = {
|
||||||
Database() withSession { implicit session =>
|
authUser match {
|
||||||
try {
|
case Some(authUser) =>
|
||||||
runTask(user)
|
Database() withSession { implicit session =>
|
||||||
callback.onExit(0)
|
try {
|
||||||
} catch {
|
runTask(authUser)
|
||||||
case e: RepositoryNotFoundException =>
|
callback.onExit(0)
|
||||||
logger.info(e.getMessage)
|
} catch {
|
||||||
callback.onExit(1, "Repository Not Found")
|
case e: RepositoryNotFoundException =>
|
||||||
case e: Throwable =>
|
logger.info(e.getMessage)
|
||||||
logger.error(e.getMessage, e)
|
callback.onExit(1, "Repository Not Found")
|
||||||
callback.onExit(1)
|
case e: Throwable =>
|
||||||
}
|
logger.error(e.getMessage, e)
|
||||||
|
callback.onExit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case None =>
|
||||||
|
val message = "User not authenticated"
|
||||||
|
logger.error(message)
|
||||||
|
callback.onExit(1, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override def start(env: Environment): Unit = {
|
final override def start(env: Environment): Unit = {
|
||||||
val user = env.getEnv.get("USER")
|
val thread = new Thread(newTask())
|
||||||
val thread = new Thread(newTask(user))
|
|
||||||
thread.start()
|
thread.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +80,10 @@ abstract class GitCommand() extends Command {
|
|||||||
this.in = in
|
this.in = in
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def setSession(serverSession:ServerSession) {
|
||||||
|
this.authUser = PublicKeyAuthenticator.getUserName(serverSession)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class DefaultGitCommand(val owner: String, val repoName: String) extends GitCommand {
|
abstract class DefaultGitCommand(val owner: String, val repoName: String) extends GitCommand {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ class NoShell(sshAddress:SshAddress) extends Factory[Command] {
|
|||||||
private var callback: ExitCallback = null
|
private var callback: ExitCallback = null
|
||||||
|
|
||||||
override def start(env: Environment): Unit = {
|
override def start(env: Environment): Unit = {
|
||||||
val user = env.getEnv.get("USER")
|
|
||||||
val message =
|
val message =
|
||||||
"""
|
"""
|
||||||
| Welcome to
|
| Welcome to
|
||||||
@@ -32,7 +31,7 @@ class NoShell(sshAddress:SshAddress) extends Factory[Command] {
|
|||||||
| Please use:
|
| Please use:
|
||||||
|
|
|
|
||||||
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
|
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
|
||||||
""".stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
|
""".stripMargin.format(sshAddress.genericUser, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
|
||||||
err.write(Constants.encode(message))
|
err.write(Constants.encode(message))
|
||||||
err.flush()
|
err.flush()
|
||||||
in.close()
|
in.close()
|
||||||
|
|||||||
@@ -2,22 +2,73 @@ package gitbucket.core.ssh
|
|||||||
|
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
import gitbucket.core.model.SshKey
|
||||||
import gitbucket.core.service.SshKeyService
|
import gitbucket.core.service.SshKeyService
|
||||||
import gitbucket.core.servlet.Database
|
import gitbucket.core.servlet.Database
|
||||||
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
||||||
import org.apache.sshd.server.session.ServerSession
|
import org.apache.sshd.server.session.ServerSession
|
||||||
|
import org.apache.sshd.common.session.Session
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
class PublicKeyAuthenticator extends PublickeyAuthenticator with SshKeyService {
|
object PublicKeyAuthenticator {
|
||||||
|
// put in the ServerSession here to be read by GitCommand later
|
||||||
|
private val userNameSessionKey = new Session.AttributeKey[String]
|
||||||
|
|
||||||
override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean = {
|
def putUserName(serverSession:ServerSession, userName:String):Unit =
|
||||||
Database() withSession { implicit session =>
|
serverSession.setAttribute(userNameSessionKey, userName)
|
||||||
getPublicKeys(username).exists { sshKey =>
|
|
||||||
SshUtil.str2PublicKey(sshKey.publicKey) match {
|
def getUserName(serverSession:ServerSession):Option[String] =
|
||||||
case Some(publicKey) => key.equals(publicKey)
|
Option(serverSession.getAttribute(userNameSessionKey))
|
||||||
case _ => false
|
}
|
||||||
}
|
|
||||||
}
|
class PublicKeyAuthenticator(genericUser:String) extends PublickeyAuthenticator with SshKeyService {
|
||||||
|
private val logger = LoggerFactory.getLogger(classOf[PublicKeyAuthenticator])
|
||||||
|
|
||||||
|
override def authenticate(username: String, key: PublicKey, session: ServerSession): Boolean =
|
||||||
|
if (username == genericUser) authenticateGenericUser(username, key, session, genericUser)
|
||||||
|
else authenticateLoginUser(username, key, session)
|
||||||
|
|
||||||
|
private def authenticateLoginUser(username: String, key: PublicKey, session: ServerSession): Boolean = {
|
||||||
|
val authenticated =
|
||||||
|
Database()
|
||||||
|
.withSession { implicit dbSession => getPublicKeys(username) }
|
||||||
|
.map(_.publicKey)
|
||||||
|
.flatMap(SshUtil.str2PublicKey)
|
||||||
|
.contains(key)
|
||||||
|
if (authenticated) {
|
||||||
|
logger.info(s"authentication as ssh user ${username} succeeded")
|
||||||
|
PublicKeyAuthenticator.putUserName(session, username)
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
logger.info(s"authentication as ssh user ${username} failed")
|
||||||
|
}
|
||||||
|
authenticated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def authenticateGenericUser(username: String, key: PublicKey, session: ServerSession, genericUser:String): Boolean = {
|
||||||
|
// find all users having the key we got from ssh
|
||||||
|
val possibleUserNames =
|
||||||
|
Database()
|
||||||
|
.withSession { implicit dbSession => getAllKeys() }
|
||||||
|
.filter { sshKey =>
|
||||||
|
SshUtil.str2PublicKey(sshKey.publicKey).exists(_ == key)
|
||||||
|
}
|
||||||
|
.map(_.userName)
|
||||||
|
.distinct
|
||||||
|
// determine the user - if different accounts share the same key, tough luck
|
||||||
|
val uniqueUserName =
|
||||||
|
possibleUserNames match {
|
||||||
|
case List() =>
|
||||||
|
logger.info(s"authentication as generic user ${genericUser} failed, public key not found")
|
||||||
|
None
|
||||||
|
case List(name) =>
|
||||||
|
logger.info(s"authentication as generic user ${genericUser} succeeded, identified ${name}")
|
||||||
|
Some(name)
|
||||||
|
case _ =>
|
||||||
|
logger.info(s"authentication as generic user ${genericUser} failed, public key is ambiguous")
|
||||||
|
None
|
||||||
|
}
|
||||||
|
uniqueUserName.foreach(PublicKeyAuthenticator.putUserName(session, _))
|
||||||
|
uniqueUserName.isDefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ object SshServer {
|
|||||||
provider.setAlgorithm("RSA")
|
provider.setAlgorithm("RSA")
|
||||||
provider.setOverwriteAllowed(false)
|
provider.setOverwriteAllowed(false)
|
||||||
server.setKeyPairProvider(provider)
|
server.setKeyPairProvider(provider)
|
||||||
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
|
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
|
||||||
server.setCommandFactory(new GitCommandFactory(baseUrl))
|
server.setCommandFactory(new GitCommandFactory(baseUrl))
|
||||||
server.setShellFactory(new NoShell(sshAddress))
|
server.setShellFactory(new NoShell(sshAddress))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,7 @@ object SshUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def fingerPrint(key: String): Option[String] = str2PublicKey(key) match {
|
def fingerPrint(key: String): Option[String] =
|
||||||
case Some(publicKey) => Some(KeyUtils.getFingerPrint(publicKey))
|
str2PublicKey(key) map KeyUtils.getFingerPrint
|
||||||
case None => None
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,83 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import Directory.DatabaseHome
|
import java.io.File
|
||||||
|
import Directory._
|
||||||
|
import liquibase.database.AbstractJdbcDatabase
|
||||||
|
import liquibase.database.core.{PostgresDatabase, MySQLDatabase, H2Database}
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
|
||||||
object DatabaseConfig {
|
object DatabaseConfig {
|
||||||
|
|
||||||
private val config = ConfigFactory.load("database")
|
private lazy val config = {
|
||||||
private val dbUrl = config.getString("db.url")
|
val file = new File(GitBucketHome, "database.conf")
|
||||||
|
if(!file.exists){
|
||||||
|
FileUtils.write(file,
|
||||||
|
"""db {
|
||||||
|
| url = "jdbc:h2:${DatabaseHome};MVCC=true"
|
||||||
|
| user = "sa"
|
||||||
|
| password = "sa"
|
||||||
|
|}
|
||||||
|
|""".stripMargin, "UTF-8")
|
||||||
|
}
|
||||||
|
ConfigFactory.parseFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy val dbUrl = config.getString("db.url")
|
||||||
|
|
||||||
def url(directory: Option[String]): String =
|
def url(directory: Option[String]): String =
|
||||||
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
|
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
|
||||||
|
|
||||||
val url: String = url(None)
|
lazy val url: String = url(None)
|
||||||
val user: String = config.getString("db.user")
|
lazy val user: String = config.getString("db.user")
|
||||||
val password: String = config.getString("db.password")
|
lazy val password: String = config.getString("db.password")
|
||||||
val driver: String = config.getString("db.driver")
|
lazy val jdbcDriver: String = DatabaseType(url).jdbcDriver
|
||||||
|
lazy val slickDriver: slick.driver.JdbcProfile = DatabaseType(url).slickDriver
|
||||||
|
lazy val liquiDriver: AbstractJdbcDatabase = DatabaseType(url).liquiDriver
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed trait DatabaseType {
|
||||||
|
val jdbcDriver: String
|
||||||
|
val slickDriver: slick.driver.JdbcProfile
|
||||||
|
val liquiDriver: AbstractJdbcDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
object DatabaseType {
|
||||||
|
|
||||||
|
def apply(url: String): DatabaseType = {
|
||||||
|
if(url.startsWith("jdbc:h2:")){
|
||||||
|
H2
|
||||||
|
} else if(url.startsWith("jdbc:mysql:")){
|
||||||
|
MySQL
|
||||||
|
} else if(url.startsWith("jdbc:postgresql:")){
|
||||||
|
PostgreSQL
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(s"${url} is not supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object H2 extends DatabaseType {
|
||||||
|
val jdbcDriver = "org.h2.Driver"
|
||||||
|
val slickDriver = slick.driver.H2Driver
|
||||||
|
val liquiDriver = new H2Database()
|
||||||
|
}
|
||||||
|
|
||||||
|
object MySQL extends DatabaseType {
|
||||||
|
val jdbcDriver = "com.mysql.jdbc.Driver"
|
||||||
|
val slickDriver = slick.driver.MySQLDriver
|
||||||
|
val liquiDriver = new MySQLDatabase()
|
||||||
|
}
|
||||||
|
|
||||||
|
object PostgreSQL extends DatabaseType {
|
||||||
|
val jdbcDriver = "org.postgresql.Driver2"
|
||||||
|
val slickDriver = new slick.driver.PostgresDriver {
|
||||||
|
override def quoteIdentifier(id: String): String = {
|
||||||
|
val s = new StringBuilder(id.length + 4) append '"'
|
||||||
|
for(c <- id) if(c == '"') s append "\"\"" else s append c.toLower
|
||||||
|
(s append '"').toString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val liquiDriver = new PostgresDatabase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
|
import java.io._
|
||||||
import java.sql._
|
import java.sql._
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import javax.xml.stream.{XMLStreamConstants, XMLInputFactory, XMLOutputFactory}
|
||||||
import ControlUtil._
|
import ControlUtil._
|
||||||
|
import scala.StringBuilder
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.collection.mutable
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,6 +64,265 @@ object JDBCUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def importAsXML(in: InputStream): Unit = {
|
||||||
|
conn.setAutoCommit(false)
|
||||||
|
try {
|
||||||
|
val factory = XMLInputFactory.newInstance()
|
||||||
|
using(factory.createXMLStreamReader(in, "UTF-8")){ reader =>
|
||||||
|
// stateful objects
|
||||||
|
var elementName = ""
|
||||||
|
var insertTable = ""
|
||||||
|
var insertColumns = Map.empty[String, (String, String)]
|
||||||
|
|
||||||
|
while(reader.hasNext){
|
||||||
|
reader.next()
|
||||||
|
|
||||||
|
reader.getEventType match {
|
||||||
|
case XMLStreamConstants.START_ELEMENT =>
|
||||||
|
elementName = reader.getName.getLocalPart
|
||||||
|
if(elementName == "insert"){
|
||||||
|
insertTable = reader.getAttributeValue(null, "table")
|
||||||
|
} else if(elementName == "delete"){
|
||||||
|
val tableName = reader.getAttributeValue(null, "table")
|
||||||
|
conn.update(s"DELETE FROM ${tableName}")
|
||||||
|
} else if(elementName == "column"){
|
||||||
|
val columnName = reader.getAttributeValue(null, "name")
|
||||||
|
val columnType = reader.getAttributeValue(null, "type")
|
||||||
|
val columnValue = reader.getElementText
|
||||||
|
insertColumns = insertColumns + (columnName -> (columnType, columnValue))
|
||||||
|
}
|
||||||
|
case XMLStreamConstants.END_ELEMENT =>
|
||||||
|
// Execute insert statement
|
||||||
|
reader.getName.getLocalPart match {
|
||||||
|
case "insert" => {
|
||||||
|
val sb = new StringBuilder()
|
||||||
|
sb.append(s"INSERT INTO ${insertTable} (")
|
||||||
|
sb.append(insertColumns.map { case (columnName, _) => columnName }.mkString(", "))
|
||||||
|
sb.append(") VALUES (")
|
||||||
|
sb.append(insertColumns.map { case (_, (columnType, columnValue)) =>
|
||||||
|
if(columnType == null || columnValue == null){
|
||||||
|
"NULL"
|
||||||
|
} else if(columnType == "string"){
|
||||||
|
"'" + columnValue.replace("'", "''") + "'"
|
||||||
|
} else if(columnType == "timestamp"){
|
||||||
|
"'" + columnValue + "'"
|
||||||
|
} else {
|
||||||
|
columnValue.toString
|
||||||
|
}
|
||||||
|
}.mkString(", "))
|
||||||
|
sb.append(")")
|
||||||
|
|
||||||
|
conn.update(sb.toString)
|
||||||
|
|
||||||
|
insertColumns = Map.empty[String, (String, String)] // Clear column information
|
||||||
|
}
|
||||||
|
case _ => // Nothing to do
|
||||||
|
}
|
||||||
|
case _ => // Nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
case e: Exception => {
|
||||||
|
conn.rollback()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def exportAsXML(targetTables: Seq[String]): File = {
|
||||||
|
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
||||||
|
val file = File.createTempFile("gitbucket-export-", ".xml")
|
||||||
|
|
||||||
|
val factory = XMLOutputFactory.newInstance()
|
||||||
|
using(factory.createXMLStreamWriter(new FileOutputStream(file), "UTF-8")){ writer =>
|
||||||
|
val dbMeta = conn.getMetaData
|
||||||
|
val allTablesInDatabase = allTablesOrderByDependencies(dbMeta)
|
||||||
|
|
||||||
|
writer.writeStartDocument("UTF-8", "1.0")
|
||||||
|
writer.writeStartElement("tables")
|
||||||
|
|
||||||
|
println(allTablesInDatabase.mkString(", "))
|
||||||
|
|
||||||
|
allTablesInDatabase.reverse.foreach { tableName =>
|
||||||
|
if (targetTables.contains(tableName)) {
|
||||||
|
writer.writeStartElement("delete")
|
||||||
|
writer.writeAttribute("table", tableName)
|
||||||
|
writer.writeEndElement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allTablesInDatabase.foreach { tableName =>
|
||||||
|
if (targetTables.contains(tableName)) {
|
||||||
|
select(s"SELECT * FROM ${tableName}") { rs =>
|
||||||
|
writer.writeStartElement("insert")
|
||||||
|
writer.writeAttribute("table", tableName)
|
||||||
|
val rsMeta = rs.getMetaData
|
||||||
|
(1 to rsMeta.getColumnCount).foreach { i =>
|
||||||
|
val columnName = rsMeta.getColumnName(i)
|
||||||
|
val (columnType, columnValue) = if(rs.getObject(columnName) == null){
|
||||||
|
(null, null)
|
||||||
|
} else {
|
||||||
|
rsMeta.getColumnType(i) match {
|
||||||
|
case Types.BOOLEAN | Types.BIT => ("boolean", rs.getBoolean(columnName))
|
||||||
|
case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => ("string", rs.getString(columnName))
|
||||||
|
case Types.INTEGER => ("int", rs.getInt(columnName))
|
||||||
|
case Types.TIMESTAMP => ("timestamp", dateFormat.format(rs.getTimestamp(columnName)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.writeStartElement("column")
|
||||||
|
writer.writeAttribute("name", columnName)
|
||||||
|
if(columnType != null){
|
||||||
|
writer.writeAttribute("type", columnType)
|
||||||
|
}
|
||||||
|
if(columnValue != null){
|
||||||
|
writer.writeCharacters(columnValue.toString)
|
||||||
|
}
|
||||||
|
writer.writeEndElement()
|
||||||
|
}
|
||||||
|
writer.writeEndElement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.writeEndElement()
|
||||||
|
writer.writeEndDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
def exportAsSQL(targetTables: Seq[String]): File = {
|
||||||
|
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
|
||||||
|
val file = File.createTempFile("gitbucket-export-", ".sql")
|
||||||
|
|
||||||
|
using(new FileOutputStream(file)) { out =>
|
||||||
|
val dbMeta = conn.getMetaData
|
||||||
|
val allTablesInDatabase = allTablesOrderByDependencies(dbMeta)
|
||||||
|
|
||||||
|
allTablesInDatabase.reverse.foreach { tableName =>
|
||||||
|
if (targetTables.contains(tableName)) {
|
||||||
|
out.write(s"DELETE FROM ${tableName};\n".getBytes("UTF-8"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allTablesInDatabase.foreach { tableName =>
|
||||||
|
if (targetTables.contains(tableName)) {
|
||||||
|
val sb = new StringBuilder()
|
||||||
|
select(s"SELECT * FROM ${tableName}") { rs =>
|
||||||
|
sb.append(s"INSERT INTO ${tableName} (")
|
||||||
|
|
||||||
|
val rsMeta = rs.getMetaData
|
||||||
|
val columns = (1 to rsMeta.getColumnCount).map { i =>
|
||||||
|
(rsMeta.getColumnName(i), rsMeta.getColumnType(i))
|
||||||
|
}
|
||||||
|
sb.append(columns.map(_._1).mkString(", "))
|
||||||
|
sb.append(") VALUES (")
|
||||||
|
|
||||||
|
val values = columns.map { case (columnName, columnType) =>
|
||||||
|
if(rs.getObject(columnName) == null){
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
columnType match {
|
||||||
|
case Types.BOOLEAN | Types.BIT => rs.getBoolean(columnName)
|
||||||
|
case Types.VARCHAR | Types.CLOB | Types.CHAR | Types.LONGVARCHAR => rs.getString(columnName)
|
||||||
|
case Types.INTEGER => rs.getInt(columnName)
|
||||||
|
case Types.TIMESTAMP => rs.getTimestamp(columnName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val columnValues = values.map { value =>
|
||||||
|
value match {
|
||||||
|
case x: String => "'" + x.replace("'", "''") + "'"
|
||||||
|
case x: Timestamp => "'" + dateFormat.format(x) + "'"
|
||||||
|
case null => "NULL"
|
||||||
|
case x => x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(columnValues.mkString(", "))
|
||||||
|
sb.append(");\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.write(sb.toString.getBytes("UTF-8"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file
|
||||||
|
}
|
||||||
|
|
||||||
|
def allTableNames(): Seq[String] = {
|
||||||
|
using(conn.getMetaData.getTables(null, null, "%", Seq("TABLE").toArray)) { rs =>
|
||||||
|
val tableNames = new ListBuffer[String]
|
||||||
|
while (rs.next) {
|
||||||
|
val name = rs.getString("TABLE_NAME").toUpperCase
|
||||||
|
if (name != "VERSIONS" && name != "PLUGIN") {
|
||||||
|
tableNames += name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tableNames.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def childTables(meta: DatabaseMetaData, tableName: String): Seq[String] = {
|
||||||
|
val normalizedTableName =
|
||||||
|
if(meta.getDatabaseProductName == "PostgreSQL"){
|
||||||
|
tableName.toLowerCase
|
||||||
|
} else {
|
||||||
|
tableName
|
||||||
|
}
|
||||||
|
|
||||||
|
using(meta.getExportedKeys(null, null, normalizedTableName)) { rs =>
|
||||||
|
val children = new ListBuffer[String]
|
||||||
|
while (rs.next) {
|
||||||
|
val childTableName = rs.getString("FKTABLE_NAME").toUpperCase
|
||||||
|
if(!children.contains(childTableName)){
|
||||||
|
children += childTableName
|
||||||
|
children ++= childTables(meta, childTableName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.distinct.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private def allTablesOrderByDependencies(meta: DatabaseMetaData): Seq[String] = {
|
||||||
|
val tables = allTableNames.map { tableName =>
|
||||||
|
val result = TableDependency(tableName, childTables(meta, tableName))
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
val edges = tables.flatMap { table =>
|
||||||
|
table.children.map { child => (table.tableName, child) }
|
||||||
|
}
|
||||||
|
|
||||||
|
tsort(edges).toSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
case class TableDependency(tableName: String, children: Seq[String])
|
||||||
|
|
||||||
|
|
||||||
|
def tsort[A](edges: Traversable[(A, A)]): Iterable[A] = {
|
||||||
|
@tailrec
|
||||||
|
def tsort(toPreds: Map[A, Set[A]], done: Iterable[A]): Iterable[A] = {
|
||||||
|
val (noPreds, hasPreds) = toPreds.partition { _._2.isEmpty }
|
||||||
|
if (noPreds.isEmpty) {
|
||||||
|
if (hasPreds.isEmpty) done else sys.error(hasPreds.toString)
|
||||||
|
} else {
|
||||||
|
val found = noPreds.map { _._1 }
|
||||||
|
tsort(hasPreds.mapValues { _ -- found }, done ++ found)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val toPred = edges.foldLeft(Map[A, Set[A]]()) { (acc, e) =>
|
||||||
|
acc + (e._1 -> acc.getOrElse(e._1, Set())) + (e._2 -> (acc.getOrElse(e._2, Set()) + e._1))
|
||||||
|
}
|
||||||
|
tsort(toPred, Seq())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -896,17 +896,13 @@ object JGitUtil {
|
|||||||
git.branchList.call.asScala.map { ref =>
|
git.branchList.call.asScala.map { ref =>
|
||||||
val walk = new RevWalk(repo)
|
val walk = new RevWalk(repo)
|
||||||
try {
|
try {
|
||||||
val defaultCommit = walk.parseCommit(defaultObject)
|
val defaultCommit = walk.parseCommit(defaultObject)
|
||||||
val branchName = ref.getName.stripPrefix("refs/heads/")
|
val branchName = ref.getName.stripPrefix("refs/heads/")
|
||||||
val branchCommit = if(branchName == defaultBranch){
|
val branchCommit = walk.parseCommit(ref.getObjectId)
|
||||||
defaultCommit
|
val when = branchCommit.getCommitterIdent.getWhen
|
||||||
} else {
|
val committer = branchCommit.getCommitterIdent.getName
|
||||||
walk.parseCommit(ref.getObjectId)
|
|
||||||
}
|
|
||||||
val when = branchCommit.getCommitterIdent.getWhen
|
|
||||||
val committer = branchCommit.getCommitterIdent.getName
|
|
||||||
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
|
val committerEmail = branchCommit.getCommitterIdent.getEmailAddress
|
||||||
val mergeInfo = if(origin && branchName == defaultBranch){
|
val mergeInfo = if(origin && branchName == defaultBranch){
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
walk.reset()
|
walk.reset()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package gitbucket.core.util
|
package gitbucket.core.util
|
||||||
|
|
||||||
|
// TODO Move to gitbucket.core.api package?
|
||||||
case class RepositoryName(owner:String, name:String){
|
case class RepositoryName(owner:String, name:String){
|
||||||
val fullName = s"${owner}/${name}"
|
val fullName = s"${owner}/${name}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package gitbucket.core.util
|
|
||||||
|
|
||||||
import java.sql.Connection
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import ControlUtil._
|
|
||||||
|
|
||||||
case class Version(majorVersion: Int, minorVersion: Int) {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(classOf[Version])
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute update/MAJOR_MINOR.sql to update schema to this version.
|
|
||||||
* If corresponding SQL file does not exist, this method do nothing.
|
|
||||||
*/
|
|
||||||
def update(conn: Connection, cl: ClassLoader): Unit = {
|
|
||||||
val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
|
|
||||||
|
|
||||||
using(cl.getResourceAsStream(sqlPath)){ in =>
|
|
||||||
if(in != null){
|
|
||||||
val sql = IOUtils.toString(in, "UTF-8")
|
|
||||||
using(conn.createStatement()){ stmt =>
|
|
||||||
logger.debug(sqlPath + "=" + sql)
|
|
||||||
stmt.executeUpdate(sql)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MAJOR.MINOR
|
|
||||||
*/
|
|
||||||
val versionString = s"${majorVersion}.${minorVersion}"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
object Versions {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(Versions.getClass)
|
|
||||||
|
|
||||||
def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader)
|
|
||||||
(save: Connection => Unit): Unit = {
|
|
||||||
logger.debug("Start schema update")
|
|
||||||
try {
|
|
||||||
if(currentVersion == headVersion){
|
|
||||||
logger.debug("No update")
|
|
||||||
} else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){
|
|
||||||
logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
|
|
||||||
} else {
|
|
||||||
versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl))
|
|
||||||
save(conn)
|
|
||||||
logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
case ex: Throwable => {
|
|
||||||
logger.error("Failed to schema update", ex)
|
|
||||||
ex.printStackTrace()
|
|
||||||
conn.rollback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.debug("End schema update")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -5,56 +5,51 @@
|
|||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Applications"){
|
@html.main("Applications"){
|
||||||
<div class="container body">
|
<div class="container body">
|
||||||
<div class="row">
|
@menu("application", settings.ssh){
|
||||||
<div class="col-md-3">
|
<div class="panel panel-default">
|
||||||
@menu("application", settings.ssh)
|
<div class="panel-heading strong">Personal access tokens</div>
|
||||||
</div>
|
<div class="panel-body">
|
||||||
<div class="col-md-9">
|
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
||||||
<div class="panel panel-default">
|
No tokens.
|
||||||
<div class="panel-heading strong">Personal access tokens</div>
|
} else {
|
||||||
<div class="panel-body">
|
Tokens you have generated that can be used to access the GitBucket API.
|
||||||
@if(personalTokens.isEmpty && gneratedToken.isEmpty){
|
<hr style="margin-top: 10px;">
|
||||||
No tokens.
|
}
|
||||||
} else {
|
@gneratedToken.map{ case (token, tokenString) =>
|
||||||
Tokens you have generated that can be used to access the GitBucket API.
|
<div class="alert alert-info">
|
||||||
<hr style="margin-top: 10px;">
|
Make sure to copy your new personal access token now. You won't be able to see it again!
|
||||||
}
|
</div>
|
||||||
@gneratedToken.map{ case (token, tokenString) =>
|
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
<div class="alert alert-info">
|
<div style="width: 50%;">
|
||||||
Make sure to copy your new personal access token now. You won't be able to see it again!
|
@helper.html.copy("generated-token-copy", tokenString){
|
||||||
</div>
|
<input type="text" value="@tokenString" class="form-control input-sm" readonly>
|
||||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
|
||||||
<div style="width: 50%;">
|
|
||||||
@helper.html.copy("generated-token-copy", tokenString){
|
|
||||||
<input type="text" value="@tokenString" class="form-control input-sm" readonly>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<hr style="margin-top: 10px;">
|
|
||||||
}
|
|
||||||
@personalTokens.zipWithIndex.map { case (token, i) =>
|
|
||||||
@if(i != 0){
|
|
||||||
<hr style="margin-top: 10px;">
|
|
||||||
}
|
}
|
||||||
<strong>@token.note</strong>
|
</div>
|
||||||
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
<hr style="margin-top: 10px;">
|
||||||
|
}
|
||||||
|
@personalTokens.zipWithIndex.map { case (token, i) =>
|
||||||
|
@if(i != 0){
|
||||||
|
<hr>
|
||||||
}
|
}
|
||||||
|
<strong style="line-height: 30px;">@token.note</strong>
|
||||||
|
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">Generate new token</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<fieldset>
|
||||||
|
<label for="note" class="strong">Token description</label>
|
||||||
|
<div><span id="error-note" class="error"></span></div>
|
||||||
|
<input type="text" name="note" id="note" class="form-control"/>
|
||||||
|
<p class="muted">What's this token for?</p>
|
||||||
|
</fieldset>
|
||||||
|
<input type="submit" class="btn btn-success" value="Generate token"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="@path/@account.userName/_personalToken" validate="true">
|
</form>
|
||||||
<div class="panel panel-default">
|
}
|
||||||
<div class="panel-heading strong">Generate new token</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<fieldset>
|
|
||||||
<label for="note" class="strong">Token description</label>
|
|
||||||
<div><span id="error-note" class="error"></span></div>
|
|
||||||
<input type="text" name="note" id="note" class="form-control"/>
|
|
||||||
<p class="muted">What's this token for?</p>
|
|
||||||
</fieldset>
|
|
||||||
<input type="submit" class="btn btn-success" value="Generate token"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
@(account: gitbucket.core.model.Account, info: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
@(account: gitbucket.core.model.Account, info: Option[Any], error: Option[Any])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import gitbucket.core.util.LDAPUtil
|
@import gitbucket.core.util.LDAPUtil
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Edit your profile"){
|
@html.main("Edit your profile"){
|
||||||
<div class="container body">
|
<div class="container body">
|
||||||
<div class="row">
|
@menu("profile", settings.ssh){
|
||||||
<div class="col-md-3">
|
@helper.html.information(info)
|
||||||
@menu("profile", settings.ssh)
|
@helper.html.error(error)
|
||||||
</div>
|
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
|
||||||
<div class="col-md-9">
|
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
||||||
@helper.html.information(info)
|
|
||||||
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>}
|
|
||||||
<form action="@url(account.userName)/_edit" method="POST" validate="true">
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">Profile</div>
|
<div class="panel-heading strong">Profile</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@@ -49,17 +46,17 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<div class="pull-right">
|
|
||||||
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
|
||||||
</div>
|
|
||||||
<input type="submit" class="btn btn-success" value="Save"/>
|
|
||||||
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn btn-default">Cancel</a>}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="btn btn-success" value="Save"/>
|
||||||
|
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn btn-default">Cancel</a>}
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
|
@html.main(if(account.isEmpty) "Create group" else "Edit group"){
|
||||||
<div class="container body">
|
<div class="body main-center">
|
||||||
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
|
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
@@ -42,12 +42,12 @@
|
|||||||
<fieldset class="margin">
|
<fieldset class="margin">
|
||||||
@if(account.isDefined){
|
@if(account.isDefined){
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger btn-lg">Delete Group</a>
|
<a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<input type="submit" class="btn btn-success btn-lg" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
|
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create Group} else {Update Group}"/>
|
||||||
@if(account.isDefined){
|
@if(account.isDefined){
|
||||||
<a href="@url(account.get.userName)" class="btn btn-default btn-lg">Cancel</a>
|
<a href="@url(account.get.userName)" class="btn btn-default">Cancel</a>
|
||||||
}
|
}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
@@ -107,18 +107,18 @@ $(function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addMemberHTML(userName, isManager){
|
function addMemberHTML(userName, isManager){
|
||||||
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
|
var memberButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="false" name="' + userName + '">Member</label>');
|
||||||
if(!isManager){
|
if(!isManager){
|
||||||
memberButton.addClass('active');
|
memberButton.addClass('active');
|
||||||
}
|
}
|
||||||
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
|
var managerButton = $('<label class="btn btn-default btn-mini"><input type="radio" value="true" name="' + userName + '">Manager</label>');
|
||||||
if(isManager){
|
if(isManager){
|
||||||
managerButton.addClass('active');
|
managerButton.addClass('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#member-list').append($('<li>')
|
$('#member-list').append($('<li>')
|
||||||
.data('name', userName)
|
.data('name', userName)
|
||||||
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
|
.append($('<div class="btn-group is_manager" data-toggle="buttons">')
|
||||||
.append(memberButton)
|
.append(memberButton)
|
||||||
.append(managerButton))
|
.append(managerButton))
|
||||||
.append(' ')
|
.append(' ')
|
||||||
@@ -130,9 +130,7 @@ $(function(){
|
|||||||
function updateMembers(){
|
function updateMembers(){
|
||||||
var members = $('#member-list li').map(function(i, e){
|
var members = $('#member-list li').map(function(i, e){
|
||||||
var userName = $(e).data('name');
|
var userName = $(e).data('name');
|
||||||
return userName + ':' + $('button.active').filter(function(i, e){
|
return userName + ':' + $(e).find('label.active input[type=radio]').attr('value');
|
||||||
return $(e).data('name') == userName;
|
|
||||||
}).attr('value');
|
|
||||||
}).get().join(',');
|
}).get().join(',');
|
||||||
$('#members').val(members);
|
$('#members').val(members);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,56 +4,56 @@
|
|||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main(account.userName){
|
@html.main(account.userName){
|
||||||
<div class="container body">
|
<div class="container body">
|
||||||
<div class="container-fluid">
|
<div class="main-sidebar">
|
||||||
<div class="row">
|
<div class="block">
|
||||||
<div class="col-md-4">
|
<div class="account-image">@avatar(account.userName, 240)</div>
|
||||||
<div class="block">
|
<div class="account-fullname">@account.fullName</div>
|
||||||
<div class="account-image">@avatar(account.userName, 270)</div>
|
<div class="account-username">@account.userName</div>
|
||||||
<div class="account-fullname">@account.fullName</div>
|
|
||||||
<div class="account-username">@account.userName</div>
|
|
||||||
</div>
|
|
||||||
<div class="block">
|
|
||||||
@if(account.url.isDefined){
|
|
||||||
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div>
|
|
||||||
}
|
|
||||||
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
|
||||||
</div>
|
|
||||||
@if(groupNames.nonEmpty){
|
|
||||||
<div>
|
|
||||||
<div>Groups</div>
|
|
||||||
@groupNames.map { groupName =>
|
|
||||||
@avatarLink(groupName, 36, tooltip = true)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
|
|
||||||
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
|
||||||
@if(account.isGroupAccount){
|
|
||||||
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
|
||||||
} else {
|
|
||||||
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
|
||||||
}
|
|
||||||
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
|
||||||
<li class="pull-right">
|
|
||||||
<div class="button-group">
|
|
||||||
<a href="@url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
|
|
||||||
<li class="pull-right">
|
|
||||||
<div class="button-group">
|
|
||||||
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
@body
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="block">
|
||||||
|
@if(account.url.isDefined){
|
||||||
|
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div>
|
||||||
|
}
|
||||||
|
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div>
|
||||||
|
</div>
|
||||||
|
@if(groupNames.nonEmpty){
|
||||||
|
<div>
|
||||||
|
<div>Groups</div>
|
||||||
|
@groupNames.map { groupName =>
|
||||||
|
@avatarLink(groupName, 36, tooltip = true)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
<ul class="nav nav-tabs" style="margin-bottom: 5px;">
|
||||||
|
<li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
|
||||||
|
@if(account.isGroupAccount){
|
||||||
|
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li>
|
||||||
|
} else {
|
||||||
|
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li>
|
||||||
|
}
|
||||||
|
@gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab =>
|
||||||
|
@tab(account, context).map { link =>
|
||||||
|
<li@if(active == link.id){ class="active"}><a href="@path/@link.path">@link.label</a></li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){
|
||||||
|
<li class="pull-right">
|
||||||
|
<div class="button-group">
|
||||||
|
<a href="@url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){
|
||||||
|
<li class="pull-right">
|
||||||
|
<div class="button-group">
|
||||||
|
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
@body
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@(active: String, ssh: Boolean)(implicit context: gitbucket.core.controller.Context)
|
@(active: String, ssh: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
<div class="box">
|
<div class="main-sidebar">
|
||||||
<ul class="nav nav-tabs nav-stacked side-menu">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li@if(active=="profile"){ class="active"}>
|
<li@if(active=="profile"){ class="active"}>
|
||||||
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
|
<a href="@path/@loginAccount.get.userName/_edit">Profile</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -13,5 +13,15 @@
|
|||||||
<li@if(active=="application"){ class="active"}>
|
<li@if(active=="application"){ class="active"}>
|
||||||
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
|
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
|
||||||
</li>
|
</li>
|
||||||
|
@gitbucket.core.plugin.PluginRegistry().getAccountSettingMenus.map { menu =>
|
||||||
|
@menu(context).map { link =>
|
||||||
|
<li@if(active==link.id){ class="active"}>
|
||||||
|
<a href="@path/@link.path">@link.label</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
@body
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Create a New Repository"){
|
@html.main("Create a New Repository"){
|
||||||
<div class="body" style="width: 600px; margin: 10px auto;">
|
<div class="body main-center">
|
||||||
<h2>Create a new repository</h2>
|
<h2>Create a new repository</h2>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
A repository contains all the files for your project, including the revision history.
|
A repository contains all the files for your project, including the revision history.
|
||||||
@@ -67,7 +67,7 @@ isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.C
|
|||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="margin form-actions">
|
<fieldset class="margin form-actions">
|
||||||
<input type="submit" class="btn btn-success btn-lg" value="Create repository"/>
|
<input type="submit" class="btn btn-success" value="Create repository"/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,36 +2,36 @@
|
|||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Create your account"){
|
@html.main("Create your account"){
|
||||||
<div class="container body">
|
<div class="container body main-center">
|
||||||
<h3>Create your account</h3>
|
<h3>Create your account</h3>
|
||||||
<form action="@path/register" method="POST" validate="true">
|
<form action="@path/register" method="POST" validate="true">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="userName" class="strong">Username:</label>
|
<label for="userName" class="strong">Username:</label>
|
||||||
<input type="text" name="userName" id="userName" value="" autofocus/>
|
<input type="text" name="userName" id="userName" value="" class="form-control" autofocus/>
|
||||||
<span id="error-userName" class="error"></span>
|
<span id="error-userName" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="password" class="strong">
|
<label for="password" class="strong">
|
||||||
Password:
|
Password:
|
||||||
</label>
|
</label>
|
||||||
<input type="password" name="password" id="password" value=""/>
|
<input type="password" name="password" id="password" class="form-control" value=""/>
|
||||||
<span id="error-password" class="error"></span>
|
<span id="error-password" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="fullName" class="strong">Full Name:</label>
|
<label for="fullName" class="strong">Full Name:</label>
|
||||||
<input type="text" name="fullName" id="fullName" value=""/>
|
<input type="text" name="fullName" id="fullName" class="form-control" value=""/>
|
||||||
<span id="error-fullName" class="error"></span>
|
<span id="error-fullName" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="mailAddress" class="strong">Mail Address:</label>
|
<label for="mailAddress" class="strong">Mail Address:</label>
|
||||||
<input type="text" name="mailAddress" id="mailAddress" value=""/>
|
<input type="text" name="mailAddress" id="mailAddress" class="form-control" value=""/>
|
||||||
<span id="error-mailAddress" class="error"></span>
|
<span id="error-mailAddress" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label for="url" class="strong">URL (optional):</label>
|
<label for="url" class="strong">URL (optional):</label>
|
||||||
<input type="text" name="url" id="url" style="width: 400px;" value=""/>
|
<input type="text" name="url" id="url" class="form-control" value=""/>
|
||||||
<span id="error-url" class="error"></span>
|
<span id="error-url" class="error"></span>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,45 +4,40 @@
|
|||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("SSH Keys"){
|
@html.main("SSH Keys"){
|
||||||
<div class="container body">
|
<div class="container body">
|
||||||
<div class="row">
|
@menu("ssh", settings.ssh){
|
||||||
<div class="col-md-3">
|
<div class="panel panel-default">
|
||||||
@menu("ssh", settings.ssh)
|
<div class="panel-heading strong">SSH Keys</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
@if(sshKeys.isEmpty){
|
||||||
|
No keys
|
||||||
|
}
|
||||||
|
@sshKeys.zipWithIndex.map { case (key, i) =>
|
||||||
|
@if(i != 0){
|
||||||
|
<hr>
|
||||||
|
}
|
||||||
|
<strong style="line-height: 30px;">@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
||||||
|
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-9">
|
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">SSH Keys</div>
|
<div class="panel-heading strong">Add an SSH Key</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
@if(sshKeys.isEmpty){
|
<fieldset class="form-group">
|
||||||
No keys
|
<label for="title" class="strong">Title</label>
|
||||||
}
|
<div><span id="error-title" class="error"></span></div>
|
||||||
@sshKeys.zipWithIndex.map { case (key, i) =>
|
<input type="text" name="title" id="title" class="form-control"/>
|
||||||
@if(i != 0){
|
</fieldset>
|
||||||
<hr style="margin-top: 10px;">
|
<fieldset class="form-group">
|
||||||
}
|
<label for="publicKey" class="strong">Key</label>
|
||||||
<strong>@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
|
<div><span id="error-publicKey" class="error"></span></div>
|
||||||
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
|
<textarea name="publicKey" id="publicKey" class="form-control" style="height: 250px;"></textarea>
|
||||||
}
|
</fieldset>
|
||||||
|
<input type="submit" class="btn btn-success" value="Add"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="@path/@account.userName/_ssh" validate="true">
|
</form>
|
||||||
<div class="panel panel-default">
|
}
|
||||||
<div class="panel-heading strong">Add an SSH Key</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<fieldset class="form-group">
|
|
||||||
<label for="title" class="strong">Title</label>
|
|
||||||
<div><span id="error-title" class="error"></span></div>
|
|
||||||
<input type="text" name="title" id="title" class="form-control"/>
|
|
||||||
</fieldset>
|
|
||||||
<fieldset class="form-group">
|
|
||||||
<label for="publicKey" class="strong">Key</label>
|
|
||||||
<div><span id="error-publicKey" class="error"></span></div>
|
|
||||||
<textarea name="publicKey" id="publicKey" class="form-control" style="height: 250px;"></textarea>
|
|
||||||
</fieldset>
|
|
||||||
<input type="submit" class="btn btn-success" value="Add"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/main/twirl/gitbucket/core/admin/data.scala.html
Normal file
56
src/main/twirl/gitbucket/core/admin/data.scala.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@(tableNames: Seq[String])(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import context._
|
||||||
|
@import gitbucket.core.view.helpers._
|
||||||
|
@html.main("Data export / import"){
|
||||||
|
@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="@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">
|
||||||
|
<div class="radio pull-right" style="margin-right: 10px;">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="type" value="sql">SQL
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio pull-right" style="margin-right: 10px;">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="type" value="xml" checked>XML
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading strong">Import (only XML)</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<form class="form form-horizontal" action="@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 XML file.');
|
||||||
|
return false;
|
||||||
|
} else if(!$('#file').val().endsWith(".xml")){
|
||||||
|
alert('Import is available for only the XML file.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return confirm('All existing data is deleted before importing.\nAre you sure?');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,25 +1,33 @@
|
|||||||
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
<div class="container body">
|
<div class="container body">
|
||||||
<div class="row">
|
<div class="main-sidebar">
|
||||||
<div class="col-md-3">
|
<ul class="nav nav-pills nav-stacked" id="system-admin-menu-container">
|
||||||
<ul class="nav nav-tabs nav-stacked side-menu" id="system-admin-menu-container">
|
<li@if(active=="users"){ class="active"}>
|
||||||
<li@if(active=="users"){ class="active"}>
|
<a href="@path/admin/users">User Management</a>
|
||||||
<a href="@path/admin/users">User Management</a>
|
</li>
|
||||||
</li>
|
<li@if(active=="system"){ class="active"}>
|
||||||
<li@if(active=="system"){ class="active"}>
|
<a href="@path/admin/system">System Settings</a>
|
||||||
<a href="@path/admin/system">System Settings</a>
|
</li>
|
||||||
</li>
|
<li@if(active=="plugins"){ class="active"}>
|
||||||
<li@if(active=="plugins"){ class="active"}>
|
<a href="@path/admin/plugins">Plugins</a>
|
||||||
<a href="@path/admin/plugins">Plugins</a>
|
</li>
|
||||||
</li>
|
<li@if(active=="data"){ class="active"}>
|
||||||
<li>
|
<a href="@path/admin/data">Data export / import</a>
|
||||||
<a href="@path/console/login.jsp">H2 Console</a>
|
</li>
|
||||||
</li>
|
<li>
|
||||||
</ul>
|
<a href="@path/console/login.jsp">H2 Console</a>
|
||||||
</div>
|
</li>
|
||||||
<div class="col-md-9">
|
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
|
||||||
@body
|
@menu(context).map { link =>
|
||||||
</div>
|
<li@if(active==link.id){ class="active"}>
|
||||||
|
<a href="@path/@link.path">@link.label</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="main-content">
|
||||||
|
@body
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -9,11 +9,23 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading strong">System Settings</div>
|
<div class="panel-heading strong">System Settings</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- GITBUCKET_HOME -->
|
<!-- System properties -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<label class="strong">GITBUCKET_HOME</label>
|
<table class="table table-bordered">
|
||||||
@GitBucketHome
|
<tr>
|
||||||
|
<th>Property</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>GITBUCKET_HOME</td>
|
||||||
|
<td>@GitBucketHome</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DATABASE_URL</td>
|
||||||
|
<td>@gitbucket.core.util.DatabaseConfig.url</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
<!-- Base URL -->
|
<!-- Base URL -->
|
||||||
<!--====================================================================-->
|
<!--====================================================================-->
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
@(account: Option[gitbucket.core.model.Account])(implicit context: gitbucket.core.controller.Context)
|
@(account: Option[gitbucket.core.model.Account], error: Option[Any] = None)(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@html.main(if(account.isEmpty) "New User" else "Update User"){
|
@html.main(if(account.isEmpty) "New User" else "Update User"){
|
||||||
@admin.html.menu("users"){
|
@admin.html.menu("users"){
|
||||||
|
@helper.html.error(error)
|
||||||
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
|
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newuser} else {@path/admin/users/@account.get.userName/_edituser}" validate="true">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
@admin.html.menu("users"){
|
@admin.html.menu("users"){
|
||||||
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
|
<form method="POST" action="@if(account.isEmpty){@path/admin/users/_newgroup} else {@path/admin/users/@account.get.userName/_editgroup}" validate="true">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-5">
|
<div class="col-md-6">
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<label for="groupName" class="strong">Group name</label>
|
<label for="groupName" class="strong">Group name</label>
|
||||||
<div>
|
<div>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
@helper.html.uploadavatar(account)
|
@helper.html.uploadavatar(account)
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7">
|
<div class="col-md-6">
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<label class="strong">Members</label>
|
<label class="strong">Members</label>
|
||||||
<ul id="member-list" class="collaborator">
|
<ul id="member-list" class="collaborator">
|
||||||
@@ -78,7 +78,8 @@ $(function(){
|
|||||||
|
|
||||||
// check existence
|
// check existence
|
||||||
$.post('@path/_user/existence', {
|
$.post('@path/_user/existence', {
|
||||||
'userName': userName
|
'userName': userName,
|
||||||
|
'userOnly': true
|
||||||
}, function(data, status){
|
}, function(data, status){
|
||||||
if(data == 'true'){
|
if(data == 'true'){
|
||||||
addMemberHTML(userName, false);
|
addMemberHTML(userName, false);
|
||||||
@@ -102,20 +103,20 @@ $(function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addMemberHTML(userName, isManager){
|
function addMemberHTML(userName, isManager){
|
||||||
var memberButton = $('<button type="button" class="btn btn-default btn-mini" value="false">Member</button>').data('name', userName);
|
var memberButton = $('<input type="radio" value="false" checked>Member</input>').data('name', userName);
|
||||||
if(!isManager){
|
if(!isManager){
|
||||||
memberButton.addClass('active');
|
memberButton.addClass('active');
|
||||||
}
|
}
|
||||||
var managerButton = $('<button type="button" class="btn btn-default btn-mini" value="true">Manager</button>').data('name', userName);
|
var managerButton = $('<input type="radio" value="true">Manager</input>').data('name', userName);
|
||||||
if(isManager){
|
if(isManager){
|
||||||
managerButton.addClass('active');
|
managerButton.addClass('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#member-list').append($('<li>')
|
$('#member-list').append($('<li>')
|
||||||
.data('name', userName)
|
.data('name', userName)
|
||||||
.append($('<div class="btn-group is_manager" data-toggle="buttons-radio">')
|
.append($('<div class="btn-group is_manager" data-toggle="buttons">')
|
||||||
.append(memberButton)
|
.append($('<label class="btn btn-default btn-mini active">').append(memberButton))
|
||||||
.append(managerButton))
|
.append($('<label class="btn btn-default btn-mini">').append(managerButton)))
|
||||||
.append(' ')
|
.append(' ')
|
||||||
.append($('<a>').attr('href', '@path/' + userName).text(userName))
|
.append($('<a>').attr('href', '@path/' + userName).text(userName))
|
||||||
.append(' ')
|
.append(' ')
|
||||||
@@ -125,7 +126,7 @@ $(function(){
|
|||||||
function updateMembers(){
|
function updateMembers(){
|
||||||
var members = $('#member-list li').map(function(i, e){
|
var members = $('#member-list li').map(function(i, e){
|
||||||
var userName = $(e).data('name');
|
var userName = $(e).data('name');
|
||||||
return userName + ':' + $('button.active').filter(function(i, e){
|
return userName + ':' + $('label.active > input[type=radio]').filter(function(i, e){
|
||||||
return $(e).data('name') == userName;
|
return $(e).data('name') == userName;
|
||||||
}).attr('value');
|
}).attr('value');
|
||||||
}).get().join(',');
|
}).get().join(',');
|
||||||
@@ -4,18 +4,8 @@
|
|||||||
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
<span class="small">
|
<div id="table-issues-control">
|
||||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
@helper.html.dropdown("Visibility"){
|
||||||
<i class="octicon octicon-issue-opened @(if(condition.state == "open"){"active"})"></i>
|
|
||||||
@openCount Open
|
|
||||||
</a>
|
|
||||||
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
|
||||||
<i class="octicon octicon-check @(if(condition.state == "closed"){"active"})"></i>
|
|
||||||
@closedCount Closed
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<div class="pull-right" id="table-issues-control">
|
|
||||||
@helper.html.dropdown("Visibility", flat = true){
|
|
||||||
<li>
|
<li>
|
||||||
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
||||||
@helper.html.checkicon(condition.visibility == Some("private"))
|
@helper.html.checkicon(condition.visibility == Some("private"))
|
||||||
@@ -29,7 +19,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Organization", flat = true){
|
@helper.html.dropdown("Organization"){
|
||||||
@groups.map { group =>
|
@groups.map { group =>
|
||||||
<li>
|
<li>
|
||||||
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
||||||
@@ -39,7 +29,7 @@
|
|||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@helper.html.dropdown("Sort", flat = true){
|
@helper.html.dropdown("Sort"){
|
||||||
<li>
|
<li>
|
||||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
closedCount: Int,
|
closedCount: Int,
|
||||||
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
|
||||||
filter: String,
|
filter: String,
|
||||||
groups: List[String])(implicit context: gitbucket.core.controller.Context)
|
groups: List[String],
|
||||||
|
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
|
||||||
|
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
|
||||||
@import context._
|
@import context._
|
||||||
@import gitbucket.core.view.helpers._
|
@import gitbucket.core.view.helpers._
|
||||||
@html.main("Issues"){
|
@html.main("Issues"){
|
||||||
<div class="body">
|
@sidebar(recentRepositories, userRepositories){
|
||||||
@dashboard.html.tab("issues")
|
@dashboard.html.tab("issues")
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@issuesnavi(filter, "issues", condition)
|
@issuesnavi(filter, openCount, closedCount, condition)
|
||||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,6 @@
|
|||||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-top: 12px; padding-bottom: 12px;">
|
<td style="padding-top: 12px; padding-bottom: 12px;">
|
||||||
@if(issue.isPullRequest){
|
|
||||||
<i class="octicon octicon-git-pull-request @(if(issue.closed) "closed" else "open")"></i>
|
|
||||||
} else {
|
|
||||||
<i class="octicon octicon-issue-@(if(issue.closed) "closed" else "opened")"></i>
|
|
||||||
}
|
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a> ・
|
||||||
@if(issue.isPullRequest){
|
@if(issue.isPullRequest){
|
||||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||||
@@ -50,7 +45,7 @@
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<div class="small muted" style="margin-left: 20px; margin-top: 2px;">
|
<div class="small muted" style="margin-top: 2px;">
|
||||||
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||||
@milestone.map { milestone =>
|
@milestone.map { milestone =>
|
||||||
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user