Compare commits

...

151 Commits
3.14 ... 4.2

Author SHA1 Message Date
Naoki Takezoe
7876a60106 Fix doc 2016-07-02 10:40:12 +09:00
Naoki Takezoe
38e71001cb 4.2.0 release 2016-07-02 10:39:01 +09:00
Naoki Takezoe
a1307b7464 Fix NumberFormatException 2016-07-02 02:19:06 +09:00
Naoki Takezoe
fd413d36ad Merge branch 'master' of https://github.com/gitbucket/gitbucket 2016-07-02 02:18:49 +09:00
Naoki Takezoe
509dfc57ca Fix sidebar scrolling 2016-07-02 02:01:51 +09:00
Matthieu Brouillard
95bdd6228e Merge pull request #1224 from McFoggy/issue-1168-json
handle empty password for JSON webhook tests, fixes #1168
2016-06-30 13:30:58 +02:00
Matthieu Brouillard
fd1430371a handle empty password for JSON webhook tests, fixes #1168 2016-06-30 10:47:41 +02:00
Naoki Takezoe
437f944c6e Rename octicons directory 2016-06-26 00:54:41 +09:00
Naoki Takezoe
f64b6e10bb Remove AdminLTEOptions 2016-06-26 00:53:04 +09:00
Naoki Takezoe
5925bd3772 Bump Octicons to 4.2.0 2016-06-26 00:29:01 +09:00
Naoki Takezoe
fe8d4616db Content is scrollable 2016-06-26 00:07:46 +09:00
Naoki Takezoe
99b40974c3 Fix displayed version 2016-06-25 23:41:24 +09:00
Naoki Takezoe
c463590ede Fix styles for AdminLTE 2016-06-25 23:37:01 +09:00
Naoki Takezoe
82163eebc2 Fix styles of account pages 2016-06-25 23:16:00 +09:00
Naoki Takezoe
f1e427f926 Remove unnecessary overflow: hidden; 2016-06-25 22:09:52 +09:00
Naoki Takezoe
c21dcdca80 Apply AdminLTE sidemenu to dashboard 2016-06-25 16:51:38 +09:00
Naoki Takezoe
2ce51472c3 Introduce AdminLTE 2016-06-25 12:52:45 +09:00
Naoki Takezoe
514b1aeec1 Remove link to the commit from the commit message because there are an other link to the commit. 2016-06-21 12:55:41 +09:00
Naoki Takezoe
3f34622fe0 Merge pull request #1221 from seratch/bump-deps
Bump patch version of Scalatra and some libs
2016-06-19 14:05:00 +09:00
Kaz Sera
8c6d5b8178 Bump minor version of libs 2016-06-19 12:03:50 +09:00
Kaz Sera
1971c29fd0 Bump sbt version to 0.13.11 2016-06-19 12:02:32 +09:00
Shintaro Murakami
40ca9b6682 Merge pull request #1220 from mrkm4ntr/link-to-plugin
Add link to gitbucket-network-plugin
2016-06-16 00:38:50 +09:00
Shintaro Murakami
81aeed6f6c Add link to gitbucket-network-plugin 2016-06-16 00:16:45 +09:00
Naoki Takezoe
bf50b1bf82 Add option to allow non-contributors to edit Wiki pages 2016-06-12 09:24:30 +09:00
Naoki Takezoe
1094e8ca2d Change order of columns 2016-06-12 08:23:57 +09:00
Naoki Takezoe
860ccce89b (refs #1208) Update document about schema migration 2016-06-12 08:14:01 +09:00
Naoki Takezoe
59e5993eba Update document 2016-06-12 00:53:20 +09:00
Naoki Takezoe
c08627e5d6 (refs #80) Add options to turn-off Wiki and Issues 2016-06-10 14:22:27 +09:00
Naoki Takezoe
3e0bb46699 Fix invalid margin of the plugin list 2016-06-09 22:10:42 +09:00
Naoki Takezoe
9a972e40ef (refs #1216) Add sending test mail capability 2016-06-09 21:30:28 +09:00
Naoki Takezoe
99ad0db1f6 Fix system settings page 2016-06-09 18:42:17 +09:00
Naoki Takezoe
12d11bc80c (refs #1212) GC button was finished 2016-06-09 00:32:51 +09:00
Naoki Takezoe
dc98079b55 (refs #1212) Add GC button to the danger zone 2016-06-08 20:52:40 +09:00
Naoki Takezoe
88f56126a6 (refs #1215) Open H2 console in new window 2016-06-08 19:48:39 +09:00
Naoki Takezoe
313c9976c8 (refs #1217) Fix permission toggle button in the group editing page 2016-06-08 19:38:09 +09:00
Naoki Takezoe
5b6a1d0adc (refs #1196)Fix search results paging for Wiki 2016-06-04 12:19:54 +09:00
Naoki Takezoe
6db43e6ca7 4.1.0 release 2016-06-04 12:14:15 +09:00
Naoki Takezoe
46177e814c Merge remote-tracking branch 'origin/master' 2016-06-01 22:06:15 +09:00
Naoki Takezoe
2fe79baed8 Close issue by pull request title 2016-06-01 22:06:00 +09:00
Naoki Takezoe
02e17c76a7 Merge pull request #1203 from MasanoriMT/webhook_proxy_support
Add support for proxy setting
2016-06-01 21:38:21 +09:00
Naoki Takezoe
4d8acfd286 Merge pull request #1211 from gitbucket/pull-request-title
Default value of pull request title
2016-06-01 01:26:55 +09:00
Naoki Takezoe
4a5c287b8f Default value of pull request title 2016-06-01 01:08:59 +09:00
Naoki Takezoe
0d4047b4ee Fix presentation of pull request page 2016-05-30 09:19:11 +09:00
Naoki Takezoe
2b730ef180 Fix presentation of branch list 2016-05-30 09:05:01 +09:00
Naoki Takezoe
ecbe7228b9 Fix top margin of titles 2016-05-29 21:17:29 +09:00
Naoki Takezoe
e36d0f65d6 Accept some file types as Wiki attachement file 2016-05-29 21:12:15 +09:00
Naoki Takezoe
30818fb797 Stop PostgreSQL before Travis test 2016-05-28 23:43:26 +09:00
Naoki Takezoe
6d7685fcce Fix Travis test 2016-05-28 23:31:00 +09:00
Naoki Takezoe
2cec5be3d9 Format 2016-05-28 23:28:25 +09:00
Naoki Takezoe
038b70ff0b Format 2016-05-28 22:18:57 +09:00
Naoki Takezoe
23a5f7dcf9 Fix pull request status styles 2016-05-28 04:16:13 +09:00
matoh
baa27d6090 Add support for proxy setting 2016-05-27 10:30:26 +09:00
Naoki Takezoe
f71acfcbe8 Skip external database test in Travis build 2016-05-26 20:01:54 +09:00
Naoki Takezoe
c65e843491 Use Travis provided DB in migration test 2016-05-26 19:01:06 +09:00
Naoki Takezoe
9103c88f0e Add migration test 2016-05-26 15:36:49 +09:00
Naoki Takezoe
90170f0fcc Add TODO comment 2016-05-25 19:15:31 +09:00
Naoki Takezoe
2e3de336cb Add TODO comment 2016-05-25 19:12:46 +09:00
Naoki Takezoe
da50d317e7 Merge pull request #1197 from apkd/master
Fix source display on mobile devices
2016-05-25 13:46:53 +09:00
Naoki Takezoe
7a91e14b03 Merge pull request #1198 from team-lab/fix-1192-branchprotection-setting
fix #1192 Cannot disable option "Require status checks to pass before…
2016-05-25 13:45:29 +09:00
Naoki Takezoe
58eff0a1a3 Merge pull request #1199 from team-lab/fix-jrebel
fix jrebel integration
2016-05-25 13:31:50 +09:00
Naoki Takezoe
8559f3e354 Merge pull request #1200 from team-lab/fix-java8-MaxPermSize-warning
remove -XX:MaxPermSize from java option
2016-05-25 13:31:37 +09:00
nazoking
7606097a4d remove -XX:MaxPermSize from java option 2016-05-24 16:46:19 +09:00
nazoking
dfbace3d26 fix jrebel integration 2016-05-23 17:22:18 +09:00
nazoking
d61ab632f1 fix #1192 Cannot disable option "Require status checks to pass before merging" 2016-05-23 15:31:39 +09:00
apkd
c3b341d945 Fix source display on mobile devices
The raw/mobile/history buttons used to incorrectly overflow on mobile devices, obscuring the entire (!) source view.
2016-05-22 16:21:05 +02:00
Naoki Takezoe
59f063627c Fix style 2016-05-19 14:18:31 +09:00
Naoki Takezoe
b4aba76005 Merge remote-tracking branch 'origin/master' 2016-05-19 12:01:59 +09:00
Naoki Takezoe
81afea350d (refs #1195)Fix date in the commit list 2016-05-19 12:01:41 +09:00
Matthieu Brouillard
5c5da60dd6 Merge pull request #1194 from shiena/patch/fix-missing-ctype
Fix a missing ctype from test hook
2016-05-18 07:25:32 +02:00
Mitsuhiro Koga
89c69cdfc2 Fix a missing ctype from test hook 2016-05-18 02:14:08 +09:00
Naoki Takezoe
0789010248 (refs #533)Add checking for the last one administrator. 2016-05-14 14:01:55 -04:00
Naoki Takezoe
37c23f615f (refs #533)Add remove user checking for the last one administrator. 2016-05-14 08:04:30 -04:00
Naoki Takezoe
b4d3573a84 Fix layout 2016-05-13 17:33:24 -04:00
Naoki Takezoe
5161ece63b (refs #1104)Unique checking for the public key 2016-05-13 15:43:44 -04:00
Naoki Takezoe
5b7955cee6 Merge pull request #1104 from ritschwumm/wip/generic
generic ssh user
2016-05-13 15:30:59 -04:00
Naoki Takezoe
60099e2b0d (refs #1190, #1191)Fix tab floating 2016-05-13 08:32:39 -04:00
Naoki Takezoe
f04c486251 Bump markedj 1.0.9-SNAPSHOT 2016-05-13 00:28:13 -04:00
Herr Ritschwumm
7b23bbf9ba move filtering into the database 2016-05-05 23:15:22 +02:00
Herr Ritschwumm
0a532d9774 optionally all accounts can now use the same generic ssh username. does not work for accounts sharing their key with another account.
based on work Fabian Markert's work.
2016-05-05 23:15:22 +02:00
Herr Ritschwumm
208b98285c small cleanup 2016-05-05 23:15:22 +02:00
Naoki Takezoe
56c9aa32a4 Merge pull request #1185 from dariko/system_admin_typo
typo: DAT(A)BASE_URL
2016-05-05 16:38:09 +09:00
Naoki Takezoe
35080a9f33 (refs #1167)Fix commit information in the branch view 2016-05-05 16:08:38 +09:00
Dario Zanzico
6c41505c91 typo: DAT(A)BASE_URL 2016-05-05 09:03:31 +02:00
Naoki Takezoe
05bfaafe32 Tweak branch list presentation 2016-05-05 16:00:04 +09:00
Naoki Takezoe
7534a88607 (refs #1182)Remove code which is deleting the temporary directory on bootstrap 2016-05-05 12:51:02 +09:00
Naoki Takezoe
d7817d3d88 Update README.md 2016-05-03 01:03:12 +09:00
Naoki Takezoe
37780d467d Update README.md 2016-05-03 01:00:54 +09:00
Naoki Takezoe
ad4af67b30 (refs #1172)Fix: Some of issue filters has been impossible to turn off 2016-05-02 20:17:50 +09:00
Naoki Takezoe
29b0c22b0e Fix radio button layout 2016-05-02 15:48:18 +09:00
Naoki Takezoe
c400678550 (refs #1178)Check group member isn't group account 2016-05-02 15:47:59 +09:00
Naoki Takezoe
3c40e93346 Fix use type selection buttons 2016-05-02 15:36:45 +09:00
Naoki Takezoe
247b664654 Ready for 4.0 release 2016-04-30 15:17:23 +09:00
Naoki Takezoe
b3db0a6a7b Merge pull request #1170 from gitbucket/mysql_support
GitBucket 4.0 (MySQL and PostgreSQL support)
2016-04-30 15:13:23 +09:00
Naoki Takezoe
0a03e41f1c Merge branch 'master' into mysql_support
# Conflicts:
#	build.sbt
2016-04-30 15:12:56 +09:00
Naoki Takezoe
74f3f6bf2e Wrap the PostgreSQL JDBC driver to convert the returning column names to lower case 2016-04-30 03:19:25 +09:00
Naoki Takezoe
a4bf73724d Fix GROUP BY clause for PostgreSQL support 2016-04-30 00:38:52 +09:00
Naoki Takezoe
8d91253ede Use topological sorting to sort tables by dependency 2016-04-29 13:19:38 +09:00
Naoki Takezoe
b7355af49a Fix error checking 2016-04-29 13:18:53 +09:00
Naoki Takezoe
7ec85cbf99 Add error checking for data import 2016-04-29 12:52:10 +09:00
Naoki Takezoe
a6790b049d Specify charset of XML file 2016-04-29 12:27:38 +09:00
Naoki Takezoe
dce747b1e8 Export supports both of XML and SQL 2016-04-25 01:08:19 +09:00
Naoki Takezoe
c22ee8acfd Export / Import as XML 2016-04-25 00:35:25 +09:00
Naoki Takezoe
30b8b738b6 Merge branch 'master' into mysql_support 2016-04-20 01:13:25 +09:00
Naoki Takezoe
1accafa8b1 Fix solidbase version with 1.0.0 2016-04-17 12:28:03 +09:00
Naoki Takezoe
11700f4cb4 Remove PLUGIN table from export target 2016-04-17 12:27:41 +09:00
Naoki Takezoe
c9de8dd323 Fix solidbase plugin support 2016-04-16 13:16:00 +09:00
Naoki Takezoe
fd694e38aa Update version and release document 2016-04-16 11:26:53 +09:00
Naoki Takezoe
8822c36b5f Remove unnecessary files 2016-04-16 11:21:25 +09:00
Naoki Takezoe
e614e31162 Update pre-condition checking 2016-04-16 11:07:55 +09:00
Naoki Takezoe
ad47ad4269 Merge branch 'master' into mysql_support
# Conflicts:
#	src/main/resources/update/3_13.sql
#	src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
2016-04-16 11:06:45 +09:00
Naoki Takezoe
0c50545cbd Fix SQL for PostgreSQL 2016-04-15 20:27:41 +09:00
Naoki Takezoe
53cbc36a01 Display database url at the system setting console 2016-04-15 13:40:26 +09:00
Naoki Takezoe
85b2053004 Fixup 2016-04-15 13:26:27 +09:00
Naoki Takezoe
eba240de65 Fix table order in the exported sql file 2016-04-15 13:18:58 +09:00
Naoki Takezoe
1e5114cd54 Fix for PostgreSQL support 2016-04-15 09:48:09 +09:00
Naoki Takezoe
90cb5de5f0 Update PostgreSQL configuration example 2016-04-15 09:46:42 +09:00
Naoki Takezoe
11d33e9389 Remove unnecessary file 2016-04-15 01:50:49 +09:00
Naoki Takezoe
c71e9331ae Use COALESCE instead of IFNULL 2016-04-15 01:50:29 +09:00
Naoki Takezoe
f37b5fa682 Add PostgreSQL support 2016-04-14 11:39:42 +09:00
Naoki Takezoe
8cb1ac734d Update MySQL configuration example 2016-04-14 11:11:47 +09:00
Naoki Takezoe
05ff2a854c Data import is available 2016-04-14 09:58:00 +09:00
Naoki Takezoe
d956ade5e3 Working for data import tool 2016-04-13 11:43:38 +09:00
Naoki Takezoe
73228506a5 Add data import form 2016-04-13 11:34:41 +09:00
Naoki Takezoe
2525bbafa8 Database export tool is available 2016-04-13 11:15:42 +09:00
Naoki Takezoe
338946dd3a Start to implement the database export tool 2016-04-13 10:36:03 +09:00
Naoki Takezoe
2d225641ee Start to implement the database export tool 2016-04-12 22:03:31 +09:00
Naoki Takezoe
3c727fe678 Backup H2 data files before migration to 4.0 if files exist 2016-04-12 15:05:59 +09:00
Naoki Takezoe
523ea0d437 Fix comment 2016-04-12 14:46:20 +09:00
Naoki Takezoe
9eff8f248b Experimental support for MySQL is available! 2016-04-12 11:16:01 +09:00
Naoki Takezoe
d50c858a26 Use ${currentDateTime} 2016-04-12 11:12:32 +09:00
Naoki Takezoe
6f4e94ba9a Update migration scripts for MySQL compatibility 2016-04-12 10:06:38 +09:00
Naoki Takezoe
f2750c20a2 Merge branch 'solidbase-integration' into switch_slick_driver 2016-04-12 01:10:23 +09:00
Naoki Takezoe
2da2b426a1 Merge branch 'master' into solidbase-integration
# Conflicts:
#	build.sbt
#	src/main/twirl/gitbucket/core/main.scala.html
2016-04-12 01:08:59 +09:00
Naoki Takezoe
5a0bc127b7 Add MySQL jdbc driver 2016-04-12 01:06:12 +09:00
Naoki Takezoe
23a482bbba Use HikariCP instead of c3p0 2016-04-11 22:16:41 +09:00
Naoki Takezoe
6c2fce1b16 Switch Slick driver by system property 2016-04-11 22:01:40 +09:00
Naoki Takezoe
c8686f4b34 Pick adding WEB_HOOK.TOKEN into solidbase integration
WEB_HOOK.TOKEN is added in #1127
2016-03-06 13:10:28 +09:00
Naoki Takezoe
43097b4c1c Restore amateras-snapshot repository setting 2016-03-06 13:07:43 +09:00
Naoki Takezoe
539751a1d9 Merge branch 'master' into solidbase-integration
# Conflicts:
#	build.sbt
#	src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
#	src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
2016-03-06 13:07:03 +09:00
Naoki Takezoe
40e36e3f8b Merge branch 'master'
Conflicts:
	project/build.scala
2016-01-27 13:35:30 +09:00
Naoki Takezoe
3d1c9bc9de Merge branch 'master'
Conflicts:
	src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
2016-01-24 18:40:25 +09:00
Naoki Takezoe
5a5bf34fe0 Merged branch master into solidbase-integration 2016-01-21 12:05:55 +09:00
Naoki Takezoe
af7043f4bf Update for GitBucket 3.11 2016-01-21 11:35:00 +09:00
Naoki Takezoe
249b27593e Apply DDL for branch protection to solidbase migration 2016-01-20 03:12:30 +09:00
Naoki Takezoe
1201271949 Merge branch 'master' into solidbase-integration
Conflicts:
	src/main/scala/gitbucket/core/model/Profile.scala
	src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
2016-01-19 15:31:26 +09:00
Naoki Takezoe
19e74a4fe1 Remove unused model and service for PLUGIN table 2016-01-17 13:29:27 +09:00
Naoki Takezoe
130aa1e515 Merge branch 'master' into solidbase-integration 2016-01-17 12:57:52 +09:00
Naoki Takezoe
e92d1eae5a Add migration process from 3.10 2016-01-16 13:54:06 +09:00
Naoki Takezoe
8b44a00299 Fix foreign key cascading 2016-01-13 11:57:36 +09:00
Naoki Takezoe
f5c1c0703d Fix compilation error in testcase 2016-01-13 11:47:59 +09:00
Naoki Takezoe
5036b1a1aa Replace migration system with Solidbase 2016-01-10 01:43:13 +09:00
195 changed files with 13728 additions and 2760 deletions

View File

@@ -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

View File

@@ -49,6 +49,7 @@ GitBucket has the plug-in system to extend GitBucket from outside of GitBucket.
- [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin) - [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin)
- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin) - [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin)
- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin) - [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin)
- [gitbucket-network-plugin](https://github.com/mrkm4ntr/gitbucket-network-plugin)
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/). You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
@@ -62,13 +63,28 @@ Support
- First priority of GitBucket is easy installation and API compatibility with GitHub, 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.2 - 2 Jul 2016
- New UI based on [AdminLTE](https://github.com/almasaeed2010/AdminLTE)
- git gc
- Issues and Wiki have been possible to be disabled
- SMTP configuration test mail
### 4.1 - 4 Jun 2016
- Generic ssh user
- Improve branch protection UI
- Default value of pull request title
### 4.0 - 30 Apr 2016 ### 4.0 - 30 Apr 2016
- MySQL and PostgreSQL support - MySQL and PostgreSQL support
- Data export and import - Data export and import
- Migration system has been switched to [solidbase](https://github.com/gitbucket/solidbase) - 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 ### 3.14 - 30 Apr 2016
- File attachment and search for wiki pages - File attachment and search for wiki pages

View File

@@ -1,8 +1,8 @@
val Organization = "gitbucket" val Organization = "gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "3.14.0" val GitBucketVersion = "4.2.0"
val ScalatraVersion = "2.4.0" val ScalatraVersion = "2.4.1"
val JettyVersion = "9.3.6.v20151106" val JettyVersion = "9.3.9.v20160517"
lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin) lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin)
@@ -15,37 +15,44 @@ scalaVersion := "2.11.8"
// dependency settings // dependency settings
resolvers ++= Seq( resolvers ++= Seq(
Classpaths.typesafeReleases, Classpaths.typesafeReleases,
"sonatype-snapshot" at "https://oss.sonatype.org/content/repositories/snapshots/" "amateras" at "http://amateras.sourceforge.jp/mvn/",
"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.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.2.201602141800-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r", "org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.2.201602141800-r",
"org.scalatra" %% "scalatra" % ScalatraVersion, "org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion, "org.scalatra" %% "scalatra-json" % ScalatraVersion,
"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.8", "io.github.gitbucket" % "solidbase" % "1.0.0",
"org.apache.commons" % "commons-compress" % "1.10", "io.github.gitbucket" % "markedj" % "1.0.9-SNAPSHOT",
"org.apache.commons" % "commons-compress" % "1.11",
"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",
"org.apache.sshd" % "apache-sshd" % "1.0.0", "org.apache.sshd" % "apache-sshd" % "1.0.0",
"org.apache.tika" % "tika-core" % "1.11", "org.apache.tika" % "tika-core" % "1.13",
"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.192",
"ch.qos.logback" % "logback-classic" % "1.1.1", "mysql" % "mysql-connector-java" % "5.1.39",
"com.mchange" % "c3p0" % "0.9.5.2", "org.postgresql" % "postgresql" % "9.4.1208",
"ch.qos.logback" % "logback-classic" % "1.1.7",
"com.zaxxer" % "HikariCP" % "2.4.6",
"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.15",
"fr.brouillard.oss.security.xhub" % "xhub4j-core" % "1.0.0", "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.4" % "test",
"com.wix" % "wix-embedded-mysql" % "1.0.3" % "test",
"ru.yandex.qatools.embed" % "postgresql-embedded" % "1.14" % "test"
) )
// Twirl settings // Twirl settings
@@ -55,10 +62,14 @@ play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
scalacOptions := Seq("-deprecation", "-language:postfixOps", "-Ybackend:GenBCode", "-Ydelambdafy:method", "-target:jvm-1.8") 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
@@ -73,12 +84,13 @@ 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

View File

@@ -1,37 +1,54 @@
Automatic Schema Updating Automatic Schema Updating
======== ========
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading. GitBucket updates database schema automatically using [Solidbase](https://github.com/gitbucket/solidbase) in the first run after the upgrading.
To release a new version of GitBucket, add the version definition to the [gitbucket.core.servlet.AutoUpdate](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.scala) at first. To release a new version of GitBucket, add the version definition to the [gitbucket.core.GitBucketCoreModule](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/GitBucketCoreModule.scala) at first.
```scala ```scala
object AutoUpdate { object GitBucketCoreModule extends Module("gitbucket-core",
... new Version("4.0.0",
/** new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
* The history of versions. A head of this sequence is the current GitBucket version. new SqlMigration("update/gitbucket-core_4.0.sql")
*/ ),
val versions = Seq( new Version("4.1.0"),
Version(1, 0) new Version("4.2.0",
new LiquibaseMigration("update/gitbucket-core_4.2.xml")
) )
...
```
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/gitbucket/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
We can also add any Scala code for upgrade GitBucket which modifies resources other than database. Override ```Version.update``` like below:
```scala
val versions = Seq(
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
// Add any code here!
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0)
) )
``` ```
Next, add a XML file which updates database schema into [/src/main/resources/update/](https://github.com/gitbucket/gitbucket/tree/master/src/main/resources/update) with a filenane defined in `GitBucketCoreModule`.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="REPOSITORY">
<column name="ENABLE_WIKI" type="boolean" nullable="false" defaultValueBoolean="true"/>
<column name="ENABLE_ISSUES" type="boolean" nullable="false" defaultValueBoolean="true"/>
<column name="EXTERNAL_WIKI_URL" type="varchar(200)" nullable="true"/>
<column name="EXTERNAL_ISSUES_URL" type="varchar(200)" nullable="true"/>
</addColumn>
</changeSet>
```
Solidbase stores the current version to `VERSIONS` table and checks it at start-up. If the stored version differs from the actual version, it executes differences between the stored version and the actual version.
We can add the SQL file instead of the XML file using `SqlMigration`. It try to load a SQL file from classpath as following order:
1. Specified path (if specified)
2. `${moduleId}_${version}_${database}.sql`
3. `${moduleId}_${version}.sql`
Also we can add any code by extending `Migration`:
```scala
object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.0.0", new Migration(){
override def migrate(moduleId: String, version: String, context: java.util.Map[String, String]): Unit = {
...
}
})
)
```
See more details [README of Solidbase](https://github.com/gitbucket/solidbase).

View File

@@ -11,22 +11,24 @@ Note to update version number in files below:
```scala ```scala
val Organization = "gitbucket" val Organization = "gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "3.12.0" // <---- update version!! val GitBucketVersion = "4.0.0" // <---- update version!!
val ScalatraVersion = "2.4.0" val ScalatraVersion = "2.4.0"
val JettyVersion = "9.3.6.v20151106" val JettyVersion = "9.3.6.v20151106"
``` ```
### 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",
new Version("4.0.0",
/** new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
* The history of versions. A head of this sequence is the current GitBucket version. new SqlMigration("update/gitbucket-core_4.0.sql")
*/ ),
val versions = Seq( // add new version definition
new Version(3, 12), // <---- add this line!! new Version("4.1.0",
new Version(3, 11), new LiquibaseMigration("update/gitbucket-core_4.1.xml")
)
)
``` ```
Generate release files Generate release files
@@ -39,7 +41,7 @@ Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https
Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`. Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`.
```bash ```bash
$sbt executable $ sbt executable
``` ```
### Deploy assembly jar file ### Deploy assembly jar file

View File

@@ -1 +1 @@
sbt.version=0.13.9 sbt.version=0.13.11

View File

@@ -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
View File

@@ -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 "$@"

View File

@@ -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();

View 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);
}
}
}

View File

@@ -1,6 +0,0 @@
db {
driver = "org.h2.Driver"
url = "jdbc:h2:${DatabaseHome};MVCC=true"
user = "sa"
password = "sa"
}

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
c3p0 {
privilegeSpawnedThreads=true
contextClassLoaderSource=library
}

View File

@@ -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
);

View File

@@ -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);

View File

@@ -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);

View File

@@ -1 +0,0 @@
DROP TABLE COMMIT_LOG;

View File

@@ -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);

View File

@@ -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';

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -1 +0,0 @@
ALTER TABLE ACCOUNT ADD COLUMN REMOVED BOOLEAN DEFAULT FALSE;

View File

@@ -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);

View File

@@ -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);

View File

@@ -1 +0,0 @@
ALTER TABLE COMMIT_COMMENT ALTER COLUMN FILE_NAME NVARCHAR(260);

View File

@@ -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;

View File

@@ -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;

View File

@@ -1 +0,0 @@
ALTER TABLE WEB_HOOK ADD COLUMN TOKEN VARCHAR(100);

View File

@@ -1,3 +0,0 @@
ALTER TABLE WEB_HOOK ADD COLUMN CTYPE VARCHAR(10);
UPDATE WEB_HOOK SET CTYPE = 'form';

View File

@@ -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;

View 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);

View 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>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<changeSet>
<addColumn tableName="REPOSITORY">
<column name="ENABLE_ISSUES" type="boolean" nullable="false" defaultValueBoolean="true"/>
<column name="EXTERNAL_ISSUES_URL" type="varchar(200)" nullable="true"/>
<column name="ENABLE_WIKI" type="boolean" nullable="false" defaultValueBoolean="true"/>
<column name="ALLOW_WIKI_EDITING" type="boolean" nullable="false" defaultValueBoolean="false"/>
<column name="EXTERNAL_WIKI_URL" type="varchar(200)" nullable="true"/>
</addColumn>
</changeSet>

View File

@@ -0,0 +1,15 @@
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.0.0",
new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
new SqlMigration("update/gitbucket-core_4.0.sql")
),
new Version("4.1.0"),
new Version("4.2.0",
new LiquibaseMigration("update/gitbucket-core_4.2.xml")
)
)

View File

@@ -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{
} }
)) ))
} }

View File

@@ -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,

View File

@@ -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 =

View File

@@ -155,7 +155,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
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
}) })
@@ -178,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)
@@ -187,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 {
@@ -447,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.")
} }
} }

View File

@@ -119,7 +119,9 @@ trait ApiControllerBase extends ControllerBase {
}) getOrElse NotFound }) getOrElse NotFound
}) })
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */ /**
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository => patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._ import gitbucket.core.api._
(for{ (for{
@@ -261,18 +263,28 @@ trait ApiControllerBase extends ControllerBase {
* https://developer.github.com/v3/pulls/#list-pull-requests * https://developer.github.com/v3/pulls/#list-pull-requests
*/ */
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository => get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
// TODO: more api spec condition // TODO: more api spec condition
val condition = IssueSearchCondition(request) val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get 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) => 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( ApiPullRequest(
issue, issue,
pullRequest, pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)), ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)), ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)) }) ApiUser(issueUser)
)
})
}) })
/** /**

View File

@@ -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,7 +103,7 @@ 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))
}, },
@@ -128,7 +128,7 @@ 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))
}, },

View File

@@ -6,9 +6,11 @@ import gitbucket.core.servlet.Database
import gitbucket.core.util._ 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.api.Git
import org.eclipse.jgit.dircache.DirCache import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib.{FileMode, Constants} 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.{IOUtils, FileUtils} import org.apache.commons.io.{IOUtils, FileUtils}
@@ -71,11 +73,26 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
fileName fileName
} }
} }
}, FileUtil.isImage) }, FileUtil.isUploadableType)
} }
} getOrElse BadRequest } 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 = { private def collaboratorsOnly(owner: String, repository: String, loginAccount: Account)(action: => Any): Any = {
implicit val session = Database.getSession(request) implicit val session = Database.getSession(request)
loginAccount match { loginAccount match {

View File

@@ -117,7 +117,9 @@ 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
}) })
// TODO Move to RepositoryViwerController? // TODO Move to RepositoryViwerController?

View File

@@ -202,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)
} }
} }
@@ -255,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)
} }
@@ -354,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 {

View File

@@ -27,12 +27,26 @@ trait RepositorySettingsControllerBase extends ControllerBase {
with OwnerAuthenticator with UsersAuthenticator => with OwnerAuthenticator with UsersAuthenticator =>
// for repository options // for repository options
case class OptionsForm(repositoryName: String, description: Option[String], isPrivate: Boolean) case class OptionsForm(
repositoryName: String,
description: Option[String],
isPrivate: Boolean,
enableIssues: Boolean,
externalIssuesUrl: Option[String],
enableWiki: Boolean,
allowWikiEditing: Boolean,
externalWikiUrl: Option[String]
)
val optionsForm = mapping( val optionsForm = mapping(
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))), "repositoryName" -> trim(label("Repository Name" , text(required, maxlength(40), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))), "description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())) "isPrivate" -> trim(label("Repository Type" , boolean())),
"enableIssues" -> trim(label("Enable Issues" , boolean())),
"externalIssuesUrl" -> trim(label("External Issues URL", optional(text(maxlength(200))))),
"enableWiki" -> trim(label("Enable Wiki" , boolean())),
"allowWikiEditing" -> trim(label("Allow Wiki Editing" , boolean())),
"externalWikiUrl" -> trim(label("External Wiki URL" , optional(text(maxlength(200)))))
)(OptionsForm.apply) )(OptionsForm.apply)
// for default branch // for default branch
@@ -92,7 +106,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
form.description, form.description,
repository.repository.parentUserName.map { _ => repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate repository.repository.isPrivate
} getOrElse form.isPrivate } getOrElse form.isPrivate,
form.enableIssues,
form.externalIssuesUrl,
form.enableWiki,
form.allowWikiEditing,
form.externalWikiUrl
) )
// Change repository name // Change repository name
if(repository.name != form.repositoryName){ if(repository.name != form.repositoryName){
@@ -294,7 +313,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the danger zone. * Display the danger zone.
*/ */
get("/:owner/:repository/settings/danger")(ownerOnly { get("/:owner/:repository/settings/danger")(ownerOnly {
html.danger(_) html.danger(_, flash.get("info"))
}) })
/** /**
@@ -333,6 +352,19 @@ trait RepositorySettingsControllerBase extends ControllerBase {
redirect(s"/${repository.owner}") redirect(s"/${repository.owner}")
}) })
/**
* Run GC
*/
post("/:owner/:repository/settings/gc")(ownerOnly { repository =>
LockUtil.lock(s"${repository.owner}/${repository.name}") {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.gc();
}
}
flash += "info" -> "Garbage collection has been executed."
redirect(s"/${repository.owner}/${repository.name}/settings/danger")
})
/** /**
* Provides duplication check for web hook url. * Provides duplication check for web hook url.
*/ */

View File

@@ -691,8 +691,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean = private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = { override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
e.printStackTrace() e.printStackTrace()
} }

View File

@@ -1,8 +1,10 @@
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, RepositoryService} import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.AdminAuthenticator import gitbucket.core.util.{AdminAuthenticator, Mailer}
import gitbucket.core.ssh.SshServer import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import SystemSettingsService._ import SystemSettingsService._
@@ -11,7 +13,8 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.StringUtil._ import gitbucket.core.util.StringUtil._
import io.github.gitbucket.scalatra.forms._ import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils import org.apache.commons.io.{FileUtils, IOUtils}
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
@@ -68,12 +71,22 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
).flatten ).flatten
} }
private val pluginForm = mapping( private val sendMailForm = mapping(
"pluginId" -> list(trim(label("", text()))) "smtp" -> mapping(
)(PluginForm.apply) "host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply),
"testAddress" -> trim(label("", text(required)))
)(SendMailForm.apply)
case class PluginForm(pluginIds: List[String]) case class SendMailForm(smtp: Smtp, testAddress: String)
case class DataExportForm(tableNames: List[String])
case class NewUserForm(userName: String, password: String, fullName: String, case class NewUserForm(userName: String, password: String, fullName: String,
mailAddress: String, isAdmin: Boolean, mailAddress: String, isAdmin: Boolean,
@@ -149,6 +162,18 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
redirect("/admin/system") redirect("/admin/system")
}) })
post("/admin/system/sendmail", sendMailForm)(adminOnly { form =>
try {
new Mailer(form.smtp).send(form.testAddress,
"Test message from GitBucket", "This is a test message from GitBucket.")
"Test mail has been sent to: " + form.testAddress
} catch {
case e: Exception => "[Error] " + e.toString
}
})
get("/admin/plugins")(adminOnly { get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins()) html.plugins(PluginRegistry().getPlugins())
}) })
@@ -176,36 +201,39 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
get("/admin/users/:userName/_edituser")(adminOnly { get("/admin/users/:userName/_edituser")(adminOnly {
val userName = params("userName") val userName = params("userName")
html.user(getAccountByUserName(userName, true)) html.user(getAccountByUserName(userName, true), flash.get("error"))
}) })
post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form => post("/admin/users/:name/_edituser", editUserForm)(adminOnly { form =>
val userName = params("userName") val userName = params("userName")
getAccountByUserName(userName, true).map { account => 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)
}
if(form.isRemoved){ updateAccount(account.copy(
// Remove repositories password = form.password.map(sha1).getOrElse(account.password),
// getRepositoryNamesOfUser(userName).foreach { repositoryName => fullName = form.fullName,
// deleteRepository(userName, repositoryName) mailAddress = form.mailAddress,
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName)) isAdmin = form.isAdmin,
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName)) url = form.url,
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName)) isRemoved = form.isRemoved))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY updateImage(userName, form.fileId, form.clearImage)
removeUserRelatedData(userName) redirect("/admin/users")
} }
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 } getOrElse NotFound
}) })
@@ -268,6 +296,32 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
} }
}) })
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(){ private def members: 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(value.split(",").exists { if(value.split(",").exists {

View File

@@ -1,7 +1,8 @@
package gitbucket.core.controller package gitbucket.core.controller
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.wiki.html import gitbucket.core.wiki.html
import gitbucket.core.service.{RepositoryService, WikiService, ActivityService, AccountService} import gitbucket.core.service.{AccountService, ActivityService, RepositoryService, WikiService}
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.StringUtil._ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
@@ -39,7 +40,7 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository => get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page => getWikiPage(repository.owner, repository.name, "Home").map { page =>
html.page("Home", page, getWikiPageList(repository.owner, repository.name), html.page("Home", page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository, isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar"), getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer")) getWikiPage(repository.owner, repository.name, "_Footer"))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
@@ -50,7 +51,7 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page => getWikiPage(repository.owner, repository.name, pageName).map { page =>
html.page(pageName, page, getWikiPageList(repository.owner, repository.name), html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository, isEditable(repository),
getWikiPage(repository.owner, repository.name, "_Sidebar"), getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer")) getWikiPage(repository.owner, repository.name, "_Footer"))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit") } getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
@@ -62,7 +63,7 @@ trait WikiControllerBase extends ControllerBase {
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match { JGitUtil.getCommitLog(git, "master", path = pageName + ".md") match {
case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository) case Right((logs, hasNext)) => html.history(Some(pageName), logs, repository)
case Left(_) => NotFound case Left(_) => NotFound()
} }
} }
}) })
@@ -73,7 +74,7 @@ trait WikiControllerBase extends ControllerBase {
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository, html.compare(Some(pageName), from, to, JGitUtil.getDiffs(git, from, to, true).filter(_.newPath == pageName + ".md"), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) isEditable(repository), flash.get("info"))
} }
}) })
@@ -82,102 +83,115 @@ trait WikiControllerBase extends ControllerBase {
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository, html.compare(None, from, to, JGitUtil.getDiffs(git, from, to, true), repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount), flash.get("info")) isEditable(repository), flash.get("info"))
} }
}) })
get("/:owner/:repository/wiki/:page/_revert/:commitId")(collaboratorsOnly { repository => get("/:owner/:repository/wiki/:page/_revert/:commitId")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) if(isEditable(repository)){
val Array(from, to) = params("commitId").split("\\.\\.\\.") val pageName = StringUtil.urlDecode(params("page"))
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){ if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, Some(pageName))){
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}") redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
}
})
get("/:owner/:repository/wiki/_revert/:commitId")(collaboratorsOnly { repository =>
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki/")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
}
})
get("/:owner/:repository/wiki/:page/_edit")(collaboratorsOnly { repository =>
val pageName = StringUtil.urlDecode(params("page"))
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
})
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
loginAccount,
form.message.getOrElse(""),
Some(form.id)
).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else { } else {
redirect(s"/${repository.owner}/${repository.name}/wiki") flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_compare/${from}...${to}")
} }
} } else Unauthorized()
})
get("/:owner/:repository/wiki/_revert/:commitId")(referrersOnly { repository =>
if(isEditable(repository)){
val Array(from, to) = params("commitId").split("\\.\\.\\.")
if(revertWikiPage(repository.owner, repository.name, from, to, context.loginAccount.get, None)){
redirect(s"/${repository.owner}/${repository.name}/wiki/")
} else {
flash += "info" -> "This patch was not able to be reversed."
redirect(s"/${repository.owner}/${repository.name}/wiki/_compare/${from}...${to}")
}
} else Unauthorized()
})
get("/:owner/:repository/wiki/:page/_edit")(referrersOnly { repository =>
if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page"))
html.edit(pageName, getWikiPage(repository.owner, repository.name, pageName), repository)
} else Unauthorized()
}) })
get("/:owner/:repository/wiki/_new")(collaboratorsOnly { post("/:owner/:repository/wiki/_edit", editForm)(referrersOnly { (form, repository) =>
html.edit("", None, _) if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
loginAccount,
form.message.getOrElse(""),
Some(form.id)
).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
}
} else Unauthorized()
}) })
post("/:owner/:repository/wiki/_new", newForm)(collaboratorsOnly { (form, repository) => get("/:owner/:repository/wiki/_new")(referrersOnly { repository =>
defining(context.loginAccount.get){ loginAccount => if(isEditable(repository)){
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName, html.edit("", None, repository)
} else Unauthorized()
})
post("/:owner/:repository/wiki/_new", newForm)(referrersOnly { (form, repository) =>
if(isEditable(repository)){
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), None) form.content, loginAccount, form.message.getOrElse(""), None)
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName) recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
if(notReservedPageName(form.pageName)) { if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}") redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else { } else {
redirect(s"/${repository.owner}/${repository.name}/wiki") redirect(s"/${repository.owner}/${repository.name}/wiki")
}
} }
} } else Unauthorized()
}) })
get("/:owner/:repository/wiki/:page/_delete")(collaboratorsOnly { repository => get("/:owner/:repository/wiki/:page/_delete")(referrersOnly { repository =>
val pageName = StringUtil.urlDecode(params("page")) if(isEditable(repository)){
val pageName = StringUtil.urlDecode(params("page"))
defining(context.loginAccount.get){ loginAccount => defining(context.loginAccount.get){ loginAccount =>
deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}") deleteWikiPage(repository.owner, repository.name, pageName, loginAccount.fullName, loginAccount.mailAddress, s"Destroyed ${pageName}")
updateLastActivityDate(repository.owner, repository.name) updateLastActivityDate(repository.owner, repository.name)
redirect(s"/${repository.owner}/${repository.name}/wiki") redirect(s"/${repository.owner}/${repository.name}/wiki")
} }
} else Unauthorized()
}) })
get("/:owner/:repository/wiki/_pages")(referrersOnly { repository => get("/:owner/:repository/wiki/_pages")(referrersOnly { repository =>
html.pages(getWikiPageList(repository.owner, repository.name), repository, html.pages(getWikiPageList(repository.owner, repository.name), repository, isEditable(repository))
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
get("/:owner/:repository/wiki/_history")(referrersOnly { repository => get("/:owner/:repository/wiki/_history")(referrersOnly { repository =>
using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getWikiRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, "master") match { JGitUtil.getCommitLog(git, "master") match {
case Right((logs, hasNext)) => html.history(None, logs, repository) case Right((logs, hasNext)) => html.history(None, logs, repository)
case Left(_) => NotFound case Left(_) => NotFound()
} }
} }
}) })
@@ -226,4 +240,9 @@ trait WikiControllerBase extends ControllerBase {
private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName")) private def targetWikiPage = getWikiPage(params("owner"), params("repository"), params("pageName"))
private def isEditable(repository: RepositoryInfo)(implicit context: Context): Boolean =
repository.repository.allowWikiEditing || (
hasWritePermission(repository.owner, repository.name, context.loginAccount)
)
} }

View File

@@ -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
)

View File

@@ -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

View File

@@ -17,7 +17,14 @@ trait RepositoryComponent extends TemplateComponent { self: Profile =>
val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME") val originRepositoryName = column[String]("ORIGIN_REPOSITORY_NAME")
val parentUserName = column[String]("PARENT_USER_NAME") val parentUserName = column[String]("PARENT_USER_NAME")
val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME") val parentRepositoryName = column[String]("PARENT_REPOSITORY_NAME")
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch, registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?) <> (Repository.tupled, Repository.unapply) val enableIssues = column[Boolean]("ENABLE_ISSUES")
val externalIssuesUrl = column[String]("EXTERNAL_ISSUES_URL")
val enableWiki = column[Boolean]("ENABLE_WIKI")
val allowWikiEditing = column[Boolean]("ALLOW_WIKI_EDITING")
val externalWikiUrl = column[String]("EXTERNAL_WIKI_URL")
def * = (userName, repositoryName, isPrivate, description.?, defaultBranch,
registeredDate, updatedDate, lastActivityDate, originUserName.?, originRepositoryName.?, parentUserName.?, parentRepositoryName.?,
enableIssues, externalIssuesUrl.?, enableWiki, allowWikiEditing, externalWikiUrl.?) <> (Repository.tupled, Repository.unapply)
def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository)
} }
@@ -35,5 +42,10 @@ case class Repository(
originUserName: Option[String], originUserName: Option[String],
originRepositoryName: Option[String], originRepositoryName: Option[String],
parentUserName: Option[String], parentUserName: Option[String],
parentRepositoryName: Option[String] parentRepositoryName: Option[String],
enableIssues: Boolean,
externalIssuesUrl: Option[String],
enableWiki: Boolean,
allowWikiEditing: Boolean,
externalWikiUrl: Option[String]
) )

View File

@@ -6,7 +6,7 @@ import gitbucket.core.model.Account
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.Version import io.github.gitbucket.solidbase.model.Version
/** /**
* Trait for define plugin interface. * Trait for define plugin interface.

View File

@@ -10,9 +10,11 @@ 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
@@ -187,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
)) ))

View File

@@ -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(

View File

@@ -18,7 +18,7 @@ trait IssuesService {
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) =
if (issueId forall (_.isDigit)) if (isInteger(issueId))
Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption Issues filter (_.byPrimaryKey(owner, repository, issueId.toInt)) firstOption
else None else None
@@ -108,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
@@ -218,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
@@ -228,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)) &&
@@ -431,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",
@@ -475,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)),
@@ -525,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,
@@ -549,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"),

View File

@@ -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
}

View File

@@ -36,7 +36,13 @@ trait RepositoryService { self: AccountService =>
originUserName = originUserName, originUserName = originUserName,
originRepositoryName = originRepositoryName, originRepositoryName = originRepositoryName,
parentUserName = parentUserName, parentUserName = parentUserName,
parentRepositoryName = parentRepositoryName) parentRepositoryName = parentRepositoryName,
enableIssues = true,
externalIssuesUrl = None,
enableWiki = true,
allowWikiEditing = true,
externalWikiUrl = None
)
IssueId insert (userName, repositoryName, 0) IssueId insert (userName, repositoryName, 0)
} }
@@ -222,7 +228,7 @@ trait RepositoryService { self: AccountService =>
* Include public repository, private own repository and private but collaborator repository. * Include public repository, private own repository and private but collaborator repository.
* *
* @param userName the user name of collaborator * @param userName the user name of collaborator
* @return the repository infomation list * @return the repository information list
*/ */
def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = { def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = {
Repositories.filter { t1 => Repositories.filter { t1 =>
@@ -313,10 +319,12 @@ trait RepositoryService { self: AccountService =>
* Save repository options. * Save repository options.
*/ */
def saveRepositoryOptions(userName: String, repositoryName: String, def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], isPrivate: Boolean)(implicit s: Session): Unit = description: Option[String], isPrivate: Boolean,
enableIssues: Boolean, externalIssuesUrl: Option[String],
enableWiki: Boolean, allowWikiEditing: Boolean, externalWikiUrl: Option[String])(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName)) Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => (r.description.?, r.isPrivate, r.updatedDate) } .map { r => (r.description.?, r.isPrivate, r.enableIssues, r.externalIssuesUrl.?, r.enableWiki, r.allowWikiEditing, r.externalWikiUrl.?, r.updatedDate) }
.update (description, isPrivate, currentDate) .update (description, isPrivate, enableIssues, externalIssuesUrl, enableWiki, allowWikiEditing, externalWikiUrl, currentDate)
def saveRepositoryDefaultBranch(userName: String, repositoryName: String, def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
defaultBranch: String)(implicit s: Session): Unit = defaultBranch: String)(implicit s: Session): Unit =
@@ -417,9 +425,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}"

View File

@@ -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

View File

@@ -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

View File

@@ -97,7 +97,7 @@ 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 ${webHook.url}") logger.debug(s"start web hook invocation for ${webHook.url}")
val httpPost = new HttpPost(webHook.url) val httpPost = new HttpPost(webHook.url)
logger.info(s"Content-Type: ${webHook.ctype.ctype}") logger.info(s"Content-Type: ${webHook.ctype.ctype}")
@@ -119,7 +119,7 @@ trait WebHookService {
} }
case WebHookContentType.JSON => { case WebHookContentType.JSON => {
httpPost.setEntity(EntityBuilder.create().setText(json).build()) httpPost.setEntity(EntityBuilder.create().setText(json).build())
if (!webHook.token.isEmpty) { if (webHook.token.exists(_.trim.nonEmpty)) {
httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, json.getBytes("UTF-8"))) httpPost.addHeader("X-Hub-Signature", XHub.generateHeaderXHubToken(XHubConverter.HEXA_LOWERCASE, XHubDigest.SHA1, webHook.token.orNull, json.getBytes("UTF-8")))
} }
} }

View File

@@ -1,187 +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, 14),
new Version(3, 13),
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)
}
}

View File

@@ -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)
} }

View File

@@ -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 = {

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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
}
} }

View File

@@ -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))
} }

View File

@@ -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
}
} }

View File

@@ -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()
}
}

View File

@@ -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())
}
} }
} }

View File

@@ -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()

View File

@@ -84,26 +84,7 @@ class Mailer(private val smtp: Smtp) extends Notifier {
enableLineBreaks = false enableLineBreaks = false
))) { case (subject, msg) => ))) { case (subject, msg) =>
recipients(issue) { to => recipients(issue) { to =>
val email = new HtmlEmail send(to, subject, msg)
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(context.loginAccount.get.userName))
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
} }
} }
} }
@@ -116,6 +97,30 @@ class Mailer(private val smtp: Smtp) extends Notifier {
case t => logger.error("Notifications Failed.", t) case t => logger.error("Notifications Failed.", t)
} }
} }
def send(to: String, subject: String, msg: String)(implicit context: Context): Unit = {
val email = new HtmlEmail
email.setHostName(smtp.host)
email.setSmtpPort(smtp.port.get)
smtp.user.foreach { user =>
email.setAuthenticator(new DefaultAuthenticator(user, smtp.password.getOrElse("")))
}
smtp.ssl.foreach { ssl =>
email.setSSLOnConnect(ssl)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(context.loginAccount.get.userName))
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
}
} }
class MockMailer extends Notifier { class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String) def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)

View File

@@ -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}"
} }

View File

@@ -5,6 +5,7 @@ import org.mozilla.universalchardet.UniversalDetector
import ControlUtil._ import ControlUtil._
import org.apache.commons.io.input.BOMInputStream import org.apache.commons.io.input.BOMInputStream
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
import scala.util.control.Exception._
object StringUtil { object StringUtil {
@@ -26,6 +27,8 @@ object StringUtil {
def splitWords(value: String): Array[String] = value.split("[ \\t ]+") def splitWords(value: String): Array[String] = value.split("[ \\t ]+")
def isInteger(value: String): Boolean = allCatch opt { value.toInt } map(_ => true) getOrElse(false)
def escapeHtml(value: String): String = def escapeHtml(value: String): String =
value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;") value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")

View File

@@ -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")
}
}

View File

@@ -222,8 +222,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Generates the avatar link to the account page. * Generates the avatar link to the account page.
* If user does not exist or disabled, this method returns avatar image without link. * If user does not exist or disabled, this method returns avatar image without link.
*/ */
def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false)(implicit context: Context): Html = def avatarLink(userName: String, size: Int, mailAddress: String = "", tooltip: Boolean = false, label: Boolean = false)
userWithContent(userName, mailAddress)(avatar(userName, size, tooltip, mailAddress)) (implicit context: Context): Html = {
val avatarHtml = avatar(userName, size, tooltip, mailAddress)
val contentHtml = if(label == true) Html(avatarHtml.body + " " + userName) else avatarHtml
userWithContent(userName, mailAddress)(contentHtml)
}
/** /**
* Generates the avatar link to the account page. * Generates the avatar link to the account page.
@@ -232,7 +238,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html = def avatarLink(commit: JGitUtil.CommitInfo, size: Int)(implicit context: Context): Html =
userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size)) userWithContent(commit.authorName, commit.authorEmailAddress)(avatar(commit, size))
private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)(implicit context: Context): Html = private def userWithContent(userName: String, mailAddress: String = "", styleClass: String = "")(content: Html)
(implicit context: Context): Html =
(if(mailAddress.isEmpty){ (if(mailAddress.isEmpty){
getAccountByUserName(userName) getAccountByUserName(userName)
} else { } else {

View File

@@ -1,4 +1,4 @@
@(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._
@@ -6,6 +6,7 @@
<div class="container body"> <div class="container body">
@menu("profile", settings.ssh){ @menu("profile", settings.ssh){
@helper.html.information(info) @helper.html.information(info)
@helper.html.error(error)
@if(LDAPUtil.isDummyMailAddress(account)){<div class="alert alert-danger">Please register your mail address.</div>} @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"> <form action="@url(account.userName)/_edit" method="POST" validate="true">
<div class="panel panel-default"> <div class="panel panel-default">

View File

@@ -2,8 +2,10 @@
@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="body main-center"> <div class="content-wrapper main-center">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true"> <div class="content body">
<h2>@{if(account.isEmpty) "Create group" else "Edit group"}</h2>
<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">
<fieldset class="form-group"> <fieldset class="form-group">
@@ -39,7 +41,7 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<fieldset class="margin"> <fieldset class="border-top">
@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">Delete Group</a> <a href="@url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
@@ -50,7 +52,8 @@
<a href="@url(account.get.userName)" class="btn btn-default">Cancel</a> <a href="@url(account.get.userName)" class="btn btn-default">Cancel</a>
} }
</fieldset> </fieldset>
</form> </form>
</div>
</div> </div>
} }
<script> <script>

View File

@@ -3,29 +3,37 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@html.main(account.userName){ @html.main(account.userName){
<div class="container body"> <div class="main-sidebar">
<div class="main-sidebar"> <div class="sidebar">
<div class="block"> <div class="user-panel">
<div class="account-image">@avatar(account.userName, 240)</div> <div class="pull-left image">@avatar(account.userName, 40)</div>
<div class="account-fullname">@account.fullName</div> <div class="pull-left info">
<div class="account-username">@account.userName</div> <p>@account.userName</p>
@account.fullName
</div>
</div> </div>
<div class="block"> <div style="padding-left: 10px; padding-right: 10px;">
@if(account.url.isDefined){ @if(account.url.isDefined){
<div><i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a></div> <p style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<i class="octicon octicon-home"></i> <a href="@account.url">@account.url</a>
</p>
} }
<div><i class="octicon octicon-clock"></i> <span class="muted">Joined on</span> @date(account.registeredDate)</div> <p style="color: white;">
<i class="octicon octicon-clock"></i> Joined on @date(account.registeredDate)
</p>
</div> </div>
@if(groupNames.nonEmpty){ @if(groupNames.nonEmpty){
<div> <ul class="sidebar-menu">
<div>Groups</div> <li class="header">Groups</li>
@groupNames.map { groupName => @groupNames.map { groupName =>
@avatarLink(groupName, 36, tooltip = true) <li>@avatarLink(groupName, 20, tooltip = true, label = true)</li>
} }
</div> </div>
} }
</div> </div>
<div class="main-content"> </div>
<div class="content-wrapper">
<div class="content body">
<ul class="nav nav-tabs" style="margin-bottom: 5px;"> <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> <li@if(active == "repositories"){ class="active"}><a href="@url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){ @if(account.isGroupAccount){

View File

@@ -1,27 +1,31 @@
@(active: String, ssh: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context) @(active: String, ssh: Boolean)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
<div class="main-sidebar"> <div class="main-sidebar">
<ul class="nav nav-pills nav-stacked"> <div class="sidebar">
<li@if(active=="profile"){ class="active"}> <ul class="sidebar-menu">
<a href="@path/@loginAccount.get.userName/_edit">Profile</a> <li@if(active=="profile"){ class="active"}>
</li> <a href="@path/@loginAccount.get.userName/_edit">Profile</a>
@if(ssh){ </li>
<li@if(active=="ssh"){ class="active"}> @if(ssh){
<a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a> <li@if(active=="ssh"){ class="active"}>
</li> <a href="@path/@loginAccount.get.userName/_ssh">SSH Keys</a>
} </li>
<li@if(active=="application"){ class="active"}>
<a href="@path/@loginAccount.get.userName/_application">Applications</a>
</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>
} }
} <li@if(active=="application"){ class="active"}>
</ul> <a href="@path/@loginAccount.get.userName/_application">Applications</a>
</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>
</div>
</div> </div>
<div class="main-content"> <div class="content-wrapper">
@body <div class="content body">
@body
</div>
</div> </div>

View File

@@ -3,73 +3,75 @@ 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 main-center"> <div class="content-wrapper main-center">
<h2>Create a new repository</h2> <div class="content body">
<p class="muted"> <h2>Create a new repository</h2>
A repository contains all the files for your project, including the revision history. <p class="muted">
</p> A repository contains all the files for your project, including the revision history.
<form id="form" method="post" action="@path/new" validate="true"> </p>
<fieldset class="margin form-group"> <form id="form" method="post" action="@path/new" validate="true">
<dl style="float: left;"> <fieldset class="border-top form-group">
<dt>Owner</dt> <dl style="float: left;">
<dd style="margin-left: 0px;"> <dt>Owner</dt>
<div class="btn-group" id="owner-dropdown"> <dd style="margin-left: 0px;">
<button class="btn dropdown-toggle btn-default" data-toggle="dropdown"> <div class="btn-group" id="owner-dropdown">
<span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span> <button class="btn dropdown-toggle btn-default" data-toggle="dropdown">
<span class="caret"></span> <span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span>
</button> <span class="caret"></span>
<ul class="dropdown-menu"> </button>
<li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="octicon octicon-check"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li> <ul class="dropdown-menu">
@groupNames.map { groupName => <li><a href="javascript:void(0);" data-name="@loginAccount.get.userName"><i class="octicon octicon-check"></i> <span>@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span></a></li>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="octicon"></i> <span>@avatar(groupName, 20) @groupName</span></a></li> @groupNames.map { groupName =>
} <li><a href="javascript:void(0);" data-name="@groupName"><i class="octicon"></i> <span>@avatar(groupName, 20) @groupName</span></a></li>
</ul> }
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/> </ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/>
</div>
</dd>
</dl>
<span class="slash" style="float: left; margin-left: 10px; margin-right: 10px; margin-top: 15px;">/</span>
<dl>
<dt>Repository name</dt>
<dd style="margin-left: 0px;">
<input type="text" name="name" id="name" class="form-control" style="width: 200px;" autofocus />
<span id="error-name" class="error"></span>
</dd>
</dl>
</fieldset>
<fieldset class="form-group">
<label for="description" class="strong">Description (optional):</label>
<input type="text" name="description" id="description" class="form-control" style="width: 95%;"/>
</fieldset>
<fieldset class="border-top">
<label class="radio">
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}>
<span class="strong"><i class="octicon octicon-repo"></i>&nbsp;</i>&nbsp;Public</span>
<div class="normal muted">
Anyone can see this repository. You choose who can commit.
</div> </div>
</dd> </label>
</dl> <label class="radio">
<span class="slash" style="float: left; margin-left: 10px; margin-right: 10px; margin-top: 15px;">/</span> <input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<dl> <span class="strong"><i class="octicon octicon-lock"></i>&nbsp;</i>&nbsp;Private</span>
<dt>Repository name</dt> <div class="normal muted">
<dd style="margin-left: 0px;"> You choose who can see and commit to this repository.
<input type="text" name="name" id="name" class="form-control" style="width: 200px;" autofocus /> </div>
<span id="error-name" class="error"></span> </label>
</dd> </fieldset>
</dl> <fieldset class="border-top">
</fieldset> <label for="createReadme" class="checkbox">
<fieldset class="form-group"> <input type="checkbox" name="createReadme" id="createReadme"/>
<label for="description" class="strong">Description (optional):</label> <span class="strong">Initialize this repository with a README</span>
<input type="text" name="description" id="description" class="form-control" style="width: 95%;"/> <div class="normal muted">
</fieldset> This will let you immediately clone the repository to your computer. Skip this step if youre importing an existing repository.
<fieldset class="margin"> </div>
<label class="radio"> </label>
<input type="radio" name="isPrivate" value="false" @if(isCreateRepoOptionPublic){checked}> </fieldset>
<span class="strong"><i class="octicon octicon-repo"></i>&nbsp;</i>&nbsp;Public</span> <fieldset class="border-top form-actions">
<div class="normal muted"> <input type="submit" class="btn btn-success" value="Create repository"/>
Anyone can see this repository. You choose who can commit. </fieldset>
</div> </form>
</label> </div>
<label class="radio">
<input type="radio" name="isPrivate" value="true" @if(!isCreateRepoOptionPublic){checked}>
<span class="strong"><i class="octicon octicon-lock"></i>&nbsp;</i>&nbsp;Private</span>
<div class="normal muted">
You choose who can see and commit to this repository.
</div>
</label>
</fieldset>
<fieldset class="margin">
<label for="createReadme" class="checkbox">
<input type="checkbox" name="createReadme" id="createReadme"/>
<span class="strong">Initialize this repository with a README</span>
<div class="normal muted">
This will let you immediately clone the repository to your computer. Skip this step if youre importing an existing repository.
</div>
</label>
</fieldset>
<fieldset class="margin form-actions">
<input type="submit" class="btn btn-success" value="Create repository"/>
</fieldset>
</form>
</div> </div>
} }
<script> <script>

View File

@@ -2,49 +2,51 @@
@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="content-wrapper main-center">
<h3>Create your account</h3> <div class="content body">
<form action="@path/register" method="POST" validate="true"> <h2>Create your account</h2>
<div class="row"> <form action="@path/register" method="POST" validate="true">
<div class="col-md-6"> <div class="row">
<fieldset> <div class="col-md-6">
<label for="userName" class="strong">Username:</label> <fieldset>
<input type="text" name="userName" id="userName" value="" autofocus/> <label for="userName" class="strong">Username:</label>
<span id="error-userName" class="error"></span> <input type="text" name="userName" id="userName" value="" class="form-control" autofocus/>
</fieldset> <span id="error-userName" class="error"></span>
<fieldset> </fieldset>
<label for="password" class="strong"> <fieldset>
Password: <label for="password" class="strong">
</label> Password:
<input type="password" name="password" id="password" value=""/> </label>
<span id="error-password" class="error"></span> <input type="password" name="password" id="password" class="form-control" value=""/>
</fieldset> <span id="error-password" class="error"></span>
<fieldset> </fieldset>
<label for="fullName" class="strong">Full Name:</label> <fieldset>
<input type="text" name="fullName" id="fullName" value=""/> <label for="fullName" class="strong">Full Name:</label>
<span id="error-fullName" class="error"></span> <input type="text" name="fullName" id="fullName" class="form-control" value=""/>
</fieldset> <span id="error-fullName" class="error"></span>
<fieldset> </fieldset>
<label for="mailAddress" class="strong">Mail Address:</label> <fieldset>
<input type="text" name="mailAddress" id="mailAddress" value=""/> <label for="mailAddress" class="strong">Mail Address:</label>
<span id="error-mailAddress" class="error"></span> <input type="text" name="mailAddress" id="mailAddress" class="form-control" value=""/>
</fieldset> <span id="error-mailAddress" class="error"></span>
<fieldset> </fieldset>
<label for="url" class="strong">URL (optional):</label> <fieldset>
<input type="text" name="url" id="url" style="width: 400px;" value=""/> <label for="url" class="strong">URL (optional):</label>
<span id="error-url" class="error"></span> <input type="text" name="url" id="url" class="form-control" value=""/>
</fieldset> <span id="error-url" class="error"></span>
</fieldset>
</div>
<div class="col-md-6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(None)
</fieldset>
</div>
</div> </div>
<div class="col-md-6"> <fieldset class="border-top">
<fieldset> <input type="submit" class="btn btn-success" value="Create account"/>
<label for="avatar" class="strong">Image (optional):</label> </fieldset>
@helper.html.uploadavatar(None) </form>
</fieldset> </div>
</div>
</div>
<fieldset class="margin">
<input type="submit" class="btn btn-success" value="Create account"/>
</fieldset>
</form>
</div> </div>
} }

View 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>

View File

@@ -1,8 +1,8 @@
@(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="main-sidebar">
<div class="main-sidebar"> <div class="sidebar">
<ul class="nav nav-pills nav-stacked" id="system-admin-menu-container"> <ul class="sidebar-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>
@@ -12,8 +12,11 @@
<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"}>
<a href="@path/admin/data">Data export / import</a>
</li>
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@path/console/login.jsp" target="_blank">H2 Console</a>
</li> </li>
@gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu => @gitbucket.core.plugin.PluginRegistry().getSystemSettingMenus.map { menu =>
@menu(context).map { link => @menu(context).map { link =>
@@ -24,7 +27,9 @@
} }
</ul> </ul>
</div> </div>
<div class="main-content"> </div>
<div class="content-wrapper">
<div class="content body">
@body @body
</div> </div>
</div> </div>

View File

@@ -18,19 +18,19 @@
<div class="panel-body"> <div class="panel-body">
<div class="row"> <div class="row">
<label class="col-md-2">Id</label> <label class="col-md-2">Id</label>
<span class="col-md-8">@plugin.pluginId</span> <span class="col-md-10">@plugin.pluginId</span>
</div> </div>
<div class="row"> <div class="row">
<label class="col-md-2">Version</label> <label class="col-md-2">Version</label>
<span class="col-md-8">@plugin.version</span> <span class="col-md-10">@plugin.version</span>
</div> </div>
<div class="row"> <div class="row">
<label class="col-md-2">Name</label> <label class="col-md-2">Name</label>
<span class="col-md-8">@plugin.pluginName</span> <span class="col-md-10">@plugin.pluginName</span>
</div> </div>
<div class="row"> <div class="row">
<label class="col-md-2">Description</label> <label class="col-md-2">Description</label>
<span class="col-md-8 muted">@plugin.description</span> <span class="col-md-10 muted">@plugin.description</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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 -->
<!--====================================================================--> <!--====================================================================-->
@@ -85,10 +97,15 @@
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>
<label><span class="strong">Limit of activity logs</span> (Unlimited if it's not specified or zero)</label> <label><span class="strong">Limit of activity logs</span> (Unlimited if it's not specified or zero)</label>
<div class="controls"> <fieldset>
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@settings.activityLogLimit"/> <div class="form-group">
<span id="error-activityLogLimit" class="error"></span> <label class="control-label col-md-3" for="activityLogLimit">Limit</label>
</div> <div class="col-md-9">
<input type="text" id="activityLogLimit" name="activityLogLimit" class="form-control input-mini" value="@settings.activityLogLimit"/>
<span id="error-activityLogLimit" class="error"></span>
</div>
</div>
</fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Services --> <!-- Services -->
<!--====================================================================--> <!--====================================================================-->
@@ -277,7 +294,7 @@
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="smtpPassword">Enable SSL</label> <label class="control-label col-md-3" for="smtpPassword">Enable SSL</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="checkbox" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/> <input type="checkbox" id="smtpSsl" name="smtp.ssl"@if(settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -292,6 +309,12 @@
<input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@settings.smtp.map(_.fromName)"/> <input type="text" id="fromName" name="smtp.fromName" class="form-control" value="@settings.smtp.map(_.fromName)"/>
</div> </div>
</div> </div>
<div class="text-right">
Send test mail to:
<input type="text" id="testAddress" size="30"/>
<input type="button" id="sendTestMail" value="Send"/>
</div>
<p class="muted"> <p class="muted">
Enable notification not only SMTP configuration if you want to send notification email. Enable notification not only SMTP configuration if you want to send notification email.
</p> </p>
@@ -306,6 +329,40 @@
} }
<script> <script>
$(function(){ $(function(){
$('#sendTestMail').click(function(){
var host = $('#smtpHost' ).val();
var port = $('#smtpPort' ).val();
var user = $('#smtpUser' ).val();
var password = $('#smtpPassword').val();
var ssl = $('#smtpSsl' ).prop('checked');
var fromAddress = $('#fromAddress' ).val();
var fromName = $('#fromName' ).val();
var testAddress = $('#testAddress' ).val();
if(host == ''){
alert('SMTP Host is required.');
$('#smtpHost').focus();
} else if(testAddress == ''){
alert('Destination is required.');
$('#testAddress').focus();
} else {
$.post('@path/admin/system/sendmail', {
'smtp.host': host,
'smtp.port': port,
'smtp.user': user,
'smtp.password': password,
'smtp.ssl': ssl,
'smtp.fromAddress': fromAddress,
'smtp.fromName': fromName,
'testAddress': testAddress
}, function(data, status){
if(data != ''){
alert(data);
}
});
}
});
$('#ssh').change(function(){ $('#ssh').change(function(){
$('.ssh input').prop('disabled', !$(this).prop('checked')); $('.ssh input').prop('disabled', !$(this).prop('checked'));
}).change(); }).change();

View File

@@ -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">
@@ -74,7 +75,7 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<fieldset class="margin"> <fieldset class="border-top">
<input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/> <input type="submit" class="btn btn-success" value="@if(account.isEmpty){Create User} else {Update User}"/>
<a href="@path/admin/users" class="btn btn-default">Cancel</a> <a href="@path/admin/users" class="btn btn-default">Cancel</a>
</fieldset> </fieldset>

View File

@@ -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">
@@ -45,7 +45,7 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<fieldset class="margin"> <fieldset class="border-top">
<input type="submit" class="btn btn-success" 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}"/>
<a href="@path/admin/users" class="btn btn-default">Cancel</a> <a href="@path/admin/users" class="btn btn-default">Cancel</a>
</fieldset> </fieldset>
@@ -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,18 +103,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(' ')
@@ -125,9 +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 + ':' + $(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);
} }

View File

@@ -2,71 +2,62 @@
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context) userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(body: Html)(implicit context: gitbucket.core.controller.Context)
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
<div class="container body"> <div class="main-sidebar">
<div class="dashboard-sidebar"> <div class="sidebar">
@if(loginAccount.isEmpty){ <ul class="nav sidebar-menu">
<div id="dashboard-signin-form">@html.signinform(settings)</div> @if(loginAccount.isDefined){
<li class="header">Your repositories <span class="label label-primary pull-right">@userRepositories.size</span></li>
@if(userRepositories.isEmpty){
<li>No repositories</li>
} else { } else {
<div class="panel panel-default"> @defining(10){ max =>
<div class="panel-heading strong"> @userRepositories.zipWithIndex.map { case (repository, i) =>
Your repositories <span class="badge">@userRepositories.size</span> <li class="repo-link" style="@if(i > max - 1){display:none;}">
</div> @if(repository.owner == loginAccount.get.userName){
<ul class="list-group list-group-flush"> <a href="@url(repository)">@helper.html.repositoryicon(repository, false) <span class="strong">@repository.name</span></a>
@if(userRepositories.isEmpty){ } else {
<li class="list-group-item">No repositories</li> <a href="@url(repository)">@helper.html.repositoryicon(repository, false) @repository.owner/<span class="strong">@repository.name</span></a>
} else {
@defining(20){ max =>
@userRepositories.zipWithIndex.map { case (repository, i) =>
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
@helper.html.repositoryicon(repository, false)
@if(repository.owner == loginAccount.get.userName){
<a href="@url(repository)"><span class="strong">@repository.name</span></a>
} else {
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
}
</li>
}
@if(userRepositories.size > max){
<li class="list-group-item show-more">
<a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a>
</li>
}
} }
</li>
} }
</ul> @if(userRepositories.size > max){
</div> <li class="show-more">
} <a href="javascript:void(0);" id="show-more-repos">Show @{userRepositories.size - max} more repositories...</a>
<div class="panel panel-default"> </li>
<div class="panel-heading strong">Recent updated repositories</div>
<ul class="list-group list-group-flush">
@if(recentRepositories.isEmpty){
<li class="list-group-item">No repositories</li>
} else {
@defining(20){ max =>
@recentRepositories.zipWithIndex.map { case (repository, i) =>
<li class="list-group-item repo-link" style="@if(i > max - 1){display:none;}">
@helper.html.repositoryicon(repository, false)
<a href="@url(repository)">@repository.owner/<span class="strong">@repository.name</span></a>
</li>
}
@if(recentRepositories.size > max){
<li class="list-group-item show-more">
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
</li>
}
} }
} }
</ul> }
</div> } else {
<li class="header">Recent updated repositories</li>
@if(recentRepositories.isEmpty){
<li>No repositories</li>
} else {
@defining(10){ max =>
@recentRepositories.zipWithIndex.map { case (repository, i) =>
<li class="repo-link" style="@if(i > max - 1){display:none;}">
<a href="@url(repository)">@helper.html.repositoryicon(repository, false) @repository.owner/<span class="strong">@repository.name</span></a>
</li>
}
@if(recentRepositories.size > max){
<li class="show-more">
<a href="javascript:void(0);" id="show-more-recent-repos">Show @{recentRepositories.size - max} more repositories...</a>
</li>
}
}
}
}
</ul>
</div> </div>
<div class="dashboard-content"> </div>
<div class="content-wrapper">
<div class="content body">
@body @body
</div> </div>
</div> </div>
<script> <script>
$(function(){ $(function(){
$('#show-more-repos, #show-more-recent-repos').click(function(e){ $('#show-more-repos, #show-more-recent-repos').click(function(e){
$(e.target).parents('ul.list-group').find('li.repo-link').show(); $(e.target).parents('ul').find('li.repo-link').show();
$(e.target).parents('li.show-more').remove(); $(e.target).parents('li.show-more').remove();
}); });
}); });

View File

@@ -10,15 +10,13 @@
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import org.eclipse.jgit.diff.DiffEntry.ChangeType @import org.eclipse.jgit.diff.DiffEntry.ChangeType
@if(showIndex){ @if(showIndex){
<div style="overflow: hidden;"> <div class="pull-right" style="margin-bottom: 10px;">
<div class="pull-right" style="margin-bottom: 10px;"> <div class="btn-group" data-toggle="buttons-radio">
<div class="btn-group" data-toggle="buttons-radio"> <input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified">
<input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified"> <input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
<input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
</div>
</div> </div>
Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @plural(diffs.size, "file")</a>
</div> </div>
Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @plural(diffs.size, "file")</a>
<ul id="commit-file-list" style="display: none;"> <ul id="commit-file-list" style="display: none;">
@diffs.zipWithIndex.map { case (diff, i) => @diffs.zipWithIndex.map { case (diff, i) =>
<li@if(i > 0){ class="border"}> <li@if(i > 0){ class="border"}>

View File

@@ -20,7 +20,7 @@
<div class="edit-title pull-right" style="display: none;"> <div class="edit-title pull-right" style="display: none;">
<a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a> <a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a>
</div> </div>
<h1> <h1 class="body-title">
<span class="show-title"> <span class="show-title">
<span id="show-title">@issue.title</span> <span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span> <span class="muted">#@issue.issueId</span>
@@ -46,7 +46,7 @@
</span> </span>
</div> </div>
<hr> <hr>
<div class="row-fluid" style="margin-top: 15px;"> <div style="margin-top: 15px;">
<div class="col-md-9"> <div class="col-md-9">
@commentlist(Some(issue), comments, hasWritePermission, repository) @commentlist(Some(issue), comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository) @commentform(issue, true, hasWritePermission, repository)

View File

@@ -51,23 +51,28 @@
} }
@helper.html.dropdown("Milestone") { @helper.html.dropdown("Milestone") {
<li> <li>
<a href="@condition.copy(milestone = Some(None)).toURL"> <a href="@condition.copy(milestone = (if(condition.milestone == Some(None)) None else Some(None))).toURL">
@helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone @helper.html.checkicon(condition.milestone == Some(None)) Issues with no milestone
</a> </a>
</li> </li>
@milestones.filter(_.closedDate.isEmpty).map { milestone => @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="@condition.copy(milestone = Some(Some(milestone.title))).toURL"> <a href="@condition.copy(milestone = (if(condition.milestone == Some(Some(milestone.title))) None else Some(Some(milestone.title)))).toURL">
@helper.html.checkicon(condition.milestone == Some(Some(milestone.title))) @milestone.title @helper.html.checkicon(condition.milestone == Some(Some(milestone.title))) @milestone.title
</a> </a>
</li> </li>
} }
} }
@helper.html.dropdown("Assignee") { @helper.html.dropdown("Assignee") {
<li>
<a href="@condition.copy(assigned = (if(condition.assigned == Some(None)) None else Some(None))).toURL">
@helper.html.checkicon(condition.assigned == Some(None)) Assigned to nobody
</a>
</li>
@collaborators.map { collaborator => @collaborators.map { collaborator =>
<li> <li>
<a href="@condition.copy(assigned = Some(collaborator)).toURL"> <a href="@condition.copy(assigned = (if(condition.assigned == Some(Some(collaborator))) None else Some(Some(collaborator)))).toURL">
@helper.html.checkicon(condition.assigned == Some(collaborator)) @helper.html.checkicon(condition.assigned == Some(Some(collaborator)))
@avatar(collaborator, 20) @collaborator @avatar(collaborator, 20) @collaborator
</a> </a>
</li> </li>

View File

@@ -1,6 +1,5 @@
@(title: String, repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: gitbucket.core.controller.Context) @(title: String, repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.plugin.PluginRegistry @import gitbucket.core.plugin.PluginRegistry
@import gitbucket.core.servlet.AutoUpdate
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
<!DOCTYPE html> <!DOCTYPE html>
@@ -12,11 +11,13 @@
<link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" /> <link rel="icon" href="@assets/common/images/gitbucket.png" type="image/vnd.microsoft.icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="@assets/vendors/bootstrap-3.3.6/css/bootstrap.min.css" rel="stylesheet"> <link href="@assets/vendors/bootstrap-3.3.6/css/bootstrap.min.css" rel="stylesheet">
<link href="@assets/vendors/octicons/octicons.css" rel="stylesheet"> <link href="@assets/vendors/octicons-4.2.0/octicons.css" rel="stylesheet">
<link href="@assets/vendors/datepicker/css/bootstrap-datetimepicker.min.css" rel="stylesheet"> <link href="@assets/vendors/datepicker/css/bootstrap-datetimepicker.min.css" rel="stylesheet">
<link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet"> <link href="@assets/vendors/colorpicker/css/bootstrap-colorpicker.css" rel="stylesheet">
<link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/> <link href="@assets/vendors/google-code-prettify/prettify.css" type="text/css" rel="stylesheet"/>
<link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/> <link href="@assets/vendors/facebox/facebox.css" rel="stylesheet"/>
<link href="@assets/vendors/AdminLTE-2.2.3/css/AdminLTE.min.css" rel="stylesheet">
<link href="@assets/vendors/AdminLTE-2.2.3/css/skins/skin-blue.min.css" rel="stylesheet">
<link href="@assets/common/css/gitbucket.css" rel="stylesheet"> <link href="@assets/common/css/gitbucket.css" rel="stylesheet">
<script src="@assets/vendors/jquery/jquery-1.11.1.js"></script> <script src="@assets/vendors/jquery/jquery-1.11.1.js"></script>
<script src="@assets/vendors/dropzone/dropzone.js"></script> <script src="@assets/vendors/dropzone/dropzone.js"></script>
@@ -37,77 +38,73 @@
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" /> <meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
} }
} }
<script src="@assets/vendors/AdminLTE-2.2.3/js/app.js" type="text/javascript"></script>
</head> </head>
<body> <body class="skin-blue">
<form id="search" action="@path/search" method="POST" class="form-inline"> <div class="wrapper">
<nav class="navbar navbar-default"> <header class="main-header">
<div class="container"> <a href="@path/" class="logo">
@* TODO: for plugi-ins? <img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px; display: inline;"/>
<button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> GitBucket
<span class="icon-bar"></span> <span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
<span class="icon-bar"></span> </a>
<span class="icon-bar"></span> <nav class="navbar navbar-static-top" role="navigation">
</button> @repository.map { repository =>
*@ <form id="search" action="@path/search" method="POST" class="navbar-form navbar-left" role="search">
<a class="navbar-brand" href="@path/"> <div class="form-group">
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px; display: inline;"/>GitBucket <input type="text" name="query" id="navbar-search-input" class="form-control" placeholder="Search this repository"/>
@defining(AutoUpdate.getCurrentVersion){ version => <input type="hidden" name="owner" value="@repository.owner"/>
<span class="header-version">@version.majorVersion.@version.minorVersion</span> <input type="hidden" name="repository" value="@repository.name"/>
}
</a>
@if(loginAccount.isDefined){
@repository.map { repository =>
<input type="text" name="query" class="form-control" style="width: 400px; margin-top: 3px; margin-bottom: 3px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
<a href="@path/dashboard/pulls" class="global-header-menu">Pull requests</a>
<a href="@path/dashboard/issues" class="global-header-menu">Issues</a>
} else {
@* TODO: merge with below *@
@repository.map { repository =>
<input type="text" name="query" class="form-control" style="width: 400px; margin-top: 3px; margin-bottom: 3px;" placeholder="Search this repository"/>
<input type="hidden" name="owner" value="@repository.owner"/>
<input type="hidden" name="repository" value="@repository.name"/>
}
}
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
@menu(context).map { link =>
<a href="@path/@link.path" class="global-header-menu">@link.label</a>
}
}
@if(loginAccount.isDefined){
<div class="pull-right" style="margin-top: 6px;">
<div class="btn-group" style="margin-right: 8px;">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#">
<i class="octicon octicon-plus" style="color: black; font-size: 20px; vertical-align: middle;height:20px !important;"></i><span class="caret" style="color: black; vertical-align: middle;"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
</div> </div>
<div class="btn-group"> </form>
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">
@avatar(loginAccount.get.userName, 16)<span class="caret" style="color: black; vertical-align: middle;"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="@url(loginAccount.get.userName)">Your profile</a></li>
<li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li>
@if(loginAccount.get.isAdmin){
<li><a href="@path/admin/users">System administration</a></li>
}
<li><a href="@path/signout">Sign out</a></li>
</ul>
</div>
</div>
} else {
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="btn btn-default pull-right" style="margin-top: 3px; margin-bottom: 3px;" id="signin">Sign in</a>
} }
</div> <ul class="nav navbar-nav">
</nav> @if(loginAccount.isDefined){
</form> <li><a href="@path/dashboard/pulls">Pull requests</a></li>
@body <li><a href="@path/dashboard/issues">Issues</a></li>
}
@gitbucket.core.plugin.PluginRegistry().getGlobalMenus.map { menu =>
@menu(context).map { link =>
<li><a href="@path/@link.path">@link.label</a></li>
}
}
</ul>
<div class="navbar-custom-menu">
<ul class="nav navbar-nav">
@if(loginAccount.isDefined){
<li class="dropdown">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#">
<i class="octicon octicon-plus" style="color: black;"></i><span class="caret" style="color: black; vertical-align: middle;"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="@path/new">New repository</a></li>
<li><a href="@path/groups/new">New group</a></li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle menu" data-toggle="dropdown" href="#" data-toggle="tooltip" data-placement="bottom" title="Signed is as @loginAccount.get.userName">
@avatar(loginAccount.get.userName, 16)<span class="caret" style="color: black; vertical-align: middle;"></span>
</a>
<ul class="dropdown-menu pull-right">
<li><a href="@url(loginAccount.get.userName)">Your profile</a></li>
<li><a href="@url(loginAccount.get.userName)/_edit">Account settings</a></li>
@if(loginAccount.get.isAdmin){
<li><a href="@path/admin/users">System administration</a></li>
}
<li><a href="@path/signout">Sign out</a></li>
</ul>
</li>
} else {
<li>
<a href="@path/signin?redirect=@urlEncode(currentPath)" class="pull-right" id="signin">Sign in</a>
</li>
}
</ul>
</div>
</nav>
</header>
@body
</div>
<script> <script>
$(function(){ $(function(){
$('#search').submit(function(){ $('#search').submit(function(){

View File

@@ -8,65 +8,80 @@
@menuitem(path: String, name: String, label: String, icon: String, count: Int = 0) = { @menuitem(path: String, name: String, label: String, icon: String, count: Int = 0) = {
<li @if(active == name){class="active"}> <li @if(active == name){class="active"}>
<a href="@url(repository)@path"> @if(path.startsWith("http")){
<i class="menu-icon octicon octicon-@icon"></i> <a href="@path" target="_blank">
<span class="pc"> <i class="menu-icon octicon octicon-@icon"></i> <span class="pc">@label @if(count > 0) { <span class="label label-primary pull-right">@count</span> }</span>
@label </a>
@if(count > 0){ } else {
<span class="badge">@count</span> <a href="@url(repository)@path">
} <i class="menu-icon octicon octicon-@icon"></i> <span class="pc">@label @if(count > 0) { <span class="label label-primary pull-right">@count</span> }</span>
</span> </a>
</a> }
</li> </li>
} }
<div class="headbar"> <div class="main-sidebar">
<div class="container"> <div class="sidebar">
@helper.html.information(info) <ul class="sidebar-menu">
@helper.html.error(error) @menuitem("", "files", "Files", "code")
<div class="head"> @if(repository.commitCount != 0) {
@helper.html.repositoryicon(repository, true) @menuitem("/branches", "branches", "Branches", "git-branch", repository.branchList.length)
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a> @menuitem("/tags", "tags", "Tags", "tag", repository.tags.length)
}
@if(repository.repository.enableIssues) {
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
@menuitem("/pulls", "pulls", "Pull Requests", "git-pull-request", repository.pullCount)
@menuitem("/issues/labels", "labels", "Labels", "tag")
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
} else {
@repository.repository.externalIssuesUrl.map { externalIssuesUrl =>
@menuitem(externalIssuesUrl, "issues", "Issues", "issue-opened")
}
}
@if(repository.repository.enableWiki) {
@menuitem("/wiki", "wiki", "Wiki", "book")
} else {
@repository.repository.externalWikiUrl.map { externalWikiUrl =>
@menuitem(externalWikiUrl, "wiki", "Wiki", "book")
}
}
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@menuitem("/settings", "settings", "Settings", "tools")
}
@gitbucket.core.plugin.PluginRegistry().getRepositoryMenus.map { menu =>
@menu(repository, context).map { link =>
@menuitem(link.path, link.id, link.label, link.icon.getOrElse("ruby"))
}
}
</ul>
</div>
</div>
<div class="content-wrapper">
<div class="content body">
<div class="headbar">
<div class="container">
@helper.html.information(info)
@helper.html.error(error)
<div class="head">
@helper.html.repositoryicon(repository, true)
<a href="@url(repository.owner)">@repository.owner</a> / <a href="@url(repository)" class="strong">@repository.name</a>
@defining(repository.repository){ x => @defining(repository.repository){ x =>
@if(repository.repository.originRepositoryName.isDefined){ @if(repository.repository.originRepositoryName.isDefined){
<div class="forked"> <div class="forked">
forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a> forked from <a href="@path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
@x.description.map { description =>
<div class="normal muted" style="margin-left: 36px; font-size: 80%;">@detectAndRenderLinks(description)</div>
}
}
</div> </div>
} </div>
@x.description.map { description => </div>
<div class="normal muted" style="margin-left: 36px; font-size: 80%;">@detectAndRenderLinks(description)</div> @body
}
}
</div> </div>
</div> </div>
</div> </div>
<div class="container body">
<div class="main-sidebar">
<ul class="nav nav-pills nav-stacked">
@menuitem("" ,"files" ,"Files", "code")
@if(repository.commitCount != 0) {
@menuitem("/branches" ,"branches" ,"Branches", "git-branch", repository.branchList.length)
@menuitem("/tags" ,"tags" ,"Tags", "tag", repository.tags.length)
}
@menuitem("/issues" ,"issues" ,"Issues", "issue-opened", repository.issueCount)
@menuitem("/pulls" ,"pulls" ,"Pull Requests", "git-pull-request", repository.pullCount)
@menuitem("/issues/labels" ,"labels" ,"Labels", "tag")
@menuitem("/issues/milestones" ,"milestones" ,"Milestones", "milestone")
@menuitem("/wiki" ,"wiki" ,"Wiki", "book")
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
@if(loginAccount.isDefined && (loginAccount.get.isAdmin || repository.managers.contains(loginAccount.get.userName))){
@menuitem("/settings" , "settings" , "Settings", "tools")
}
@gitbucket.core.plugin.PluginRegistry().getRepositoryMenus.map { menu =>
@menu(repository, context).map { link =>
@menuitem(link.path, link.id, link.label, link.icon.getOrElse("ruby"))
}
}
</ul>
</div>
<div class="main-content">
@body
</div>
</div>

View File

@@ -27,13 +27,13 @@
<pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre> <pre id="description-@commit.id" style="display: none;" class="commit-description">@link(commit.description.get, repository)</pre>
} }
<div> <div>
@user(commit.authorName, commit.authorEmailAddress, "username")
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
@if(commit.isDifferentFromAuthor) { @if(commit.isDifferentFromAuthor) {
@user(commit.authorName, commit.authorEmailAddress, "username")
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
<span class="octicon octicon-arrow-right" style="margin-top : -2px;"></span> <span class="octicon octicon-arrow-right" style="margin-top : -2px;"></span>
@user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
} }
@user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @helper.html.datetimeago(commit.commitTime)</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
@(commits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]], @(title: String,
commits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo], diffs: Seq[gitbucket.core.util.JGitUtil.DiffInfo],
members: List[(String, String)], members: List[(String, String)],
comments: List[gitbucket.core.model.Comment], comments: List[gitbucket.core.model.Comment],
@@ -56,7 +57,7 @@
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
<span class="error" id="error-title"></span> <span class="error" id="error-title"></span>
<input type="text" name="title" class="form-control" style="margin-bottom: 6px;" placeholder="Title"/> <input type="text" name="title" value="@title" class="form-control" style="margin-bottom: 6px;" placeholder="Title"/>
@helper.html.preview( @helper.html.preview(
repository = repository, repository = repository,
content = "", content = "",

View File

@@ -10,43 +10,40 @@
@import context._ @import context._
@import gitbucket.core.view.helpers._ @import gitbucket.core.view.helpers._
@import gitbucket.core.model._ @import gitbucket.core.model._
<div class="col-md-9">
<div class="row"> <div id="comment-list">
<div class="col-md-9"> @issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq))
<div id="comment-list"> </div>
@issues.html.commentlist(Some(issue), comments, hasWritePermission, repository, Some(pullreq)) @defining(comments.flatMap {
</div> case comment: gitbucket.core.model.IssueComment => Some(comment)
@defining(comments.flatMap { case other => None
case comment: gitbucket.core.model.IssueComment => Some(comment) }.exists(_.action == "merge")){ merged =>
case other => None @if(!issue.closed){
}.exists(_.action == "merge")){ merged => <div class="check-conflict" style="display: none;">
@if(!issue.closed){ <div class="box issue-comment-box" style="background-color: #fbeed5">
<div class="check-conflict" style="display: none;"> <div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
<div class="box issue-comment-box" style="background-color: #fbeed5"> <img src="@assets/common/images/indicator.gif"/> Checking...
<div class="box-content"class="issue-content" style="border: 1px solid #c09853; padding: 10px;">
<img src="@assets/common/images/indicator.gif"/> Checking...
</div>
</div> </div>
</div> </div>
} </div>
@if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged &&
pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
<div class="box issue-comment-box" style="background-color: #d0eeff;">
<div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
<a href="@url(repository)/pull/@issue.issueId/delete/@encodeRefName(pullreq.requestBranch)" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
<div>
<span class="strong">Pull request successfully merged and closed</span>
</div>
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
} }
</div> @if(hasWritePermission && issue.closed && pullreq.userName == pullreq.requestUserName && merged &&
<div class="col-md-3"> pullreq.repositoryName == pullreq.requestRepositoryName && repository.branchList.contains(pullreq.requestBranch)){
@issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository) <div class="box issue-comment-box" style="background-color: #d0eeff;">
</div> <div class="box-content"class="issue-content" style="border: 1px solid #87a8c9; padding: 10px;">
<a href="@url(repository)/pull/@issue.issueId/delete/@encodeRefName(pullreq.requestBranch)" class="btn btn-info pull-right delete-branch" data-name="@pullreq.requestBranch">Delete branch</a>
<div>
<span class="strong">Pull request successfully merged and closed</span>
</div>
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
</div>
</div>
}
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
}
</div>
<div class="col-md-3">
@issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
<script> <script>
$(function(){ $(function(){

View File

@@ -13,26 +13,24 @@
@if(!status.statuses.isEmpty){ @if(!status.statuses.isEmpty){
<div class="build-statuses"> <div class="build-statuses">
@defining(status.commitStateSummary){ case (summaryState, summary) => @defining(status.commitStateSummary){ case (summaryState, summary) =>
<div class="build-status-item-header"> <a class="pull-right" id="toggle-all-checks"></a>
<a class="pull-right" id="toggle-all-checks"></a> <span class="build-status-icon text-@{summaryState.name}">@commitStateIcon(summaryState)</span>
<span class="build-status-icon text-@{summaryState.name}">@commitStateIcon(summaryState)</span> <strong class="text-@{summaryState.name}">@commitStateText(summaryState, pullreq.commitIdTo)</strong>
<strong class="text-@{summaryState.name}">@commitStateText(summaryState, pullreq.commitIdTo)</strong> <span class="text-@{summaryState.name}">— @summary checks</span>
<span class="text-@{summaryState.name}">— @summary checks</span>
</div>
} }
<div class="build-statuses-list" style="@if(status.isAllSuccess){ display:none; }else{ }"> </div>
@status.statusesAndRequired.map{ case (status, required) => <div class="build-statuses-list" style="@if(status.isAllSuccess){ display:none; }">
@status.statusesAndRequired.map { case (status, required) =>
<div class="build-status-item"> <div class="build-status-item">
<div class="pull-right"> <div class="pull-right">
@if(required){ <span class="label">Required</span> } @if(required){ <span class="label">Required</span> }
@status.targetUrl.map{ url => <a href="@url">Details</a> } @status.targetUrl.map { url => <a href="@url">Details</a> }
</div> </div>
<span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span> <span class="build-status-icon text-@{status.state.name}">@commitStateIcon(status.state)</span>
<strong>@status.context</strong> <strong>@status.context</strong>
@status.description.map{ desc => <span class="muted">— @desc</span> } @status.description.map { desc => <span class="muted">— @desc</span> }
</div> </div>
} }
</div>
</div> </div>
} }
<div style="padding:15px"> <div style="padding:15px">
@@ -46,43 +44,47 @@
Only those with write access to this repository can merge pull requests. Only those with write access to this repository can merge pull requests.
} }
</div> </div>
} else { @if(status.branchIsOutOfDate){ } else {
@if(status.hasUpdatePermission){ @if(status.branchIsOutOfDate){
<div class="pull-right"> @if(status.hasUpdatePermission){
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/update_branch"> <div class="pull-right">
<input type="hidden" name="expected_head_oid" value="@pullreq.commitIdFrom"> <form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/update_branch">
<button class="btn btn-default"@if(!status.canUpdate){ disabled="true"} id="update-branch-button">Update branch</button> <input type="hidden" name="expected_head_oid" value="@pullreq.commitIdFrom">
</form> <button class="btn btn-default"@if(!status.canUpdate){ disabled="true"} id="update-branch-button">Update branch</button>
</div> </form>
} </div>
}
<div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div> <div class="merge-indicator merge-indicator-alert"><span class="octicon octicon-alert"></span></div>
<span class="strong">This branch is out-of-date with the base branch</span> <span class="strong">This branch is out-of-date with the base branch</span>
<div class="small"> <div class="small">
Merge the latest changes from <code>@pullreq.branch</code> into this branch. Merge the latest changes from <code>@pullreq.branch</code> into this branch.
</div> </div>
} else { @if(status.hasRequiredStatusProblem) {
<div class="merge-indicator merge-indicator-warning"><span class="octicon octicon-primitive-dot"></span></div>
<span class="strong">Required statuses must pass before merging.</span>
<div class="small">
All required status checks on this pull request must run successfully to enable automatic merging.
</div>
} else { } else {
<div class="merge-indicator merge-indicator-success"><span class="octicon octicon-check"></span></div> @if(status.hasRequiredStatusProblem) {
@if(status.hasMergePermission){ <div class="merge-indicator merge-indicator-warning"><span class="octicon octicon-primitive-dot"></span></div>
<span class="strong">Merging can be performed automatically.</span> <span class="strong">Required statuses must pass before merging.</span>
<div class="small"> <div class="small">
Merging can be performed automatically. All required status checks on this pull request must run successfully to enable automatic merging.
</div> </div>
} else { } else {
<span class="strong">This branch has no conflicts with the base branch.</span> <div class="merge-indicator merge-indicator-success"><span class="octicon octicon-check"></span></div>
<div class="small"> @if(status.hasMergePermission){
Only those with write access to this repository can merge pull requests. <span class="strong">Merging can be performed automatically.</span>
</div> <div class="small">
Merging can be performed automatically.
</div>
} else {
<span class="strong">This branch has no conflicts with the base branch.</span>
<div class="small">
Only those with write access to this repository can merge pull requests.
</div>
}
} }
} } } }
}
</div> </div>
@if(status.hasMergePermission){ @if(status.hasMergePermission){
<div style="padding:15px;border-top:solid 1px #e5e5e5;background:#fafafa"> <div style="padding:15px; border-top:solid 1px #e5e5e5; background:#fafafa">
<input type="button" class="btn @if(!status.hasProblem){btn-success} else {btn-default}" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/> <input type="button" class="btn @if(!status.hasProblem){btn-success} else {btn-default}" id="merge-pull-request-button" value="Merge pull request"@if(!status.canMerge){ disabled="true"}/>
&nbsp;&nbsp;You can also merge branches on the <a href="#" class="show-command-line">command line</a>. &nbsp;&nbsp;You can also merge branches on the <a href="#" class="show-command-line">command line</a>.
<div id="command-line" style="display: none;margin-top: 15px;"> <div id="command-line" style="display: none;margin-top: 15px;">
@@ -133,15 +135,15 @@
</div> </div>
} }
</div> </div>
<div id="confirm-merge-form" style="display: none;"> <div id="confirm-merge-form" style="display: none; padding: 12px;">
<form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge"> <form method="POST" action="@url(originRepository)/pull/@pullreq.issueId/merge">
<div class="strong"> <div class="strong">
Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch} Merge pull request #@issue.issueId from @{pullreq.requestUserName}/@{pullreq.requestBranch}
</div> </div>
<span id="error-message" class="error"></span> <span id="error-message" class="error"></span>
<textarea name="message" style="height: 80px;" class="form-control">@issue.title</textarea> <textarea name="message" style="height: 80px; margin-top: 8px; margin-bottom: 8px;" class="form-control">@issue.title</textarea>
<div> <div>
<input type="button" class="btn" value="Cancel" id="cancel-merge-pull-request"/> <input type="button" class="btn btn-default" value="Cancel" id="cancel-merge-pull-request"/>
<input type="submit" class="btn btn-success" value="Confirm merge"/> <input type="submit" class="btn btn-success" value="Confirm merge"/>
</div> </div>
</form> </form>
@@ -168,6 +170,10 @@ $(function(){
$('#merge-pull-request').hide(); $('#merge-pull-request').hide();
$('#confirm-merge-form').show(); $('#confirm-merge-form').show();
}); });
$('#cancel-merge-pull-request').click(function(){
$('#merge-pull-request').show();
$('#confirm-merge-form').hide();
});
@forkedRepository.sshUrl.map { sshUrl => @forkedRepository.sshUrl.map { sshUrl =>
$('#repository-url-http').click(function(e){ $('#repository-url-http').click(function(e){

View File

@@ -28,7 +28,7 @@
<div class="edit-title pull-right" style="display: none;"> <div class="edit-title pull-right" style="display: none;">
<a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a> <a class="btn btn-success" href="#" id="update">Save</a> <a class="btn btn-default" href="#" id="cancel">Cancel</a>
</div> </div>
<h1> <h1 class="body-title">
<span class="show-title"> <span class="show-title">
<span id="show-title">@issue.title</span> <span id="show-title">@issue.title</span>
<span class="muted">#@issue.issueId</span> <span class="muted">#@issue.issueId</span>

Some files were not shown because too many files have changed in this diff Show More