Compare commits

...

221 Commits
4.0 ... 4.5

Author SHA1 Message Date
Naoki Takezoe
c65599d995 Update README.md 2016-09-29 10:40:21 +09:00
Naoki Takezoe
22ae1df4b1 Update README.md 2016-09-29 10:30:34 +09:00
Naoki Takezoe
6b22342166 Merge pull request #1299 from gitbucket/release/gitbucket-4.5
GitBucket 4.5 release
2016-09-29 10:29:07 +09:00
Naoki Takezoe
53f6190267 Scalaz's <| is deprecated 2016-09-28 14:05:40 +09:00
Naoki Takezoe
f73daaef44 (refs #954)Cut commit id in Markdown with 7 letters 2016-09-28 13:28:50 +09:00
Naoki Takezoe
d99e382dfe (refs #1206)Display commit count on the history button 2016-09-28 10:11:55 +09:00
Naoki Takezoe
aefbee2093 (refs #1206)Display find and history icon in mobile view 2016-09-28 09:58:32 +09:00
Naoki Takezoe
11fb0a7edf (refs #1214)Gravater is disable in default 2016-09-28 09:33:16 +09:00
Naoki Takezoe
fe959aecff (refs #1298)Append raw=true only if the given url does not have it. 2016-09-25 17:42:50 +09:00
Naoki Takezoe
9b33655bd4 Bump to sbt 0.13.12. 2016-09-25 17:09:29 +09:00
Naoki Takezoe
33acad85db Merge pull request #1301 from conradlink/master
Fix host command line argument
2016-09-23 16:00:28 +09:00
conradlink
6bfe3ea760 Merge remote-tracking branch 'upstream/master' 2016-09-23 00:32:35 -04:00
Naoki Takezoe
1532fd71d0 Remove files for publishing jars to the maven repository because already it's possible by sbt. 2016-09-22 11:43:21 +09:00
Naoki Takezoe
c14a732e2a Update publishing jar operation 2016-09-22 11:41:26 +09:00
Naoki Takezoe
a1372034ed Ready for GitBucket 4.5 release 2016-09-22 11:37:37 +09:00
conradlink
98914269b7 Fix to make the --host argument work again. 2016-09-21 08:43:07 -04:00
Naoki Takezoe
d5e455336b (refs #1293)Restore dashboard issues / pull requests switcher 2016-09-20 16:41:20 +09:00
Naoki Takezoe
7b84f25c56 (refs #1297)Bugfix 2016-09-20 15:45:21 +09:00
Naoki Takezoe
2ca20af502 (refs #1297)Allow to configure HikariCP in database.conf 2016-09-19 23:13:52 +09:00
Naoki Takezoe
78df2accfc (refs #1290)Always show “Download ZIP” button 2016-09-18 21:14:00 +09:00
Naoki Takezoe
7a282fb67e (refs #1291)Add secure attribute to JSESSIONID cookie when baseUrl starts with "https://" 2016-09-12 15:06:59 +09:00
Naoki Takezoe
db679967af (refs #1291)Add http-only attribute to JSESSIONID cookie 2016-09-12 14:59:43 +09:00
Naoki Takezoe
9e98d30612 (refs #1288)Dropping files is also available in textarea, not only in the bottom bar. 2016-09-06 02:05:23 +09:00
Naoki Takezoe
a47065e4a9 (refs #1277)Don't make search filter and sorting sticky 2016-09-05 11:55:44 +09:00
Naoki Takezoe
94d18c471c Remove unnecessary style class 2016-09-04 02:59:06 +09:00
Naoki Takezoe
f8f3019228 Tweak branch list presentation 2016-09-04 02:54:54 +09:00
Naoki Takezoe
c3d90b8593 (refs #1239)Add toggle sidebar button 2016-09-04 02:12:56 +09:00
Naoki Takezoe
62c1299f29 (refs #1275, #1239)Fix CSS 2016-09-04 00:51:08 +09:00
Naoki Takezoe
b75db98cad (refs #1278) Bump to AdminLTE 2.3.6 2016-08-31 01:16:52 +09:00
Naoki Takezoe
3592b3d13c Merge pull request #1280 from kw-udon/fix-path-param-contents-api
fix get-contents API's format
2016-08-31 01:11:18 +09:00
Keiichi Watanabe
ca814e2c08 fix path parameter in get-contents API 2016-08-30 19:00:10 +09:00
Naoki Takezoe
48b6a590bf 4.4.0 release 2016-08-28 11:51:59 +09:00
Naoki Takezoe
285ef02a17 Merge pull request #1270 from S-YOU/patch-1
add copyright holder name in license
2016-08-28 11:10:42 +09:00
YOU
18375c741e Update LICENSE 2016-08-26 08:05:40 +00:00
Naoki Takezoe
21030344cc Merge pull request #1273 from kw-udon/fix-contents-api-default-ref-param
Set default value of parameter in Get-Contents API
2016-08-22 23:55:18 +09:00
Keiichi Watanabe
a494027217 Set default value to ref param in contents-API 2016-08-22 18:58:59 +09:00
Naoki Takezoe
7bca01af59 (refs #1230) Apply table-hover class for the commit history 2016-08-18 11:48:47 +09:00
YOU
acf3fa9980 add copyright holder name in license 2016-08-15 23:32:31 +09:00
Naoki Takezoe
c0ce0f8d19 (refs #1259) Add SQL import capability and remove XML export / import 2016-08-14 01:55:19 +09:00
Naoki Takezoe
56e7168461 Merge pull request #1268 from haru/FixgetGroupNames
Fix AccountService#getGroupNames returns duplicated group name .
2016-08-12 00:08:48 +09:00
Haruyuki Iida
c2d0d94f05 Fix AccountService#getGroupNames returns duplicated group name . 2016-08-11 15:28:29 +09:00
Naoki Takezoe
fc22cfbbdd Merge branch 'master' of https://github.com/gitbucket/gitbucket 2016-08-11 14:40:14 +09:00
Naoki Takezoe
d62adbf649 (refs #769) Output go-import meta tag in all repositories for go get 2016-08-11 14:40:06 +09:00
Naoki Takezoe
dba5539e3e (refs #1267) Graceful shutdown for the embedded jetty 2016-08-10 20:57:44 +09:00
Naoki Takezoe
f0a8b3bb17 (refs #1264) BugFix: File attachment does not work on the issue comment 2016-08-08 21:57:26 +09:00
Naoki Takezoe
f52e7e1bdd (refs #1255) Display the newest version as plugin version because if migration was failed, plugin is not registered. 2016-08-07 14:10:57 +09:00
Naoki Takezoe
58ba26f21e (refs #1255) Also check plugin version 2016-08-07 13:58:51 +09:00
Naoki Takezoe
bf7b30630c (refs #1255) Check whether database version is same as GitBucket version in startup 2016-08-07 13:42:42 +09:00
Naoki Takezoe
b5cac0308e Merge branch 'mcveat-217-sort-milestones' 2016-08-06 10:04:18 +09:00
Naoki Takezoe
373ea39048 (refs #219) Add milestoneId as a lowest priority sort column 2016-08-06 10:03:59 +09:00
Naoki Takezoe
427f5eec5f Merge branch '217-sort-milestones' of https://github.com/mcveat/gitbucket into mcveat-217-sort-milestones
# Conflicts:
#	src/main/scala/service/MilestonesService.scala
2016-08-06 09:56:56 +09:00
Naoki Takezoe
a4e9903e00 Merge pull request #1261 from UprootStaging/sshdUpdate
Sshd update
2016-08-04 15:51:03 +09:00
Naoki Takezoe
0d900a892c Merge pull request #1260 from team-lab/fix-blame-admin-lte
(fix #1246) cannot see the blame
2016-08-04 15:28:23 +09:00
hrj
dc6fdaf482 Fix for test compilation 2016-08-04 10:03:35 +05:30
hrj
b79498ed9f Update apache-sshd to latest version 2016-08-04 09:54:07 +05:30
nazoking
69e8f628df (fix #1246) cannot see the blame 2016-08-04 12:46:02 +09:00
Naoki Takezoe
d3d8e3ce5f Merge pull request #1256 from team-lab/git-is-reserved-user-name
(refs #1251) git is reserved user name. add validation.
2016-08-03 15:42:13 +09:00
nazoking
0499c47f4b (refs #1251, #1256) add admin, upload and api to reserved. 2016-08-03 12:14:32 +09:00
nazoking
7fd0cdd7d8 (refs #1251) git is reserved user name 2016-08-02 18:00:40 +09:00
Naoki Takezoe
49eaf79e01 Merge pull request #1253 from gitbucket/publish-maven-central
(refs #935) Update project configuration for deploying artifacts to Maven central
2016-07-31 21:45:04 +09:00
Naoki Takezoe
3a96c30aa8 (refs #935) Reoveride artifact to remove war from published artifacts 2016-07-31 21:21:27 +09:00
Naoki Takezoe
6d550fa485 (refs #935) Revert version to 4.3.0 2016-07-31 20:49:16 +09:00
Naoki Takezoe
7f9d69bb51 (refs #935) Update project configuration for deploying artifacts to Maven central 2016-07-31 18:08:47 +09:00
Naoki Takezoe
709fab9ccc GitBucket 4.3 release 2016-07-30 10:34:40 +09:00
Naoki Takezoe
fd13a2db79 Update README.md for 4.3 release 2016-07-30 10:21:01 +09:00
Naoki Takezoe
840d81f7bd (refs #1250) Bump markedj 2016-07-30 10:14:05 +09:00
Naoki Takezoe
5d08f4d339 Merge pull request #1249 from shiena/patch/fix-git-repo-path
fix: can't resolve the git repository path provided by the plugin
2016-07-29 01:26:31 +09:00
Naoki Takezoe
ef48b2d5ef (refs #1248) Move splitPath() to RepositoryInfo 2016-07-28 17:37:35 +09:00
Naoki Takezoe
f54e4f337f Merge pull request #1248 from kounoike/PR-api-for-ghbs
add some API. required by Jenkins GitHub Branch Source plugin
2016-07-28 17:28:53 +09:00
Naoki Takezoe
743965d3b8 (refs #1247) cleanup 2016-07-27 02:36:30 +09:00
Naoki Takezoe
0e787eddfd Merge pull request #1247 from kounoike/PR-api-basicauth
Add Basic Authentication support for API access
2016-07-27 02:32:20 +09:00
Mitsuhiro Koga
442c0d575e Modify contextPath to the literal pattern
Because contextPath can contain some special chars.
2016-07-26 20:50:40 +09:00
Mitsuhiro Koga
485516be2e fix: can't resolve the git repository path provided by the plugin
If the contextPath is not equals to `/`, gitRepositoryPath contains
the contextPath + `/git`.  Therefore, gitbucket can not resolve
the git repository path provided by the plugin.

For example, you can not clone the snippets repository of gist-plugin.

To fix this, also deletes contextPath from requestURI.
2016-07-26 01:01:57 +09:00
KOUNOIKE Yuusuke
6b2fbb3bf0 add some API. required by Jenkins GitHub Branch Source plugin 2016-07-25 23:52:08 +09:00
KOUNOIKE Yuusuke
e510b1c26b Add Basic Authentication support for API access 2016-07-25 23:43:35 +09:00
Naoki Takezoe
8d35494169 Fix message of plugin version 2016-07-20 11:45:16 +09:00
Naoki Takezoe
cf1504bae7 (refs #1245) Display migrated plugin version if migration is failing 2016-07-20 10:58:26 +09:00
Naoki Takezoe
9bb4e473b9 Merge pull request #1236 from mrkm4ntr/fix-plugin-versions
Fix plugin versions in installed plugins page
2016-07-19 09:27:38 +09:00
Naoki Takezoe
d67afebadc Rename CompletionProposalProvider to SuggestionProvider 2016-07-16 12:29:42 +09:00
Naoki Takezoe
417886161c (refs #1242) Bugfix in branch protection for branches which contain / 2016-07-16 11:00:29 +09:00
Naoki Takezoe
1b85d511e9 Purge Emoji support because it will be provided as plugin 2016-07-14 10:35:16 +09:00
Naoki Takezoe
45d84f63c1 Merge remote-tracking branch 'origin/master' 2016-07-14 02:21:42 +09:00
Naoki Takezoe
fff60b2704 Remove head / from resource path 2016-07-14 02:21:27 +09:00
Naoki Takezoe
c9339aec9e Fix error in creating and merging pull request 2016-07-13 21:06:38 +09:00
Naoki Takezoe
7c98ae1341 (refs #1241) Update CompletionProposalProvider interface 2016-07-13 02:33:58 +09:00
Naoki Takezoe
01c2291715 Fix testcase 2016-07-13 02:14:44 +09:00
Naoki Takezoe
2e03f081d9 (refs #1241) Filter CompletionProposalProvider by the completion context 2016-07-13 01:56:43 +09:00
Naoki Takezoe
0cbafdd884 Update TextDecorator interface 2016-07-13 01:43:35 +09:00
Naoki Takezoe
d5a9c2c15d (refs #1241) Add new extension point to add completion proposals provider for the textarea 2016-07-12 19:38:42 +09:00
Naoki Takezoe
1496591244 (refs #1240) Add new extension point to add text decorators 2016-07-12 15:21:40 +09:00
Naoki Takezoe
f5acce3901 Decorate only text node 2016-07-12 01:25:02 +09:00
Shintaro Murakami
5568a0ad8e Fix plugin versions in installed plugins page 2016-07-11 20:46:48 +09:00
Naoki Takezoe
26a18287c7 (refs #1238) Add new extension point to supply assets by plugin 2016-07-11 18:14:43 +09:00
Naoki Takezoe
b0f819b9bd Remove unused import statements 2016-07-11 13:53:07 +09:00
Naoki Takezoe
ebff7baf07 Fix WebHook message garbling:
76079aa1e8
2016-07-11 13:31:06 +09:00
Naoki Takezoe
cf9a55d896 (refs #1237) Fix broken layout 2016-07-10 22:55:34 +09:00
Naoki Takezoe
72f7b659f4 Remove unused import statements 2016-07-10 12:15:17 +09:00
Naoki Takezoe
87192d025b Remove Jsoup dependency 2016-07-10 11:35:03 +09:00
Naoki Takezoe
fd181b9a0c Fix emoji conversion 2016-07-10 02:03:25 +09:00
Naoki Takezoe
9c4cc12a02 Change import to resolve resolving error in IntelliJ 2016-07-09 18:09:54 +09:00
Naoki Takezoe
44497b559e Change import to resolve resolving error in IntelliJ 2016-07-09 18:09:31 +09:00
Naoki Takezoe
09c50a149b Change import to resolve resolving error in IntelliJ 2016-07-09 16:07:32 +09:00
Naoki Takezoe
88beb68e01 Change import to resolve resolving error in IntelliJ 2016-07-09 15:27:04 +09:00
Naoki Takezoe
0da358311b Change import to resolve resolving error in IntelliJ 2016-07-09 14:50:17 +09:00
Naoki Takezoe
cf97b63dab Change import to resolve resolving error in IntelliJ 2016-07-09 14:22:23 +09:00
Naoki Takezoe
4b5f22144e Change import to resolve resolving error in IntelliJ 2016-07-09 14:22:06 +09:00
Naoki Takezoe
0d342a6863 Use completion is disabled in the Wiki editor 2016-07-09 13:46:13 +09:00
Naoki Takezoe
458820a09d Add user name completion in the textarea 2016-07-09 11:47:41 +09:00
Naoki Takezoe
135c34ef0f Remove extension point to add text decorators. We need more consideration. 2016-07-09 11:47:22 +09:00
Naoki Takezoe
8187c5a013 Add new extension point to add TextDecorator. 2016-07-08 22:50:01 +09:00
Naoki Takezoe
6ff48c8130 Clean up 2016-07-08 19:58:22 +09:00
Naoki Takezoe
d37c70cd8d Emoji completion in textarea 2016-07-08 19:51:28 +09:00
Naoki Takezoe
8abf357405 Convert emoji in commit message 2016-07-07 19:53:23 +09:00
Naoki Takezoe
c93ac71634 Merge branch 'rlazoti-emoji-support' 2016-07-07 19:46:30 +09:00
Naoki Takezoe
408180f071 Change EmojiConverter to EmojiUtil 2016-07-07 19:39:30 +09:00
Naoki Takezoe
4e98abfe5c Remove unnecessary self typing 2016-07-07 19:30:47 +09:00
Naoki Takezoe
efbb404bd4 Fix file attachement area in Wiki page editing form 2016-07-05 17:48:53 +09:00
Naoki Takezoe
66f409bfad Merge branch 'emoji-support' of https://github.com/rlazoti/gitbucket into rlazoti-emoji-support
# Conflicts:
#	src/main/scala/view/Markdown.scala
2016-07-05 17:21:10 +09:00
Naoki Takezoe
44ec64fb4b Merge pull request #1232 from shiena/patch/fix-user-profile
fix: can't show the user profile when joining any groups
2016-07-05 01:57:24 +09:00
Mitsuhiro Koga
fb27bd29e8 Add a missing ul tag 2016-07-05 01:47:20 +09:00
Mitsuhiro Koga
c26ca9d463 fix: can't show the user profile when joining any groups 2016-07-04 21:01:47 +09:00
Naoki Takezoe
8c36ba33f4 Update README.md 2016-07-04 08:39:50 +09:00
Naoki Takezoe
fe1e18b495 Bugfix for new installation 2016-07-04 08:33:02 +09:00
Naoki Takezoe
29e632af04 Update README.md 2016-07-03 01:10:17 +09:00
Naoki Takezoe
2b9daae62b Update README.md 2016-07-03 00:52:20 +09:00
Naoki Takezoe
8a11f85dd1 4.2.1 release 2016-07-03 00:46:54 +09:00
Naoki Takezoe
b09c72b106 (refs #1227)Fix migration from 3.14 to 4.0.0 2016-07-03 00:39:38 +09:00
Naoki Takezoe
43456e817a Fix badge position in the sidebar 2016-07-02 21:13:14 +09:00
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
Rodrigo Lazoti
41e49423b2 Add emoji support for markdown 2015-01-21 22:07:14 -02:00
Piotr Adamski
3cc7bd3cdb #217 open milestones sorted by due date ascending, closed milestones sorted by close date descending 2013-12-01 14:09:25 +01:00
266 changed files with 18687 additions and 3631 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

@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright [yyyy] [name of copyright owner] Copyright 2013-2016 GitBucket Team
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -49,6 +49,8 @@ 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)
- [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-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,15 +64,63 @@ 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.0 - 30 Apr 2016 ### 4.5 - 29 Sep 2016
- Attach files by dropping into textarea
- Issues / Pull requests switcher in dashboard
- HikariCP could be configured in `GITBUCKET_HOME/database.conf`
- Improve Cookie security
- Display commit count on the history button
- Improve mobile view
### 4.4 - 28 Aug 2016
- Import a SQL dump file to the database
- `go get` support in private repositories
- Sort milestones by due date
- apache-sshd has been updated to 1.2.0
### 4.3 - 30 Jul 2016
- Emoji support by [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
- User name suggestion
- Add new web APIs and basic authentication support for API access
- Root Endpoint
- [List endpoints](https://developer.github.com/v3/#root-endpoint)
- [List Branches](https://developer.github.com/v3/repos/branches/#list-branches)
- [Get contents](https://developer.github.com/v3/repos/contents/#get-contents)
- [Get a Reference](https://developer.github.com/v3/git/refs/#get-a-reference)
- [List Collaborators](https://developer.github.com/v3/repos/collaborators/#list-collaborators)
- [List user repositories](https://developer.github.com/v3/repos/#list-user-repositories)
- [Get a group](https://developer.github.com/v3/orgs/#get-an-organization)
- [List group repositories](https://developer.github.com/v3/repos/#list-organization-repositories)
- Add new extension points
- `assetsMapping` : Supplies resources in plugin classpath as web assets
- `suggestionProvider` : Provides suggestion in the Markdown editing textarea
- `textDecorator` : Decorate text nodes in HTML which is converted from Markdown
### 4.2.1 - 3 Jul 2016
- Fix migration bug
This is hotfix for a critical bug in migration. If you are new installation, use 4.2.0. But if you have an exisiting installation and it had been updated to 4.0 from 3.x, you must update to 4.2.1.
### 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
- 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)
### 3.14 - 30 Apr 2016 **Note:** You can upgrade to GitBucket 4.0 from 3.14. If your GitBucket is 3.13 or before, you have to upgrade 3.14 at first.
### 3.14 - 30 Apr 2016
- File attachment and search for wiki pages - File attachment and search for wiki pages
- New extension points to add menus - New extension points to add menus
- Content-Type of webhooks has been choosable - Content-Type of webhooks has been choosable

View File

@@ -1,8 +1,8 @@
val Organization = "gitbucket" val Organization = "io.github.gitbucket"
val Name = "gitbucket" val Name = "gitbucket"
val GitBucketVersion = "4.0.0" val GitBucketVersion = "4.5.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)
@@ -21,49 +21,52 @@ resolvers ++= Seq(
) )
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" % "solidbase" % "1.0.0", "io.github.gitbucket" % "solidbase" % "1.0.0",
"io.github.gitbucket" % "markedj" % "1.0.8", "io.github.gitbucket" % "markedj" % "1.0.9",
"org.apache.commons" % "commons-compress" % "1.10", "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.2.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",
"mysql" % "mysql-connector-java" % "5.1.38", "mysql" % "mysql-connector-java" % "5.1.39",
"org.postgresql" % "postgresql" % "9.4.1208", "org.postgresql" % "postgresql" % "9.4.1208",
"ch.qos.logback" % "logback-classic" % "1.1.1", "ch.qos.logback" % "logback-classic" % "1.1.7",
"com.zaxxer" % "HikariCP" % "2.4.5", "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
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
// Compiler settings // Compiler settings
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
@@ -78,12 +81,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
@@ -166,3 +170,56 @@ Keys.artifact in (Compile, executableKey) ~= {
} }
addArtifact(Keys.artifact in (Compile, executableKey), executableKey) addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/ */
publishTo <<= version { (v: String) =>
val nexus = "https://oss.sonatype.org/"
if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots")
else Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
publishMavenStyle := true
pomIncludeRepository := { _ => false }
artifact in Keys.`package` := Artifact(moduleName.value)
pomExtra := (
<url>https://github.com/gitbucket/gitbucket</url>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<scm>
<url>https://github.com/gitbucket/gitbucket</url>
<connection>scm:git:https://github.com/gitbucket/gitbucket.git</connection>
</scm>
<developers>
<developer>
<id>takezoe</id>
<name>Naoki Takezoe</name>
<url>https://github.com/takezoe</url>
</developer>
<developer>
<id>shimamoto</id>
<name>Takako Shimamoto</name>
<url>https://github.com/shimamoto</url>
</developer>
<developer>
<id>tanacasino</id>
<name>Tomofumi Tanaka</name>
<url>https://github.com/tanacasino</url>
</developer>
<developer>
<id>mrkm4ntr</id>
<name>Shintaro Murakami</name>
<url>https://github.com/mrkm4ntr</url>
</developer>
<developer>
<id>nazoking</id>
<name>nazoking</name>
<url>https://github.com/nazoking</url>
</developer>
<developer>
<id>McFoggy</id>
<name>Matthieu Brouillard</name>
<url>https://github.com/McFoggy</url>
</developer>
</developers>
)

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

@@ -20,13 +20,13 @@ val JettyVersion = "9.3.6.v20151106"
```scala ```scala
object GitBucketCoreModule extends Module("gitbucket-core", object GitBucketCoreModule extends Module("gitbucket-core",
// add new version definition
new Version("4.1.0",
new LiquibaseMigration("update/gitbucket-core_4.1.xml")
),
new Version("4.0.0", new Version("4.0.0",
new LiquibaseMigration("update/gitbucket-core_4.0.xml"), new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
new SqlMigration("update/gitbucket-core_4.0.sql") new SqlMigration("update/gitbucket-core_4.0.sql")
),
// add new version definition
new Version("4.1.0",
new LiquibaseMigration("update/gitbucket-core_4.1.xml")
) )
) )
``` ```
@@ -41,14 +41,15 @@ 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
For plug-in development, we have to publish the assembly jar file to the public Maven repository by `release/deploy-assembly-jar.sh`. For plug-in development, we have to publish the GitBucket jar file to the Maven central repository as well. At first, hit following command to publish artifacts to the sonatype OSS repository:
```bash ```bash
$ cd release/ $ sbt publish-signed
$ ./deploy-assembly-jar.sh
``` ```
Then operate release sequence at https://oss.sonatype.org/.

View File

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

View File

@@ -1,6 +1,7 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature") scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4") addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0") addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0") addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0") addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")

View File

@@ -1,24 +0,0 @@
#!/bin/sh
. ./env.sh
cd ../
./sbt.sh clean assembly
cd release
if [[ "$GITBUCKET_VERSION" =~ -SNAPSHOT$ ]]; then
MVN_DEPLOY_PATH=mvn-snapshot
else
MVN_DEPLOY_PATH=mvn
fi
echo $MVN_DEPLOY_PATH
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
-Dversion=$GITBUCKET_VERSION\
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/$MVN_DEPLOY_PATH/

View File

@@ -1,3 +0,0 @@
#!/bin/sh
export GITBUCKET_VERSION=`cat ../build.sbt | grep 'val GitBucketVersion' | cut -d \" -f 2`
echo "GITBUCKET_VERSION: $GITBUCKET_VERSION"

View File

@@ -1,17 +0,0 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jp.sf.amateras</groupId>
<artifactId>gitbucket-assembly</artifactId>
<version>0.0.1</version>
<build>
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>2.10</version>
</extension>
</extensions>
</build>
</project>

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.12.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.12.jar "$@"

View File

@@ -3,12 +3,14 @@ import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File; import java.io.File;
import java.net.URL; import java.net.URL;
import java.net.InetSocketAddress;
import java.security.ProtectionDomain; import java.security.ProtectionDomain;
public class JettyLauncher { public class JettyLauncher {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
String host = null; String host = null;
int port = 8080; int port = 8080;
InetSocketAddress address = null;
String contextPath = "/"; String contextPath = "/";
boolean forceHttps = false; boolean forceHttps = false;
@@ -29,7 +31,13 @@ public class JettyLauncher {
} }
} }
Server server = new Server(port); if(host != null) {
address = new InetSocketAddress(host, port);
} else {
address = new InetSocketAddress(port);
}
Server server = new Server(address);
// SelectChannelConnector connector = new SelectChannelConnector(); // SelectChannelConnector connector = new SelectChannelConnector();
// if(host != null) { // if(host != null) {
@@ -43,10 +51,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();
@@ -61,6 +68,8 @@ public class JettyLauncher {
} }
server.setHandler(context); server.setHandler(context);
server.setStopAtShutdown(true);
server.setStopTimeout(7_000);
server.start(); server.start();
server.join(); server.join();
} }

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

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

@@ -1,24 +1,30 @@
import gitbucket.core.controller._ import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter} import gitbucket.core.servlet.{ApiAuthenticationFilter, Database, GitAuthenticationFilter, TransactionFilter}
import gitbucket.core.util.Directory import gitbucket.core.util.Directory
import java.util.EnumSet import java.util.EnumSet
import javax.servlet._ import javax.servlet._
import gitbucket.core.service.SystemSettingsService
import org.scalatra._ import org.scalatra._
class ScalatraBootstrap extends LifeCycle { class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
override def init(context: ServletContext) { override def init(context: ServletContext) {
val settings = loadSystemSettings()
if(settings.baseUrl.exists(_.startsWith("https://"))) {
context.getSessionCookieConfig.setSecure(true)
}
// Register TransactionFilter and BasicAuthenticationFilter at first // Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter) context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.addFilter("gitAuthenticationFilter", new GitAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("accessTokenAuthenticationFilter", new AccessTokenAuthenticationFilter) context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context.getFilterRegistration("accessTokenAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*") context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
// Register controllers // Register controllers
context.mount(new AnonymousAccessController, "/*") context.mount(new AnonymousAccessController, "/*")

View File

@@ -7,5 +7,13 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.0.0", new Version("4.0.0",
new LiquibaseMigration("update/gitbucket-core_4.0.xml"), new LiquibaseMigration("update/gitbucket-core_4.0.xml"),
new SqlMigration("update/gitbucket-core_4.0.sql") 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")
),
new Version("4.2.1"),
new Version("4.3.0"),
new Version("4.4.0"),
new Version("4.5.0")
) )

View File

@@ -14,3 +14,10 @@ case class ApiBranch(
"self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"), "self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"),
"html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}")) "html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}"))
} }
case class ApiBranchCommit(sha: String)
case class ApiBranchForList(
name: String,
commit: ApiBranchCommit
)

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

@@ -0,0 +1,11 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil.FileInfo
case class ApiContents(`type`: String, name: String)
object ApiContents{
def apply(fileInfo: FileInfo): ApiContents =
if(fileInfo.isDirectory) ApiContents("dir", fileInfo.name)
else ApiContents("file", fileInfo.name)
}

View File

@@ -0,0 +1,3 @@
package gitbucket.core.api
case class ApiEndPoint(rate_limit_url: ApiPath = ApiPath("/api/v3/rate_limit"))

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

@@ -0,0 +1,5 @@
package gitbucket.core.api
case class ApiObject(sha: String)
case class ApiRef(ref: String, `object`: ApiObject)

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

@@ -13,6 +13,7 @@ case class ApiUser(
created_at: Date) { created_at: Date) {
val url = ApiPath(s"/api/v3/users/${login}") val url = ApiPath(s"/api/v3/users/${login}")
val html_url = ApiPath(s"/${login}") val html_url = ApiPath(s"/${login}")
val avatar_url = ApiPath(s"/${login}/_avatar")
// val followers_url = ApiPath(s"/api/v3/users/${login}/followers") // val followers_url = ApiPath(s"/api/v3/users/${login}/followers")
// val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}") // val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}")
// val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}") // val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}")

View File

@@ -38,7 +38,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class PersonalTokenForm(note: String) case class PersonalTokenForm(note: String)
val newForm = mapping( val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password" , text(required, maxlength(20)))), "password" -> trim(label("Password" , text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" , text(required, maxlength(100)))), "fullName" -> trim(label("Full Name" , text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))), "mailAddress" -> trim(label("Mail Address" , text(required, maxlength(100), uniqueMailAddress()))),
@@ -68,7 +68,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean) case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))) "members" -> trim(label("Members" ,text(required, members)))
@@ -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

@@ -7,7 +7,7 @@ import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._ import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil.CommitInfo import gitbucket.core.util.JGitUtil.{CommitInfo, getFileList, getBranches, getDefaultBranch}
import gitbucket.core.util._ import gitbucket.core.util._
import gitbucket.core.util.Implicits._ import gitbucket.core.util.Implicits._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
@@ -54,14 +54,91 @@ trait ApiControllerBase extends ControllerBase {
with CollaboratorsAuthenticator => with CollaboratorsAuthenticator =>
/** /**
* https://developer.github.com/v3/users/#get-a-single-user * https://developer.github.com/v3/#root-endpoint
*/ */
get("/api/v3/users/:userName") { get("/api/v3/") {
getAccountByUserName(params("userName")).map { account => JsonFormat(ApiEndPoint())
}
/**
* https://developer.github.com/v3/orgs/#get-an-organization
*/
get("/api/v3/orgs/:groupName") {
getAccountByUserName(params("groupName")).filter(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(account)) JsonFormat(ApiUser(account))
} getOrElse NotFound } getOrElse NotFound
} }
/**
* https://developer.github.com/v3/users/#get-a-single-user
*/
get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).filterNot(account => account.isGroupAccount).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
}
/**
* https://developer.github.com/v3/repos/#list-organization-repositories
*/
get("/api/v3/orgs/:orgName/repos") {
JsonFormat(getVisibleRepositories(context.loginAccount, Some(params("orgName"))).map{ r => ApiRepository(r, getAccountByUserName(r.owner).get)})
}
/**
* https://developer.github.com/v3/repos/#list-user-repositories
*/
get("/api/v3/users/:userName/repos") {
JsonFormat(getVisibleRepositories(context.loginAccount, Some(params("userName"))).map{ r => ApiRepository(r, getAccountByUserName(r.owner).get)})
}
/*
* https://developer.github.com/v3/repos/branches/#list-branches
*/
get ("/api/v3/repos/:owner/:repo/branches")(referrersOnly { repository =>
JsonFormat(JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
).map { br =>
ApiBranchForList(br.name, ApiBranchCommit(br.commitId))
})
})
/*
* https://developer.github.com/v3/repos/contents/#get-contents
*/
get("/api/v3/repos/:owner/:repo/contents/*")(referrersOnly { repository =>
val path = multiParams("splat").head match {
case s if s.isEmpty => "."
case s => s
}
val refStr = params.getOrElse("ref", repository.repository.defaultBranch)
using(Git.open(getRepositoryDir(params("owner"), params("repo")))){ git =>
JsonFormat(getFileList(git, refStr, path).map{f => ApiContents(f)})
}
})
/*
* https://developer.github.com/v3/git/refs/#get-a-reference
*/
get("/api/v3/repos/:owner/:repo/git/*") (referrersOnly { repository =>
val revstr = multiParams("splat").head
using(Git.open(getRepositoryDir(params("owner"), params("repo")))) { git =>
//JsonFormat( (revstr, git.getRepository().resolve(revstr)) )
// getRef is deprecated by jgit-4.2. use exactRef() or findRef()
val sha = git.getRepository().getRef(revstr).getObjectId().name()
JsonFormat(ApiRef(revstr, ApiObject(sha)))
}
})
/**
* https://developer.github.com/v3/repos/collaborators/#list-collaborators
*/
get("/api/v3/repos/:owner/:repo/collaborators") (referrersOnly { repository =>
JsonFormat(getCollaborators(params("owner"), params("repo")).map(u => ApiUser(getAccountByUserName(u).get)))
})
/** /**
* https://developer.github.com/v3/users/#get-the-authenticated-user * https://developer.github.com/v3/users/#get-the-authenticated-user
*/ */
@@ -71,6 +148,16 @@ trait ApiControllerBase extends ControllerBase {
} getOrElse Unauthorized } getOrElse Unauthorized
} }
/**
* List user's own repository
* https://developer.github.com/v3/repos/#list-your-repositories
*/
get("/api/v3/user/repos")(usersOnly{
JsonFormat(getVisibleRepositories(context.loginAccount, Option(context.loginAccount.get.userName)).map{
r => ApiRepository(r, getAccountByUserName(r.owner).get)
})
})
/** /**
* Create user repository * Create user repository
* https://developer.github.com/v3/repos/#create * https://developer.github.com/v3/repos/#create
@@ -119,7 +206,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 +350,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

@@ -244,4 +244,13 @@ trait AccountManagementControllerBase extends ControllerBase {
.map { _ => "Mail address is already registered." } .map { _ => "Mail address is already registered." }
} }
val allReservedNames = Set("git", "admin", "upload", "api")
protected def reservedNames(): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = if(allReservedNames.contains(value)){
Some(s"${value} is reserved")
}else{
None
}
}
} }

View File

@@ -15,20 +15,7 @@ trait DashboardControllerBase extends ControllerBase {
with UsersAuthenticator => with UsersAuthenticator =>
get("/dashboard/issues")(usersOnly { get("/dashboard/issues")(usersOnly {
val q = request.getParameter("q") searchIssues("created_by")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
case _ => searchIssues("created_by")
}
} getOrElse {
searchIssues("created_by")
}
}) })
get("/dashboard/issues/assigned")(usersOnly { get("/dashboard/issues/assigned")(usersOnly {
@@ -44,20 +31,7 @@ trait DashboardControllerBase extends ControllerBase {
}) })
get("/dashboard/pulls")(usersOnly { get("/dashboard/pulls")(usersOnly {
val q = request.getParameter("q") searchPullRequests("created_by")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
case _ => searchPullRequests("created_by")
}
} getOrElse {
searchPullRequests("created_by")
}
}) })
get("/dashboard/pulls/created_by")(usersOnly { get("/dashboard/pulls/created_by")(usersOnly {
@@ -73,19 +47,12 @@ trait DashboardControllerBase extends ControllerBase {
}) })
private def getOrCreateCondition(key: String, filter: String, userName: String) = { private def getOrCreateCondition(key: String, filter: String, userName: String) = {
val condition = session.putAndGet(key, if(request.hasQueryString){ val condition = IssueSearchCondition(request)
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, Map[String, Int]())
}
} 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 +70,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 +95,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

@@ -73,21 +73,16 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
fileName fileName
} }
} }
}, FileUtil.isImage) }, FileUtil.isUploadableType)
} }
} getOrElse BadRequest } getOrElse BadRequest
} }
post("/import") { post("/import") {
import JDBCUtil._
session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin => session.get(Keys.Session.LoginAccount).collect { case loginAccount: Account if loginAccount.isAdmin =>
execute({ (file, fileId) => execute({ (file, fileId) =>
if(file.getName.endsWith(".xml")){ request2Session(request).conn.importAsSQL(file.getInputStream)
import JDBCUtil._
val conn = request2Session(request).conn
conn.importAsXML(file.getInputStream)
} else {
throw new RuntimeException("Import is available for only the XML file.")
}
}, _ => true) }, _ => true)
} }
redirect("/admin/data") redirect("/admin/data")

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

@@ -204,8 +204,7 @@ trait IssuesControllerBase extends ControllerBase {
getIssue(repository.owner, repository.name, params("id")) map { x => getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => html.editissue( case t if t == "html" => html.editissue(x.content, x.issueId, repository)
x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
@@ -232,8 +231,7 @@ trait IssuesControllerBase extends ControllerBase {
getComment(repository.owner, repository.name, params("id")) map { x => getComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => html.editcomment( case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
@@ -365,16 +363,7 @@ trait IssuesControllerBase extends ControllerBase {
val sessionKey = Keys.Session.Issues(owner, repoName) val sessionKey = Keys.Session.Issues(owner, repoName)
// retrieve search condition // retrieve search condition
val condition = session.putAndGet(sessionKey, val condition = IssueSearchCondition(request)
if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null || q.trim.isEmpty){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
}
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
html.list( html.list(
"issues", "issues",

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 {
@@ -515,10 +520,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val sessionKey = Keys.Session.Pulls(owner, repoName) val sessionKey = Keys.Session.Pulls(owner, repoName)
// retrieve search condition // retrieve search condition
val condition = session.putAndGet(sessionKey, val condition = IssueSearchCondition(request)
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
gitbucket.core.issues.html.list( gitbucket.core.issues.html.list(
"pulls", "pulls",

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

@@ -117,15 +117,20 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/** /**
* Displays the file list of the repository root and the default branch. * Displays the file list of the repository root and the default branch.
*/ */
get("/:owner/:repository")(referrersOnly { get("/:owner/:repository") {
fileList(_) params.get("go-get") match {
}) case Some("1") => defining(request.paths){ paths =>
getRepository(paths(0), paths(1)).map(gitbucket.core.html.goget(_))getOrElse NotFound()
}
case _ => referrersOnly(fileList(_))
}
}
/** /**
* Displays the file list of the specified path and branch. * Displays the file list of the specified path and branch.
*/ */
get("/:owner/:repository/tree/*")(referrersOnly { repository => get("/:owner/:repository/tree/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head) val (id, path) = repository.splitPath(multiParams("splat").head)
if(path.isEmpty){ if(path.isEmpty){
fileList(repository, id) fileList(repository, id)
} else { } else {
@@ -137,7 +142,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays the commit list of the specified resource. * Displays the commit list of the specified resource.
*/ */
get("/:owner/:repository/commits/*")(referrersOnly { repository => get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head) val (branchName, path) = repository.splitPath(multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1) val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -153,7 +158,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/new/*")(collaboratorsOnly { repository => get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList, html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")), None, JGitUtil.ContentInfo("text", None, Some("UTF-8")),
@@ -161,7 +166,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository => get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName) val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
@@ -177,7 +182,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository => get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head) val (branch, path) = repository.splitPath(multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
@@ -235,7 +240,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) })
get("/:owner/:repository/raw/*")(referrersOnly { repository => get("/:owner/:repository/raw/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head) val (id, path) = repository.splitPath(multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).flatMap { objectId => getPathObjectId(git, path, revCommit).flatMap { objectId =>
@@ -253,7 +258,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays the file content of the specified branch or commit. * Displays the file content of the specified branch or commit.
*/ */
val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository => val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head) val (id, path) = repository.splitPath(multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean val raw = params.get("raw").getOrElse("false").toBoolean
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id)) val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
@@ -285,7 +290,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Blame data. * Blame data.
*/ */
ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository => ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head) val (id, path) = repository.splitPath(multiParams("splat").head)
contentType = formats("json") contentType = formats("json")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
@@ -376,8 +381,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getCommitComment(repository.owner, repository.name, params("id")) map { x => getCommitComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => html.editcomment( case t if t == "html" => html.editcomment(x.content, x.commentId, repository)
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
@@ -527,17 +531,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} }
}) })
private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} getOrElse path.split("/")(0)
(id, path.substring(id.length).stripPrefix("/"))
}
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension => private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
s"readme.${extension}" s"readme.${extension}"
} ++ Seq("readme.txt", "readme") } ++ Seq("readme.txt", "readme")
@@ -691,8 +684,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

@@ -3,8 +3,8 @@ package gitbucket.core.controller
import java.io.FileInputStream 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._
@@ -13,7 +13,7 @@ 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.{IOUtils, FileUtils} import org.apache.commons.io.{FileUtils, IOUtils}
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
class SystemSettingsController extends SystemSettingsControllerBase class SystemSettingsController extends SystemSettingsControllerBase
@@ -70,11 +70,20 @@ 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 DataExportForm(tableNames: List[String])
@@ -94,7 +103,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
val newUserForm = mapping( val newUserForm = mapping(
"userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName))), "userName" -> trim(label("Username" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"password" -> trim(label("Password" ,text(required, maxlength(20)))), "password" -> trim(label("Password" ,text(required, maxlength(20)))),
"fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))), "fullName" -> trim(label("Full Name" ,text(required, maxlength(100)))),
"mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))), "mailAddress" -> trim(label("Mail Address" ,text(required, maxlength(100), uniqueMailAddress()))),
@@ -116,7 +125,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( val newGroupForm = mapping(
"groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName))), "groupName" -> trim(label("Group name" ,text(required, maxlength(100), identifier, uniqueUserName, reservedNames))),
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"members" -> trim(label("Members" ,text(required, members))) "members" -> trim(label("Members" ,text(required, members)))
@@ -152,6 +161,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())
}) })
@@ -179,36 +200,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
}) })
@@ -279,12 +303,7 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
post("/admin/export")(adminOnly { post("/admin/export")(adminOnly {
import gitbucket.core.util.JDBCUtil._ import gitbucket.core.util.JDBCUtil._
val session = request2Session(request) val file = request2Session(request).conn.exportAsSQL(request.getParameterValues("tableNames").toSeq)
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" contentType = "application/octet-stream"
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName) response.setHeader("Content-Disposition", "attachment; filename=" + file.getName)

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

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

@@ -149,6 +149,36 @@ abstract class Plugin {
*/ */
def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil def dashboardTabs(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(Context) => Option[Link]] = Nil
/**
* Override to add assets mappings.
*/
val assetsMappings: Seq[(String, String)] = Nil
/**
* Override to add assets mappings.
*/
def assetsMappings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(String, String)] = Nil
/**
* Override to add text decorators.
*/
val textDecorators: Seq[TextDecorator] = Nil
/**
* Override to add text decorators.
*/
def textDecorators(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[TextDecorator] = Nil
/**
* Override to add suggestion provider.
*/
val suggestionProviders: Seq[SuggestionProvider] = Nil
/**
* Override to add suggestion provider.
*/
def suggestionProviders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[SuggestionProvider] = Nil
/** /**
* This method is invoked in initialization of plugin system. * This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry. * Register plugin functionality to PluginRegistry.
@@ -193,6 +223,15 @@ abstract class Plugin {
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab => (dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
registry.addDashboardTab(dashboardTab) registry.addDashboardTab(dashboardTab)
} }
(assetsMappings ++ assetsMappings(registry, context, settings)).foreach { assetMapping =>
registry.addAssetsMapping((assetMapping._1, assetMapping._2, getClass.getClassLoader))
}
(textDecorators ++ textDecorators(registry, context, settings)).foreach { textDecorator =>
registry.addTextDecorator(textDecorator)
}
(suggestionProviders ++ suggestionProviders(registry, context, settings)).foreach { suggestionProvider =>
registry.addSuggestionProvider(suggestionProvider)
}
} }
/** /**

View File

@@ -13,8 +13,8 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.DatabaseConfig import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._ import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module 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
@@ -42,10 +42,13 @@ class PluginRegistry {
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]] private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]] private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]] private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
private val assetsMappings = new ListBuffer[(String, String, ClassLoader)]
private val textDecorators = new ListBuffer[TextDecorator]
def addPlugin(pluginInfo: PluginInfo): Unit = { private val suggestionProviders = new ListBuffer[SuggestionProvider]
plugins += pluginInfo suggestionProviders += new UserNameSuggestionProvider()
}
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
def getPlugins(): List[PluginInfo] = plugins.toList def getPlugins(): List[PluginInfo] = plugins.toList
@@ -66,42 +69,26 @@ class PluginRegistry {
def getImage(id: String): String = images(id) def getImage(id: String): String = images(id)
def addController(path: String, controller: ControllerBase): Unit = { def addController(path: String, controller: ControllerBase): Unit = controllers += ((controller, path))
controllers += ((controller, path))
}
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0") @deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = { def addController(controller: ControllerBase, path: String): Unit = addController(path, controller)
addController(path, controller)
}
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
def addJavaScript(path: String, script: String): Unit = { def addJavaScript(path: String, script: String): Unit = javaScripts += ((path, script))
javaScripts += ((path, script))
}
def getJavaScript(currentPath: String): List[String] = { def getJavaScript(currentPath: String): List[String] = javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
}
def addRenderer(extension: String, renderer: Renderer): Unit = { def addRenderer(extension: String, renderer: Renderer): Unit = renderers += ((extension, renderer))
renderers += ((extension, renderer))
}
def getRenderer(extension: String): Renderer = { def getRenderer(extension: String): Renderer = renderers.get(extension).getOrElse(DefaultRenderer)
renderers.get(extension).getOrElse(DefaultRenderer)
}
def renderableExtensions: Seq[String] = renderers.keys.toSeq def renderableExtensions: Seq[String] = renderers.keys.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = { def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings += routing
repositoryRoutings += routing
}
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = { def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.toSeq
repositoryRoutings.toSeq
}
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = { def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find { PluginRegistry().getRepositoryRoutings().find {
@@ -111,54 +98,49 @@ class PluginRegistry {
} }
} }
def addReceiveHook(commitHook: ReceiveHook): Unit = { def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks += commitHook
receiveHooks += commitHook
}
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = { def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus += globalMenu
globalMenus += globalMenu
}
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = { def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus += repositoryMenu
repositoryMenus += repositoryMenu
}
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = { def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs += repositorySettingTab
repositorySettingTabs += repositorySettingTab
}
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = { def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs += profileTab
profileTabs += profileTab
}
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = { def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus += systemSettingMenu
systemSettingMenus += systemSettingMenu
}
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = { def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus += accountSettingMenu
accountSettingMenus += accountSettingMenu
}
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = { def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs += dashboardTab
dashboardTabs += dashboardTab
}
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings += assetsMapping
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.toSeq
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators += textDecorator
def getTextDecorators: Seq[TextDecorator] = textDecorators.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders += suggestionProvider
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.toSeq
} }
/** /**
@@ -180,6 +162,8 @@ object PluginRegistry {
*/ */
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = { def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome) val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
if(pluginDir.exists && pluginDir.isDirectory){ if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter { pluginDir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar") override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
@@ -192,19 +176,26 @@ object PluginRegistry {
val solidbase = new Solidbase() val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*)) solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Check version
val databaseVersion = manager.getCurrentVersion(plugin.pluginId)
val pluginVersion = plugin.versions.last.getVersion
if(databaseVersion != pluginVersion){
throw new IllegalStateException(s"Plugin version is ${pluginVersion}, but database version is ${databaseVersion}")
}
// 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.getVersion, pluginVersion = plugin.versions.last.getVersion,
description = plugin.description, description = plugin.description,
pluginClass = plugin pluginClass = plugin
)) ))
} catch { } catch {
case e: Throwable => { case e: Throwable => {
logger.error(s"Error during plugin initialization", e) logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
} }
} }
} }
@@ -231,7 +222,7 @@ case class Link(id: String, label: String, path: String, icon: Option[String] =
case class PluginInfo( case class PluginInfo(
pluginId: String, pluginId: String,
pluginName: String, pluginName: String,
version: String, pluginVersion: String,
description: String, description: String,
pluginClass: Plugin pluginClass: Plugin
) )

View File

@@ -0,0 +1,27 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo
trait SuggestionProvider {
val id: String
val prefix: String
val suffix: String = " "
val context: Seq[String]
def values(repository: RepositoryInfo): Seq[String]
def template(implicit context: Context): String = "value"
def additionalScript(implicit context: Context): String = ""
}
class UserNameSuggestionProvider extends SuggestionProvider {
override val id: String = "user"
override val prefix: String = "@"
override val context: Seq[String] = Seq("issues")
override def values(repository: RepositoryInfo): Seq[String] = Nil
override def template(implicit context: Context): String = "'@' + value"
override def additionalScript(implicit context: Context): String =
s"""$$.get('${context.path}/_user/proposals', { query: '' }, function (data) { user = data.options; });"""
}

View File

@@ -0,0 +1,10 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService.RepositoryInfo
trait TextDecorator {
def decorate(text: String, repository: RepositoryInfo)(implicit context: Context): String
}

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(
@@ -180,7 +186,7 @@ trait AccountService {
def getGroupNames(userName: String)(implicit s: Session): List[String] = { def getGroupNames(userName: String)(implicit s: Session): List[String] = {
List(userName) ++ List(userName) ++
Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list.distinct
} }
} }

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
@@ -234,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
@@ -244,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)) &&
@@ -447,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",
@@ -491,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)),
@@ -514,46 +518,6 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
} }
/**
* Restores IssueSearchCondition instance from filter query.
*/
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
val conditions = filter.split("[  \t]+").flatMap { x =>
x.split(":") match {
case Array(key, value) => Some((key, value))
case _ => None
}
}.groupBy(_._1).map { case (key, values) =>
key -> values.map(_._2).toSeq
}
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
case "created-asc" => ("created" , "asc" )
case "comments-desc" => ("comments", "desc")
case "comments-asc" => ("comments", "asc" )
case "updated-desc" => ("comments", "desc")
case "updated-asc" => ("comments", "asc" )
case _ => ("created" , "desc")
}
IssueSearchCondition(
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
conditions.get("milestone").flatMap(_.headOption) match {
case None => None
case Some("none") => Some(None)
case Some(x) => Some(Some(x)) //milestones.get(x).map(x => Some(x))
},
conditions.get("author").flatMap(_.headOption),
conditions.get("assignee").flatMap(_.headOption),
conditions.get("mentions").flatMap(_.headOption),
conditions.get("is").getOrElse(Seq.empty).find(x => x == "open" || x == "closed").getOrElse("open"),
sort,
direction,
conditions.get("visibility").flatMap(_.headOption),
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
)
}
/** /**
* Restores IssueSearchCondition instance from request parameters. * Restores IssueSearchCondition instance from request parameters.
*/ */
@@ -565,7 +529,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

@@ -41,7 +41,7 @@ trait MilestonesService {
def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = { def getMilestonesWithIssueCount(owner: String, repository: String)(implicit s: Session): List[(Milestone, Int, Int)] = {
val counts = Issues val counts = Issues
.filter { t => (t.byRepository(owner, repository)) && (t.milestoneId.? isDefined) } .filter { t => t.byRepository(owner, repository) && (t.milestoneId.? isDefined) }
.groupBy { t => t.milestoneId -> t.closed } .groupBy { t => t.milestoneId -> t.closed }
.map { case (t1, t2) => t1._1 -> t1._2 -> t2.length } .map { case (t1, t2) => t1._1 -> t1._2 -> t2.length }
.toMap .toMap
@@ -52,6 +52,6 @@ trait MilestonesService {
} }
def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] = def getMilestones(owner: String, repository: String)(implicit s: Session): List[Milestone] =
Milestones.filter(_.byRepository(owner, repository)).sortBy(_.milestoneId asc).list Milestones.filter(_.byRepository(owner, repository)).sortBy(t => (t.dueDate.asc, t.closedDate.desc, t.milestoneId.desc)).list
} }

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 =
@@ -412,14 +420,23 @@ object RepositoryService {
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name) def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name) def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
def splitPath(path: String): (String, String) = {
val id = branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} getOrElse path.split("/")(0)
(id, path.substring(id.length).stripPrefix("/"))
}
} }
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

@@ -73,7 +73,7 @@ trait SystemSettingsService {
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true), getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true), getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true), getValue(props, Gravatar, false),
getValue(props, Notification, false), getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None), getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false), getValue(props, Ssh, false),
@@ -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

@@ -1,6 +1,5 @@
package gitbucket.core.service package gitbucket.core.service
import java.io.ByteArrayInputStream
import fr.brouillard.oss.security.xhub.XHub import fr.brouillard.oss.security.xhub.XHub
import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter} import fr.brouillard.oss.security.xhub.XHub.{XHubDigest, XHubConverter}
import gitbucket.core.api._ import gitbucket.core.api._
@@ -22,6 +21,7 @@ import org.apache.http.HttpRequest
import org.apache.http.HttpResponse import org.apache.http.HttpResponse
import gitbucket.core.model.WebHookContentType import gitbucket.core.model.WebHookContentType
import org.apache.http.client.entity.EntityBuilder import org.apache.http.client.entity.EntityBuilder
import org.apache.http.entity.ContentType
trait WebHookService { trait WebHookService {
@@ -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}")
@@ -118,8 +118,8 @@ trait WebHookService {
} }
} }
case WebHookContentType.JSON => { case WebHookContentType.JSON => {
httpPost.setEntity(EntityBuilder.create().setText(json).build()) httpPost.setEntity(EntityBuilder.create().setContentType(ContentType.APPLICATION_JSON).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

@@ -4,15 +4,14 @@ import javax.servlet._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.model.Account import gitbucket.core.model.Account
import gitbucket.core.service.AccessTokenService import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.Keys import gitbucket.core.service.{AccessTokenService, AccountService, SystemSettingsService}
import gitbucket.core.util.{AuthUtil, Keys}
import org.scalatra.servlet.ServletApiImplicits._ import org.scalatra.servlet.ServletApiImplicits._
import org.scalatra._ import org.scalatra._
class AccessTokenAuthenticationFilter extends Filter with AccessTokenService { class ApiAuthenticationFilter extends Filter with AccessTokenService with AccountService with SystemSettingsService {
private val tokenHeaderPrefix = "token "
override def init(filterConfig: FilterConfig): Unit = {} override def init(filterConfig: FilterConfig): Unit = {}
@@ -23,9 +22,9 @@ class AccessTokenAuthenticationFilter extends Filter with AccessTokenService {
implicit val session = req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session] implicit val session = req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
val response = res.asInstanceOf[HttpServletResponse] val response = res.asInstanceOf[HttpServletResponse]
Option(request.getHeader("Authorization")).map{ Option(request.getHeader("Authorization")).map{
case auth if auth.startsWith("token ") => AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(Unit) case auth if auth.startsWith("token ") => AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(())
// TODO Basic Authentication Support case auth if auth.startsWith("Basic ") => doBasicAuth(auth, loadSystemSettings(), request).toRight(())
case _ => Left(Unit) case _ => Left(())
}.orElse{ }.orElse{
Option(request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]).map(Right(_)) Option(request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]).map(Right(_))
} match { } match {
@@ -40,4 +39,10 @@ class AccessTokenAuthenticationFilter extends Filter with AccessTokenService {
} }
} }
} }
def doBasicAuth(auth: String, settings: SystemSettings, request: HttpServletRequest): Option[Account] = {
implicit val session = request.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
val Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
authenticate(settings, username, password)
}
} }

View File

@@ -5,16 +5,16 @@ import javax.servlet.http._
import gitbucket.core.plugin.{GitRepositoryFilter, GitRepositoryRouting, PluginRegistry} import gitbucket.core.plugin.{GitRepositoryFilter, GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.SystemSettingsService.SystemSettings import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService} import gitbucket.core.service.{RepositoryService, AccountService, SystemSettingsService}
import gitbucket.core.util.{Keys, Implicits} import gitbucket.core.util.{Keys, Implicits, AuthUtil}
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import Implicits._ import Implicits._
/** /**
* Provides BASIC Authentication for [[GitRepositoryServlet]]. * Provides BASIC Authentication for [[GitRepositoryServlet]].
*/ */
class BasicAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService { class GitAuthenticationFilter extends Filter with RepositoryService with AccountService with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[BasicAuthenticationFilter]) private val logger = LoggerFactory.getLogger(classOf[GitAuthenticationFilter])
def init(config: FilterConfig) = {} def init(config: FilterConfig) = {}
@@ -43,7 +43,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} catch { } catch {
case ex: Exception => { case ex: Exception => {
logger.error("error", ex) logger.error("error", ex)
requireAuth(response) AuthUtil.requireAuth(response)
} }
} }
} }
@@ -54,7 +54,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
val account = for { val account = for {
auth <- Option(request.getHeader("Authorization")) auth <- Option(request.getHeader("Authorization"))
Array(username, password) = decodeAuthHeader(auth).split(":", 2) Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password) account <- authenticate(settings, username, password)
} yield { } yield {
request.setAttribute(Keys.Request.UserName, account.userName) request.setAttribute(Keys.Request.UserName, account.userName)
@@ -64,7 +64,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){ if(filter.filter(request.gitRepositoryPath, account.map(_.userName), settings, isUpdating)){
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
requireAuth(response) AuthUtil.requireAuth(response)
} }
} }
@@ -81,7 +81,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} else { } else {
val passed = for { val passed = for {
auth <- Option(request.getHeader("Authorization")) auth <- Option(request.getHeader("Authorization"))
Array(username, password) = decodeAuthHeader(auth).split(":", 2) Array(username, password) = AuthUtil.decodeAuthHeader(auth).split(":", 2)
account <- authenticate(settings, username, password) account <- authenticate(settings, username, password)
} yield if(isUpdating || repository.repository.isPrivate){ } yield if(isUpdating || repository.repository.isPrivate){
if(hasWritePermission(repository.owner, repository.name, Some(account))){ if(hasWritePermission(repository.owner, repository.name, Some(account))){
@@ -93,7 +93,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
if(passed.getOrElse(false)){ if(passed.getOrElse(false)){
chain.doFilter(request, response) chain.doFilter(request, response)
} else { } else {
requireAuth(response) AuthUtil.requireAuth(response)
} }
} }
} }
@@ -108,17 +108,4 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} }
} }
} }
private def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
}
private def decodeAuthHeader(header: String): String = {
try {
new String(new sun.misc.BASE64Decoder().decodeBuffer(header.substring(6)))
} catch {
case _: Throwable => ""
}
}
} }

View File

@@ -27,7 +27,7 @@ import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
* Provides Git repository via HTTP. * Provides Git repository via HTTP.
* *
* This servlet provides only Git repository functionality. * This servlet provides only Git repository functionality.
* Authentication is provided by [[BasicAuthenticationFilter]]. * Authentication is provided by [[GitAuthenticationFilter]].
*/ */
class GitRepositoryServlet extends GitServlet with SystemSettingsService { class GitRepositoryServlet extends GitServlet with SystemSettingsService {

View File

@@ -17,6 +17,7 @@ import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
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 scala.collection.JavaConverters._
/** /**
* Initialize GitBucket system. * Initialize GitBucket system.
@@ -35,6 +36,7 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
Database() withTransaction { session => Database() withTransaction { session =>
val conn = session.conn val conn = session.conn
val manager = new JDBCVersionManager(conn)
// Check version // Check version
val versionFile = new File(GitBucketHome, "version") val versionFile = new File(GitBucketHome, "version")
@@ -56,9 +58,8 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
} }
// Change form // Change form
val manager = new JDBCVersionManager(conn)
manager.initialize() manager.initialize()
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0") manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs => conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION")) manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
} }
@@ -77,6 +78,19 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
val solidbase = new Solidbase() val solidbase = new Solidbase()
solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule) solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
// Rescue code for users who updated from 3.14 to 4.0.0
// https://github.com/gitbucket/gitbucket/issues/1227
val currentVersion = manager.getCurrentVersion(GitBucketCoreModule.getModuleId)
val databaseVersion = if(currentVersion == "4.0"){
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
"4.0.0"
} else currentVersion
val gitbucketVersion = GitBucketCoreModule.getVersions.asScala.last.getVersion
if(databaseVersion != gitbucketVersion){
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
}
// Load plugins // Load plugins
logger.info("Initialize plugins") logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn) PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)

View File

@@ -0,0 +1,39 @@
package gitbucket.core.servlet
import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util.FileUtil
import org.apache.commons.io.IOUtils
/**
* Supply assets which are provided by plugins.
*/
class PluginAssetsServlet extends HttpServlet {
override def doGet(req: HttpServletRequest, resp: HttpServletResponse): Unit = {
val assetsMappings = PluginRegistry().getAssetsMappings
val path = req.getRequestURI.substring(req.getContextPath.length)
assetsMappings
.find { case (prefix, _, _) => path.startsWith("/plugin-assets" + prefix) }
.flatMap { case (prefix, resourcePath, classLoader) =>
val resourceName = path.substring(("/plugin-assets" + prefix).length)
Option(classLoader.getResourceAsStream(resourcePath.replaceFirst("^/", "") + resourceName))
}
.map { in =>
try {
val bytes = IOUtils.toByteArray(in)
resp.setContentLength(bytes.length)
resp.setContentType(FileUtil.getContentType(path, bytes))
resp.getOutputStream.write(bytes)
} finally {
in.close()
}
}
.getOrElse {
resp.setStatus(404)
}
}
}

View File

@@ -52,6 +52,13 @@ object Database {
config.setJdbcUrl(DatabaseConfig.url) config.setJdbcUrl(DatabaseConfig.url)
config.setUsername(DatabaseConfig.user) config.setUsername(DatabaseConfig.user)
config.setPassword(DatabaseConfig.password) config.setPassword(DatabaseConfig.password)
config.setAutoCommit(false)
DatabaseConfig.connectionTimeout.foreach(config.setConnectionTimeout)
DatabaseConfig.idleTimeout.foreach(config.setIdleTimeout)
DatabaseConfig.maxLifetime.foreach(config.setMaxLifetime)
DatabaseConfig.minimumIdle.foreach(config.setMinimumIdle)
DatabaseConfig.maximumPoolSize.foreach(config.setMaximumPoolSize)
logger.debug("load database connection pool") logger.debug("load database connection pool")
new HikariDataSource(config) new HikariDataSource(config)
} }

View File

@@ -5,14 +5,15 @@ 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._
import org.eclipse.jgit.api.Git import org.eclipse.jgit.api.Git
import Directory._ import Directory._
import org.eclipse.jgit.transport.{ReceivePack, UploadPack} import org.eclipse.jgit.transport.{ReceivePack, UploadPack}
import org.apache.sshd.server.command.UnknownCommand import org.apache.sshd.server.scp.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand { object GitCommand {
@@ -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.AttributeStore
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 AttributeStore.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

@@ -0,0 +1,21 @@
package gitbucket.core.util
import javax.servlet.http.HttpServletResponse
/**
* Provides HTTP (Basic) Authentication related functions.
*/
object AuthUtil {
def requireAuth(response: HttpServletResponse): Unit = {
response.setHeader("WWW-Authenticate", "BASIC realm=\"GitBucket\"")
response.sendError(HttpServletResponse.SC_UNAUTHORIZED)
}
def decodeAuthHeader(header: String): String = {
try {
new String(new sun.misc.BASE64Decoder().decodeBuffer(header.substring(6)))
} catch {
case _: Throwable => ""
}
}
}

View File

@@ -17,6 +17,11 @@ object DatabaseConfig {
| url = "jdbc:h2:${DatabaseHome};MVCC=true" | url = "jdbc:h2:${DatabaseHome};MVCC=true"
| user = "sa" | user = "sa"
| password = "sa" | password = "sa"
|# connectionTimeout = 30000
|# idleTimeout = 600000
|# maxLifetime = 1800000
|# minimumIdle = 10
|# maximumPoolSize = 10
|} |}
|""".stripMargin, "UTF-8") |""".stripMargin, "UTF-8")
} }
@@ -28,12 +33,21 @@ object DatabaseConfig {
def url(directory: Option[String]): String = def url(directory: Option[String]): String =
dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome)) dbUrl.replace("${DatabaseHome}", directory.getOrElse(DatabaseHome))
lazy val url: String = url(None) lazy val url : String = url(None)
lazy val user: String = config.getString("db.user") lazy val user : String = config.getString("db.user")
lazy val password: String = config.getString("db.password") lazy val password : String = config.getString("db.password")
lazy val jdbcDriver: String = DatabaseType(url).jdbcDriver lazy val jdbcDriver : String = DatabaseType(url).jdbcDriver
lazy val slickDriver: slick.driver.JdbcProfile = DatabaseType(url).slickDriver lazy val slickDriver : slick.driver.JdbcProfile = DatabaseType(url).slickDriver
lazy val liquiDriver: AbstractJdbcDatabase = DatabaseType(url).liquiDriver lazy val liquiDriver : AbstractJdbcDatabase = DatabaseType(url).liquiDriver
lazy val connectionTimeout : Option[Long] = getOptionValue("db.connectionTimeout", config.getLong)
lazy val idleTimeout : Option[Long] = getOptionValue("db.idleTimeout" , config.getLong)
lazy val maxLifetime : Option[Long] = getOptionValue("db.maxLifetime" , config.getLong)
lazy val minimumIdle : Option[Int] = getOptionValue("db.minimumIdle" , config.getInt)
lazy val maximumPoolSize : Option[Int] = getOptionValue("db.maximumPoolSize" , config.getInt)
private def getOptionValue[T](path: String, f: String => T): Option[T] = {
if(config.hasPath(path)) Some(f(path)) else None
}
} }

View File

@@ -4,6 +4,8 @@ import gitbucket.core.api.JsonFormat
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.servlet.Database import gitbucket.core.servlet.Database
import java.util.regex.Pattern.quote
import javax.servlet.http.{HttpSession, HttpServletRequest} import javax.servlet.http.{HttpSession, HttpServletRequest}
import scala.util.matching.Regex import scala.util.matching.Regex
@@ -73,7 +75,7 @@ object Implicits {
def hasAttribute(name: String): Boolean = request.getAttribute(name) != null def hasAttribute(name: String): Boolean = request.getAttribute(name) != null
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/git/", "/") def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^" + quote(request.getContextPath) + "/git/", "/")
def baseUrl:String = { def baseUrl:String = {
val url = request.getRequestURL.toString val url = request.getRequestURL.toString
@@ -83,12 +85,6 @@ object Implicits {
} }
implicit class RichSession(session: HttpSession){ implicit class RichSession(session: HttpSession){
def putAndGet[T](key: String, value: T): T = {
session.setAttribute(key, value)
value
}
def getAndRemove[T](key: String): Option[T] = { def getAndRemove[T](key: String): Option[T] = {
val value = session.getAttribute(key).asInstanceOf[T] val value = session.getAttribute(key).asInstanceOf[T]
if(value == null){ if(value == null){

View File

@@ -3,11 +3,8 @@ package gitbucket.core.util
import java.io._ import java.io._
import java.sql._ import java.sql._
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import javax.xml.stream.{XMLStreamConstants, XMLInputFactory, XMLOutputFactory}
import ControlUtil._ import ControlUtil._
import scala.StringBuilder
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable
import scala.collection.mutable.ListBuffer import scala.collection.mutable.ListBuffer
/** /**
@@ -64,65 +61,34 @@ object JDBCUtil {
} }
} }
def importAsXML(in: InputStream): Unit = { def importAsSQL(in: InputStream): Unit = {
conn.setAutoCommit(false) conn.setAutoCommit(false)
try { try {
val factory = XMLInputFactory.newInstance() using(in){ in =>
using(factory.createXMLStreamReader(in, "UTF-8")){ reader => var out = new ByteArrayOutputStream()
// stateful objects
var elementName = ""
var insertTable = ""
var insertColumns = Map.empty[String, (String, String)]
while(reader.hasNext){ var length = 0
reader.next() val bytes = new scala.Array[Byte](1024 * 8)
var stringLiteral = false
reader.getEventType match { var count = 0
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) while({ length = in.read(bytes); length != -1 }){
for(i <- 0 to length - 1){
insertColumns = Map.empty[String, (String, String)] // Clear column information val c = bytes(i)
} if(c == '\''){
case _ => // Nothing to do stringLiteral = !stringLiteral
} }
case _ => // Nothing to do if(c == ';' && !stringLiteral){
val sql = new String(out.toByteArray, "UTF-8")
conn.update(sql)
out = new ByteArrayOutputStream()
} else {
out.write(c)
}
} }
} }
} }
conn.commit() conn.commit()
} catch { } catch {
@@ -133,68 +99,6 @@ object JDBCUtil {
} }
} }
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 = { def exportAsSQL(targetTables: Seq[String]): File = {
val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") val dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val file = File.createTempFile("gitbucket-export-", ".sql") val file = File.createTempFile("gitbucket-export-", ".sql")

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

@@ -37,7 +37,7 @@ trait LinkConverter { self: RequestCache =>
// convert username/project@SHA to link // convert username/project@SHA to link
.replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m => .replaceBy("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)/([a-zA-Z0-9\\-_\\.]+)@([a-f0-9]{40})(?=(\\W|$))".r){ m =>
getAccountByUserName(m.group(2)).map { _ => getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a>""" s"""<code><a href="${context.path}/${m.group(2)}/${m.group(3)}/commit/${m.group(4)}">${m.group(2)}/${m.group(3)}@${m.group(4).substring(0, 7)}</a></code>"""
} }
} }
@@ -56,7 +56,7 @@ trait LinkConverter { self: RequestCache =>
// convert username@SHA to link // convert username@SHA to link
.replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m => .replaceBy( ("(?<=(^|\\W))([a-zA-Z0-9\\-_]+)@([a-f0-9]{40})(?=(\\W|$))").r ) { m =>
getAccountByUserName(m.group(2)).map { _ => getAccountByUserName(m.group(2)).map { _ =>
s"""<a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a>""" s"""<code><a href="${context.path}/${m.group(2)}/${repository.name}/commit/${m.group(3)}">${m.group(2)}@${m.group(3).substring(0, 7)}</a></code>"""
} }
} }
@@ -93,6 +93,8 @@ trait LinkConverter { self: RequestCache =>
} }
// convert commit id to link // convert commit id to link
.replaceAll("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))", s"""<a href="${context.path}/${repository.owner}/${repository.name}/commit/$$2">$$2</a>""") .replaceBy("(?<=(^|[^\\w/@]))([a-f0-9]{40})(?=(\\W|$))".r){ m =>
Some(s"""<code><a href="${context.path}/${repository.owner}/${repository.name}/commit/${m.group(2)}">${m.group(2).substring(0, 7)}</a></code>""")
}
} }
} }

View File

@@ -44,7 +44,7 @@ object Markdown {
val renderer = new GitBucketMarkedRenderer(options, repository, val renderer = new GitBucketMarkedRenderer(options, repository,
enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages) enableWikiLink, enableRefsLink, enableAnchor, enableTaskList, hasWritePermission, pages)
Marked.marked(source, options, renderer) helpers.decorateHtml(Marked.marked(source, options, renderer), repository)
} }
/** /**
@@ -147,21 +147,23 @@ object Markdown {
} }
private def fixUrl(url: String, isImage: Boolean = false): String = { private def fixUrl(url: String, isImage: Boolean = false): String = {
lazy val urlWithRawParam: String = url + (if(isImage && !url.endsWith("?raw=true")) "?raw=true" else "")
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){ if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("/")){
url url
} else if(url.startsWith("#")){ } else if(url.startsWith("#")){
("#" + generateAnchorName(url.substring(1))) ("#" + generateAnchorName(url.substring(1)))
} else if(!enableWikiLink){ } else if(!enableWikiLink){
if(context.currentPath.contains("/blob/")){ if(context.currentPath.contains("/blob/")){
url + (if(isImage) "?raw=true" else "") urlWithRawParam
} else if(context.currentPath.contains("/tree/")){ } else if(context.currentPath.contains("/tree/")){
val paths = context.currentPath.split("/") val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam
} else { } else {
val paths = context.currentPath.split("/") val paths = context.currentPath.split("/")
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "") repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + urlWithRawParam
} }
} else { } else {
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url

View File

@@ -5,10 +5,10 @@ import java.util.{Date, Locale, TimeZone}
import gitbucket.core.controller.Context import gitbucket.core.controller.Context
import gitbucket.core.model.CommitState import gitbucket.core.model.CommitState
import gitbucket.core.plugin.{RenderRequest, PluginRegistry} import gitbucket.core.plugin.{PluginRegistry, RenderRequest}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.{RepositoryService, RequestCache} import gitbucket.core.service.{RepositoryService, RequestCache}
import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil} import gitbucket.core.util.{FileUtil, JGitUtil, StringUtil}
import play.twirl.api.{Html, HtmlFormat} import play.twirl.api.{Html, HtmlFormat}
/** /**
@@ -151,7 +151,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
* Converts commit id, issue id and username to the link. * Converts commit id, issue id and username to the link.
*/ */
def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html = def link(value: String, repository: RepositoryService.RepositoryInfo)(implicit context: Context): Html =
Html(convertRefsLinks(value, repository)) Html(decorateHtml(convertRefsLinks(value, repository), repository))
def cut(value: String, length: Int): String = def cut(value: String, length: Int): String =
if(value.length > length){ if(value.length > length){
@@ -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 {
@@ -309,10 +316,18 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case CommitState.FAILURE => "Failed" case CommitState.FAILURE => "Failed"
} }
/**
* Render a given object as the JSON string.
*/
def json(obj: AnyRef): String = {
implicit val formats = org.json4s.DefaultFormats
org.json4s.jackson.Serialization.write(obj)
}
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string) // This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r
def detectAndRenderLinks(text: String): Html = { def detectAndRenderLinks(text: String, repository: RepositoryInfo)(implicit context: Context): String = {
val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq val matches = detectAndRenderLinksRegex.findAllMatchIn(text).toSeq
val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) => val (x, pos) = matches.foldLeft((collection.immutable.Seq.empty[Html], 0)){ case ((x, pos), m) =>
@@ -326,6 +341,43 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
// append rest fragment // append rest fragment
val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x val out = if (pos < text.length) x :+ HtmlFormat.escape(text.substring(pos)) else x
HtmlFormat.fill(out) decorateHtml(HtmlFormat.fill(out).toString, repository)
} }
def decorateHtml(html: String, repository: RepositoryInfo)(implicit context: Context): String = {
PluginRegistry().getTextDecorators.foldLeft(html){ case (html, decorator) =>
val text = new StringBuilder()
val result = new StringBuilder()
var tag = false
html.foreach { c =>
c match {
case '<' if tag == false => {
tag = true
if(text.nonEmpty){
result.append(decorator.decorate(text.toString, repository))
text.setLength(0)
}
result.append(c)
}
case '>' if tag == true => {
tag = false
result.append(c)
}
case _ if tag == false => {
text.append(c)
}
case _ if tag == true => {
result.append(c)
}
}
}
if(text.nonEmpty){
result.append(decorator.decorate(text.toString, repository))
}
result.toString
}
}
} }

View File

@@ -1,11 +1,10 @@
@(account: gitbucket.core.model.Account, @(account: gitbucket.core.model.Account,
groupNames: List[String], groupNames: List[String],
activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context) activities: List[gitbucket.core.model.Activity])(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.account.html.main(account, groupNames, "activity"){
@main(account, groupNames, "activity"){
<div class="pull-right"> <div class="pull-right">
<a href="@path/@{account.userName}.atom"><img src="@assets/common/images/feed.png" alt="activities"></a> <a href="@context.path/@{account.userName}.atom"><img src="@{helpers.assets}/common/images/feed.png" alt="activities"></a>
</div> </div>
@helper.html.activities(activities) @gitbucket.core.helper.html.activities(activities)
} }

View File

@@ -1,11 +1,9 @@
@(account: gitbucket.core.model.Account, @(account: gitbucket.core.model.Account,
personalTokens: List[gitbucket.core.model.AccessToken], personalTokens: List[gitbucket.core.model.AccessToken],
gneratedToken: Option[(gitbucket.core.model.AccessToken, String)])(implicit context: gitbucket.core.controller.Context) gneratedToken: Option[(gitbucket.core.model.AccessToken, String)])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("Applications"){
@import gitbucket.core.view.helpers._
@html.main("Applications"){
<div class="container body"> <div class="container body">
@menu("application", settings.ssh){ @gitbucket.core.account.html.menu("application", context.settings.ssh){
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Personal access tokens</div> <div class="panel-heading strong">Personal access tokens</div>
<div class="panel-body"> <div class="panel-body">
@@ -15,13 +13,13 @@
Tokens you have generated that can be used to access the GitBucket API. Tokens you have generated that can be used to access the GitBucket API.
<hr style="margin-top: 10px;"> <hr style="margin-top: 10px;">
} }
@gneratedToken.map{ case (token, tokenString) => @gneratedToken.map { case (token, tokenString) =>
<div class="alert alert-info"> <div class="alert alert-info">
Make sure to copy your new personal access token now. You won't be able to see it again! Make sure to copy your new personal access token now. You won't be able to see it again!
</div> </div>
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a> <a href="@context.path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
<div style="width: 50%;"> <div style="width: 50%;">
@helper.html.copy("generated-token-copy", tokenString){ @gitbucket.core.helper.html.copy("generated-token-copy", tokenString){
<input type="text" value="@tokenString" class="form-control input-sm" readonly> <input type="text" value="@tokenString" class="form-control input-sm" readonly>
} }
</div> </div>
@@ -32,11 +30,11 @@
<hr> <hr>
} }
<strong style="line-height: 30px;">@token.note</strong> <strong style="line-height: 30px;">@token.note</strong>
<a href="@path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a> <a href="@context.path/@account.userName/_personalToken/delete/@token.accessTokenId" class="btn btn-sm btn-danger pull-right">Delete</a>
} }
</div> </div>
</div> </div>
<form method="POST" action="@path/@account.userName/_personalToken" validate="true"> <form method="POST" action="@context.path/@account.userName/_personalToken" validate="true">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Generate new token</div> <div class="panel-heading strong">Generate new token</div>
<div class="panel-body"> <div class="panel-body">

View File

@@ -1,13 +1,13 @@
@(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 gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.html.main("Edit your profile"){
@html.main("Edit your profile"){
<div class="container body"> <div class="container body">
@menu("profile", settings.ssh){ @gitbucket.core.account.html.menu("profile", context.settings.ssh){
@helper.html.information(info) @gitbucket.core.helper.html.information(info)
@gitbucket.core.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="@helpers.url(account.userName)/_edit" method="POST" validate="true">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Profile</div> <div class="panel-heading strong">Profile</div>
<div class="panel-body"> <div class="panel-body">
@@ -41,7 +41,7 @@
<div class="col-md-6"> <div class="col-md-6">
<fieldset class="form-group"> <fieldset class="form-group">
<label for="avatar" class="strong">Image (optional):</label> <label for="avatar" class="strong">Image (optional):</label>
@helper.html.uploadavatar(Some(account)) @gitbucket.core.helper.html.uploadavatar(Some(account))
</fieldset> </fieldset>
</div> </div>
</div> </div>
@@ -49,10 +49,10 @@
</div> </div>
<div> <div>
<div class="pull-right"> <div class="pull-right">
<a href="@path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a> <a href="@context.path/@account.userName/_delete" class="btn btn-danger" id="delete">Delete account</a>
</div> </div>
<input type="submit" class="btn btn-success" value="Save"/> <input type="submit" class="btn btn-success" value="Save"/>
@if(!LDAPUtil.isDummyMailAddress(account)){<a href="@url(account.userName)" class="btn btn-default">Cancel</a>} @if(!LDAPUtil.isDummyMailAddress(account)){<a href="@helpers.url(account.userName)" class="btn btn-default">Cancel</a>}
</div> </div>
</form> </form>
} }

View File

@@ -1,9 +1,10 @@
@(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context) @(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.html.main(if(account.isEmpty) "Create group" else "Edit group"){
@html.main(if(account.isEmpty) "Create group" else "Edit group"){ <div class="content-wrapper main-center">
<div class="body main-center"> <div class="content body">
<form id="form" method="post" action="@if(account.isEmpty){@path/groups/new} else {@path/@account.get.userName/_editgroup}" validate="true"> <h2>@{if(account.isEmpty) "Create group" else "Edit group"}</h2>
<form id="form" method="post" action="@if(account.isEmpty){@context.path/groups/new} else {@context.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">
@@ -22,7 +23,7 @@
</fieldset> </fieldset>
<fieldset class="form-group"> <fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @gitbucket.core.helper.html.uploadavatar(account)
</fieldset> </fieldset>
</div> </div>
<div class="col-md-7"> <div class="col-md-7">
@@ -30,7 +31,7 @@
<label class="strong">Members</label> <label class="strong">Members</label>
<ul id="member-list" class="collaborator"> <ul id="member-list" class="collaborator">
</ul> </ul>
@helper.html.account("memberName", 200) @gitbucket.core.helper.html.account("memberName", 200)
<input type="button" class="btn btn-default" value="Add" id="addMember"/> <input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/> <input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div> <div>
@@ -39,18 +40,19 @@
</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="@helpers.url(account.get.userName)/_deletegroup" id="delete" class="btn btn-danger">Delete Group</a>
</div> </div>
} }
<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}"/>
@if(account.isDefined){ @if(account.isDefined){
<a href="@url(account.get.userName)" class="btn btn-default">Cancel</a> <a href="@helpers.url(account.get.userName)" class="btn btn-default">Cancel</a>
} }
</fieldset> </fieldset>
</form> </form>
</div>
</div> </div>
} }
<script> <script>
@@ -78,7 +80,7 @@ $(function(){
} }
// check existence // check existence
$.post('@path/_user/existence', { $.post('@context.path/_user/existence', {
'userName': userName 'userName': userName
}, function(data, status){ }, function(data, status){
if(data == 'true'){ if(data == 'true'){
@@ -122,7 +124,7 @@ $(function(){
.append(memberButton) .append(memberButton)
.append(managerButton)) .append(managerButton))
.append(' ') .append(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName)) .append($('<a>').attr('href', '@context.path/' + userName).text(userName))
.append(' ') .append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>'))); .append($('<a href="#" class="remove pull-right">(remove)</a>')));
} }

View File

@@ -1,54 +1,61 @@
@(account: gitbucket.core.model.Account, groupNames: List[String], active: String, @(account: gitbucket.core.model.Account, groupNames: List[String], active: String,
isGroupManager: Boolean = false)(body: Html)(implicit context: gitbucket.core.controller.Context) isGroupManager: Boolean = false)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.html.main(account.userName){
@html.main(account.userName){ <div class="main-sidebar">
<div class="container body"> <div class="sidebar">
<div class="main-sidebar"> <div class="user-panel">
<div class="block"> <div class="pull-left image">@helpers.avatar(account.userName, 40)</div>
<div class="account-image">@avatar(account.userName, 240)</div> <div class="pull-left info">
<div class="account-fullname">@account.fullName</div> <p>@account.userName</p>
<div class="account-username">@account.userName</div> @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 @helpers.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>@helpers.avatarLink(groupName, 20, tooltip = true, label = true)</li>
} }
</div> </ul>
} }
</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="@helpers.url(account.userName)?tab=repositories">Repositories</a></li>
@if(account.isGroupAccount){ @if(account.isGroupAccount){
<li@if(active == "members"){ class="active"}><a href="@url(account.userName)?tab=members">Members</a></li> <li@if(active == "members"){ class="active"}><a href="@helpers.url(account.userName)?tab=members">Members</a></li>
} else { } else {
<li@if(active == "activity"){ class="active"}><a href="@url(account.userName)?tab=activity">Public Activity</a></li> <li@if(active == "activity"){ class="active"}><a href="@helpers.url(account.userName)?tab=activity">Public Activity</a></li>
} }
@gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab => @gitbucket.core.plugin.PluginRegistry().getProfileTabs.map { tab =>
@tab(account, context).map { link => @tab(account, context).map { link =>
<li@if(active == link.id){ class="active"}><a href="@path/@link.path">@link.label</a></li> <li@if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>
} }
} }
@if(loginAccount.isDefined && loginAccount.get.userName == account.userName){ @if(context.loginAccount.isDefined && context.loginAccount.get.userName == account.userName){
<li class="pull-right"> <li class="pull-right">
<div class="button-group"> <div class="button-group">
<a href="@url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a> <a href="@helpers.url(account.userName)/_edit" class="btn btn-default">Edit Your Profile</a>
</div> </div>
</li> </li>
} }
@if(loginAccount.isDefined && account.isGroupAccount && isGroupManager){ @if(context.loginAccount.isDefined && account.isGroupAccount && isGroupManager){
<li class="pull-right"> <li class="pull-right">
<div class="button-group"> <div class="button-group">
<a href="@url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a> <a href="@helpers.url(account.userName)/_editgroup" class="btn btn-default">Edit Group</a>
</div> </div>
</li> </li>
} }

View File

@@ -1,14 +1,13 @@
@(account: gitbucket.core.model.Account, members: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) @(account: gitbucket.core.model.Account, members: List[String], isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.account.html.main(account, Nil, "members", isGroupManager){
@main(account, Nil, "members", isGroupManager){
@if(members.isEmpty){ @if(members.isEmpty){
No members No members
} else { } else {
@members.map { userName => @members.map { userName =>
<div class="block"> <div class="block">
<div class="block-header"> <div class="block-header">
@avatar(userName, 20) <a href="@url(userName)">@userName</a> @helpers.avatar(userName, 20) <a href="@helpers.url(userName)">@userName</a>
</div> </div>
</div> </div>
} }

View File

@@ -1,27 +1,30 @@
@(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._
<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="@context.path/@context.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="@context.path/@context.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="@context.path/@context.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="@context.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

@@ -1,75 +1,76 @@
@(groupNames: List[String], @(groupNames: List[String],
isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.Context) isCreateRepoOptionPublic: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.html.main("Create a New Repository"){
@html.main("Create a New Repository"){ <div class="content-wrapper main-center">
<div class="body main-center"> <div class="content body">
<h2>Create a new repository</h2> <h2>Create a new repository</h2>
<p class="muted"> <p class="muted">
A repository contains all the files for your project, including the revision history. A repository contains all the files for your project, including the revision history.
</p> </p>
<form id="form" method="post" action="@path/new" validate="true"> <form id="form" method="post" action="@context.path/new" validate="true">
<fieldset class="margin form-group"> <fieldset class="border-top form-group">
<dl style="float: left;"> <dl style="float: left;">
<dt>Owner</dt> <dt>Owner</dt>
<dd style="margin-left: 0px;"> <dd style="margin-left: 0px;">
<div class="btn-group" id="owner-dropdown"> <div class="btn-group" id="owner-dropdown">
<button class="btn dropdown-toggle btn-default" data-toggle="dropdown"> <button class="btn dropdown-toggle btn-default" data-toggle="dropdown">
<span class="strong">@avatar(loginAccount.get.userName, 20) @loginAccount.get.userName</span> <span class="strong">@helpers.avatar(context.loginAccount.get.userName, 20) @context.loginAccount.get.userName</span>
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<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="@context.loginAccount.get.userName"><i class="octicon octicon-check"></i> <span>@helpers.avatar(context.loginAccount.get.userName, 20) @context.loginAccount.get.userName</span></a></li>
@groupNames.map { groupName => @groupNames.map { groupName =>
<li><a href="javascript:void(0);" data-name="@groupName"><i class="octicon"></i> <span>@avatar(groupName, 20) @groupName</span></a></li> <li><a href="javascript:void(0);" data-name="@groupName"><i class="octicon"></i> <span>@helpers.avatar(groupName, 20) @groupName</span></a></li>
} }
</ul> </ul>
<input type="hidden" name="owner" id="owner" value="@loginAccount.get.userName"/> <input type="hidden" name="owner" id="owner" value="@context.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

@@ -1,50 +1,50 @@
@()(implicit context: gitbucket.core.controller.Context) @()(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("Create your account"){
@import gitbucket.core.view.helpers._ <div class="content-wrapper main-center">
@html.main("Create your account"){ <div class="content body">
<div class="container body"> <h2>Create your account</h2>
<h3>Create your account</h3> <form action="@context.path/register" method="POST" validate="true">
<form action="@path/register" method="POST" validate="true"> <div class="row">
<div class="row"> <div class="col-md-6">
<div class="col-md-6"> <fieldset>
<fieldset> <label for="userName" class="strong">Username:</label>
<label for="userName" class="strong">Username:</label> <input type="text" name="userName" id="userName" value="" class="form-control" autofocus/>
<input type="text" name="userName" id="userName" value="" autofocus/> <span id="error-userName" class="error"></span>
<span id="error-userName" class="error"></span> </fieldset>
</fieldset> <fieldset>
<fieldset> <label for="password" class="strong">
<label for="password" class="strong"> Password:
Password: </label>
</label> <input type="password" name="password" id="password" class="form-control" value=""/>
<input type="password" name="password" id="password" value=""/> <span id="error-password" class="error"></span>
<span id="error-password" class="error"></span> </fieldset>
</fieldset> <fieldset>
<fieldset> <label for="fullName" class="strong">Full Name:</label>
<label for="fullName" class="strong">Full Name:</label> <input type="text" name="fullName" id="fullName" class="form-control" value=""/>
<input type="text" name="fullName" id="fullName" value=""/> <span id="error-fullName" class="error"></span>
<span id="error-fullName" class="error"></span> </fieldset>
</fieldset> <fieldset>
<fieldset> <label for="mailAddress" class="strong">Mail Address:</label>
<label for="mailAddress" class="strong">Mail Address:</label> <input type="text" name="mailAddress" id="mailAddress" class="form-control" value=""/>
<input type="text" name="mailAddress" id="mailAddress" value=""/> <span id="error-mailAddress" class="error"></span>
<span id="error-mailAddress" class="error"></span> </fieldset>
</fieldset> <fieldset>
<fieldset> <label for="url" class="strong">URL (optional):</label>
<label for="url" class="strong">URL (optional):</label> <input type="text" name="url" id="url" class="form-control" value=""/>
<input type="text" name="url" id="url" style="width: 400px;" value=""/> <span id="error-url" class="error"></span>
<span id="error-url" class="error"></span> </fieldset>
</fieldset> </div>
<div class="col-md-6">
<fieldset>
<label for="avatar" class="strong">Image (optional):</label>
@gitbucket.core.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

@@ -1,31 +1,30 @@
@(account: gitbucket.core.model.Account, groupNames: List[String], @(account: gitbucket.core.model.Account, groupNames: List[String],
repositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], repositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context) isGroupManager: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.account.html.main(account, groupNames, "repositories", isGroupManager){
@main(account, groupNames, "repositories", isGroupManager){
@if(repositories.isEmpty){ @if(repositories.isEmpty){
No repositories No repositories
} else { } else {
@repositories.map { repository => @repositories.map { repository =>
<div class="block"> <div class="block">
<div class="repository-icon"> <div class="repository-icon">
@helper.html.repositoryicon(repository, true) @gitbucket.core.helper.html.repositoryicon(repository, true)
</div> </div>
<div class="repository-content"> <div class="repository-content">
<div class="block-header"> <div class="block-header">
<a href="@url(repository)">@repository.name</a> <a href="@helpers.url(repository)">@repository.name</a>
@if(repository.repository.isPrivate){ @if(repository.repository.isPrivate){
<i class="octicon octicon-lock"></i> <i class="octicon octicon-lock"></i>
} }
</div> </div>
@if(repository.repository.originUserName.isDefined){ @if(repository.repository.originUserName.isDefined){
<div class="small muted">forked from <a href="@path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div> <div class="small muted">forked from <a href="@context.path/@repository.repository.parentUserName/@repository.repository.parentRepositoryName">@repository.repository.parentUserName/@repository.repository.parentRepositoryName</a></div>
} }
@if(repository.repository.description.isDefined){ @if(repository.repository.description.isDefined){
<div>@repository.repository.description</div> <div>@repository.repository.description</div>
} }
<div><span class="muted small">Updated @helper.html.datetimeago(repository.repository.lastActivityDate)</span></div> <div><span class="muted small">Updated @gitbucket.core.helper.html.datetimeago(repository.repository.lastActivityDate)</span></div>
</div> </div>
</div> </div>
} }

View File

@@ -1,10 +1,8 @@
@(account: gitbucket.core.model.Account, sshKeys: List[gitbucket.core.model.SshKey])(implicit context: gitbucket.core.controller.Context) @(account: gitbucket.core.model.Account, sshKeys: List[gitbucket.core.model.SshKey])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.ssh.SshUtil @import gitbucket.core.ssh.SshUtil
@import context._ @gitbucket.core.html.main("SSH Keys"){
@import gitbucket.core.view.helpers._
@html.main("SSH Keys"){
<div class="container body"> <div class="container body">
@menu("ssh", settings.ssh){ @gitbucket.core.account.html.menu("ssh", context.settings.ssh){
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">SSH Keys</div> <div class="panel-heading strong">SSH Keys</div>
<div class="panel-body"> <div class="panel-body">
@@ -16,11 +14,11 @@
<hr> <hr>
} }
<strong style="line-height: 30px;">@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid.")) <strong style="line-height: 30px;">@key.title</strong> (@SshUtil.fingerPrint(key.publicKey).getOrElse("Key is invalid."))
<a href="@path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a> <a href="@context.path/@account.userName/_ssh/delete/@key.sshKeyId" class="btn btn-sm btn-danger pull-right">Delete</a>
} }
</div> </div>
</div> </div>
<form method="POST" action="@path/@account.userName/_ssh" validate="true"> <form method="POST" action="@context.path/@account.userName/_ssh" validate="true">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Add an SSH Key</div> <div class="panel-heading strong">Add an SSH Key</div>
<div class="panel-body"> <div class="panel-body">

View File

@@ -1,12 +1,10 @@
@(tableNames: Seq[String])(implicit context: gitbucket.core.controller.Context) @(tableNames: Seq[String])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("Data export / import"){
@import gitbucket.core.view.helpers._ @gitbucket.core.admin.html.menu("data") {
@html.main("Data export / import"){
@admin.html.menu("data") {
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Export</div> <div class="panel-heading strong">Export</div>
<div class="panel-body"> <div class="panel-body">
<form class="form form-horizontal" action="@path/admin/export" method="POST"> <form class="form form-horizontal" action="@context.path/admin/export" method="POST">
@tableNames.map { tableName => @tableNames.map { tableName =>
<div class="checkbox"> <div class="checkbox">
<label> <label>
@@ -15,24 +13,14 @@
</div> </div>
} }
<input type="submit" class="btn btn-success pull-right" value="Export"> <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> </form>
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">Import (only XML)</div> <div class="panel-heading strong">Import</div>
<div class="panel-body"> <div class="panel-body">
<form class="form form-horizontal" action="@path/upload/import" method="POST" enctype="multipart/form-data" id="import-form"> <form class="form form-horizontal" action="@context.path/upload/import" method="POST" enctype="multipart/form-data" id="import-form">
<input type="file" name="file" id="file"> <input type="file" name="file" id="file">
<input type="submit" class="btn btn-success pull-right" value="Import" id="import"> <input type="submit" class="btn btn-success pull-right" value="Import" id="import">
</form> </form>
@@ -44,10 +32,10 @@
$(function(){ $(function(){
$('#import-form').submit(function(){ $('#import-form').submit(function(){
if($('#file').val() == ''){ if($('#file').val() == ''){
alert('Choose an import XML file.'); alert('Choose an import SQL file.');
return false; return false;
} else if(!$('#file').val().endsWith(".xml")){ } else if(!$('#file').val().endsWith(".sql")){
alert('Import is available for only the XML file.'); alert('Import is available for only the SQL file.');
return false; return false;
} }
return confirm('All existing data is deleted before importing.\nAre you sure?'); return confirm('All existing data is deleted before importing.\nAre you sure?');

View File

@@ -1,33 +1,34 @@
@(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context) @(active: String)(body: Html)(implicit context: gitbucket.core.controller.Context)
@import context._ <div class="main-sidebar">
<div class="container body"> <div class="sidebar">
<div class="main-sidebar"> <ul class="sidebar-menu" id="system-admin-menu-container">
<ul class="nav nav-pills nav-stacked" 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="@context.path/admin/users">User Management</a>
</li> </li>
<li@if(active=="system"){ class="active"}> <li@if(active=="system"){ class="active"}>
<a href="@path/admin/system">System Settings</a> <a href="@context.path/admin/system">System Settings</a>
</li> </li>
<li@if(active=="plugins"){ class="active"}> <li@if(active=="plugins"){ class="active"}>
<a href="@path/admin/plugins">Plugins</a> <a href="@context.path/admin/plugins">Plugins</a>
</li> </li>
<li@if(active=="data"){ class="active"}> <li@if(active=="data"){ class="active"}>
<a href="@path/admin/data">Data export / import</a> <a href="@context.path/admin/data">Data export / import</a>
</li> </li>
<li> <li>
<a href="@path/console/login.jsp">H2 Console</a> <a href="@context.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 =>
<li@if(active==link.id){ class="active"}> <li@if(active==link.id){ class="active"}>
<a href="@path/@link.path">@link.label</a> <a href="@context.path/@link.path">@link.label</a>
</li> </li>
} }
} }
</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

@@ -1,36 +1,34 @@
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context) @(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("Plugins"){
@import gitbucket.core.view.helpers._ @gitbucket.core.admin.html.menu("plugins") {
@html.main("Plugins"){
@admin.html.menu("plugins") {
<h1>Installed plugins</h1> <h1>Installed plugins</h1>
@if(plugins.size > 0) { @if(plugins.size > 0) {
<ul> <ul>
@plugins.map {plugin => @plugins.map { plugin =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.version</a></li> <li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
} }
</ul> </ul>
@plugins.map {plugin => @plugins.map { plugin =>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading strong">@plugin.pluginName</div> <div class="panel-heading strong">@plugin.pluginName</div>
<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.pluginVersion</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

@@ -1,11 +1,8 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context) @(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("System Settings"){
@import gitbucket.core.view.helpers._ @gitbucket.core.admin.html.menu("system"){
@import gitbucket.core.util.Directory._ @gitbucket.core.helper.html.information(info)
@html.main("System Settings"){ <form action="@context.path/admin/system" method="POST" validate="true" class="form-horizontal">
@menu("system"){
@helper.html.information(info)
<form action="@path/admin/system" method="POST" validate="true" class="form-horizontal">
<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">
@@ -19,10 +16,10 @@
</tr> </tr>
<tr> <tr>
<td>GITBUCKET_HOME</td> <td>GITBUCKET_HOME</td>
<td>@GitBucketHome</td> <td>@gitbucket.core.util.Directory.GitBucketHome</td>
</tr> </tr>
<tr> <tr>
<td>DATBASE_URL</td> <td>DATABASE_URL</td>
<td>@gitbucket.core.util.DatabaseConfig.url</td> <td>@gitbucket.core.util.DatabaseConfig.url</td>
</tr> </tr>
</table> </table>
@@ -33,7 +30,7 @@
<label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label> <label><span class="strong">Base URL</span> (e.g. <code>http://example.com/gitbucket</code>)</label>
<fieldset> <fieldset>
<div class="controls"> <div class="controls">
<input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@settings.baseUrl"/> <input type="text" name="baseUrl" id="baseUrl" class="form-control" value="@context.settings.baseUrl"/>
<span id="error-baseUrl" class="error"></span> <span id="error-baseUrl" class="error"></span>
</div> </div>
</fieldset> </fieldset>
@@ -48,7 +45,7 @@
<hr> <hr>
<label><span class="strong">Information</span> (HTML is available)</label> <label><span class="strong">Information</span> (HTML is available)</label>
<fieldset> <fieldset>
<textarea name="information" class="form-control" style="height: 100px;">@settings.information</textarea> <textarea name="information" class="form-control" style="height: 100px;">@context.settings.information</textarea>
</fieldset> </fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Account registration --> <!-- Account registration -->
@@ -57,11 +54,11 @@
<label class="strong">Account registration</label> <label class="strong">Account registration</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="true"@if(settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="true"@if(context.settings.allowAccountRegistration){ checked}>
<span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span> <span class="strong">Allow</span> <span class="normal">- Users can create accounts by themselves.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAccountRegistration" value="false"@if(!settings.allowAccountRegistration){ checked}> <input type="radio" name="allowAccountRegistration" value="false"@if(!context.settings.allowAccountRegistration){ checked}>
<span class="strong">Deny</span> - <span class="normal">Only administrators can create accounts.</span> <span class="strong">Deny</span> - <span class="normal">Only administrators can create accounts.</span>
</label> </label>
</fieldset> </fieldset>
@@ -69,11 +66,11 @@
<label class="strong">Default option to create a new repository</label> <label class="strong">Default option to create a new repository</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="true"@if(settings.isCreateRepoOptionPublic){ checked}> <input type="radio" name="isCreateRepoOptionPublic" value="true"@if(context.settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Public</span> <span class="normal">- All users and guests can read that repository.</span> <span class="strong">Public</span> <span class="normal">- All users and guests can read that repository.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!settings.isCreateRepoOptionPublic){ checked}> <input type="radio" name="isCreateRepoOptionPublic" value="false"@if(!context.settings.isCreateRepoOptionPublic){ checked}>
<span class="strong">Private</span> <span class="normal">- Only collaborators can read that repository.</span> <span class="strong">Private</span> <span class="normal">- Only collaborators can read that repository.</span>
</label> </label>
</fieldset> </fieldset>
@@ -84,11 +81,11 @@
<label class="strong">Anonymous access</label> <label class="strong">Anonymous access</label>
<fieldset> <fieldset>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAnonymousAccess" value="true"@if(settings.allowAnonymousAccess){ checked}> <input type="radio" name="allowAnonymousAccess" value="true"@if(context.settings.allowAnonymousAccess){ checked}>
<span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories, user/group profiles.</span> <span class="strong">Allow</span> <span class="normal">- Anyone can view public repositories, user/group profiles.</span>
</label> </label>
<label class="radio"> <label class="radio">
<input type="radio" name="allowAnonymousAccess" value="false"@if(!settings.allowAnonymousAccess){ checked}> <input type="radio" name="allowAnonymousAccess" value="false"@if(!context.settings.allowAnonymousAccess){ checked}>
<span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span> <span class="strong">Deny</span> <span class="normal">- Users must authenticate before viewing any information.</span>
</label> </label>
</fieldset> </fieldset>
@@ -97,10 +94,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="@context.settings.activityLogLimit"/>
<span id="error-activityLogLimit" class="error"></span>
</div>
</div>
</fieldset>
<!--====================================================================--> <!--====================================================================-->
<!-- Services --> <!-- Services -->
<!--====================================================================--> <!--====================================================================-->
@@ -108,7 +110,7 @@
<label class="strong">Services</label> <label class="strong">Services</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="gravatar"@if(settings.gravatar){ checked}/> <input type="checkbox" name="gravatar"@if(context.settings.gravatar){ checked}/>
Use Gravatar for Profile-Images Use Gravatar for Profile-Images
</label> </label>
</fieldset> </fieldset>
@@ -119,7 +121,7 @@
<label class="strong">SSH access</label> <label class="strong">SSH access</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="ssh" name="ssh"@if(settings.ssh){ checked}/> <input type="checkbox" id="ssh" name="ssh"@if(context.settings.ssh){ checked}/>
Enable SSH access to git repository Enable SSH access to git repository
</label> </label>
</fieldset> </fieldset>
@@ -127,14 +129,14 @@
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="sshHost">SSH Host</label> <label class="control-label col-md-3" for="sshHost">SSH Host</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="sshHost" name="sshHost" class="form-control" value="@settings.sshHost"/> <input type="text" id="sshHost" name="sshHost" class="form-control" value="@context.settings.sshHost"/>
<span id="error-sshHost" class="error"></span> <span id="error-sshHost" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="sshPort">SSH Port</label> <label class="control-label col-md-3" for="sshPort">SSH Port</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="sshPort" name="sshPort" class="form-control" value="@settings.sshPort"/> <input type="text" id="sshPort" name="sshPort" class="form-control" value="@context.settings.sshPort"/>
<span id="error-sshPort" class="error"></span> <span id="error-sshPort" class="error"></span>
</div> </div>
</div> </div>
@@ -149,7 +151,7 @@
<label class="strong">Authentication</label> <label class="strong">Authentication</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(settings.ldap){ checked} /> <input type="checkbox" id="ldapAuthentication" name="ldapAuthentication"@if(context.settings.ldap){ checked} />
LDAP LDAP
</label> </label>
</fieldset> </fieldset>
@@ -157,82 +159,82 @@
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapHost">LDAP Host</label> <label class="control-label col-md-3" for="ldapHost">LDAP Host</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@settings.ldap.map(_.host)"/> <input type="text" id="ldapHost" name="ldap.host" class="form-control" value="@context.settings.ldap.map(_.host)"/>
<span id="error-ldap_host" class="error"></span> <span id="error-ldap_host" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapPort">LDAP Port</label> <label class="control-label col-md-3" for="ldapPort">LDAP Port</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@settings.ldap.map(_.port)"/> <input type="text" id="ldapPort" name="ldap.port" class="form-control input-mini" value="@context.settings.ldap.map(_.port)"/>
<span id="error-ldap_port" class="error"></span> <span id="error-ldap_port" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapBindDN">Bind DN</label> <label class="control-label col-md-3" for="ldapBindDN">Bind DN</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@settings.ldap.map(_.bindDN)"/> <input type="text" id="ldapBindDN" name="ldap.bindDN" class="form-control" value="@context.settings.ldap.map(_.bindDN)"/>
<span id="error-ldap_bindDN" class="error"></span> <span id="error-ldap_bindDN" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapBindPassword">Bind Password</label> <label class="control-label col-md-3" for="ldapBindPassword">Bind Password</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@settings.ldap.map(_.bindPassword)"/> <input type="password" id="ldapBindPassword" name="ldap.bindPassword" class="form-control" value="@context.settings.ldap.map(_.bindPassword)"/>
<span id="error-ldap_bindPassword" class="error"></span> <span id="error-ldap_bindPassword" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapBaseDN">Base DN</label> <label class="control-label col-md-3" for="ldapBaseDN">Base DN</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@settings.ldap.map(_.baseDN)"/> <input type="text" id="ldapBaseDN" name="ldap.baseDN" class="form-control" value="@context.settings.ldap.map(_.baseDN)"/>
<span id="error-ldap_baseDN" class="error"></span> <span id="error-ldap_baseDN" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapUserNameAttribute">User name attribute</label> <label class="control-label col-md-3" for="ldapUserNameAttribute">User name attribute</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@settings.ldap.map(_.userNameAttribute)"/> <input type="text" id="ldapUserNameAttribute" name="ldap.userNameAttribute" class="form-control" value="@context.settings.ldap.map(_.userNameAttribute)"/>
<span id="error-ldap_userNameAttribute" class="error"></span> <span id="error-ldap_userNameAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapAdditionalFilterCondition">Additional filter condition</label> <label class="control-label col-md-3" for="ldapAdditionalFilterCondition">Additional filter condition</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@settings.ldap.map(_.additionalFilterCondition)"/> <input type="text" id="ldapAdditionalFilterCondition" name="ldap.additionalFilterCondition" class="form-control" value="@context.settings.ldap.map(_.additionalFilterCondition)"/>
<span id="error-ldap_additionalFilterCondition" class="error"></span> <span id="error-ldap_additionalFilterCondition" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapFullNameAttribute">Full name attribute</label> <label class="control-label col-md-3" for="ldapFullNameAttribute">Full name attribute</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@settings.ldap.map(_.fullNameAttribute)"/> <input type="text" id="ldapFullNameAttribute" name="ldap.fullNameAttribute" class="form-control" value="@context.settings.ldap.map(_.fullNameAttribute)"/>
<span id="error-ldap_fullNameAttribute" class="error"></span> <span id="error-ldap_fullNameAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapMailAttribute">Mail address attribute</label> <label class="control-label col-md-3" for="ldapMailAttribute">Mail address attribute</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@settings.ldap.map(_.mailAttribute)"/> <input type="text" id="ldapMailAttribute" name="ldap.mailAttribute" class="form-control" value="@context.settings.ldap.map(_.mailAttribute)"/>
<span id="error-ldap_mailAttribute" class="error"></span> <span id="error-ldap_mailAttribute" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3">Enable TLS</label> <label class="control-label col-md-3">Enable TLS</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="checkbox" name="ldap.tls"@if(settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/> <input type="checkbox" name="ldap.tls"@if(context.settings.ldap.flatMap(_.tls).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3">Enable SSL</label> <label class="control-label col-md-3">Enable SSL</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="checkbox" name="ldap.ssl"@if(settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/> <input type="checkbox" name="ldap.ssl"@if(context.settings.ldap.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="ldapBindDN">Keystore</label> <label class="control-label col-md-3" for="ldapBindDN">Keystore</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@settings.ldap.map(_.keystore)"/> <input type="text" id="ldapKeystore" name="ldap.keystore" class="form-control" value="@context.settings.ldap.map(_.keystore)"/>
<span id="error-ldap_keystore" class="error"></span> <span id="error-ldap_keystore" class="error"></span>
</div> </div>
</div> </div>
@@ -244,7 +246,7 @@
<label class="strong">Notifications</label> <label class="strong">Notifications</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="notification" name="notification"@if(settings.notification){ checked}/> <input type="checkbox" id="notification" name="notification"@if(context.settings.notification){ checked}/>
Send notifications Send notifications
</label> </label>
</fieldset> </fieldset>
@@ -255,7 +257,7 @@
<label class="strong">Communication</label> <label class="strong">Communication</label>
<fieldset> <fieldset>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="useSMTP" name="useSMTP" @if(settings.useSMTP){ checked}/> <input type="checkbox" id="useSMTP" name="useSMTP" @if(context.settings.useSMTP){ checked}/>
SMTP SMTP
</label> </label>
</fieldset> </fieldset>
@@ -263,47 +265,53 @@
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="smtpHost">SMTP Host</label> <label class="control-label col-md-3" for="smtpHost">SMTP Host</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@settings.smtp.map(_.host)"/> <input type="text" id="smtpHost" name="smtp.host" class="form-control" value="@context.settings.smtp.map(_.host)"/>
<span id="error-smtp_host" class="error"></span> <span id="error-smtp_host" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="smtpPort">SMTP Port</label> <label class="control-label col-md-3" for="smtpPort">SMTP Port</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@settings.smtp.map(_.port)"/> <input type="text" id="smtpPort" name="smtp.port" class="form-control input-mini" value="@context.settings.smtp.map(_.port)"/>
<span id="error-smtp_port" class="error"></span> <span id="error-smtp_port" class="error"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="smtpUser">SMTP User</label> <label class="control-label col-md-3" for="smtpUser">SMTP User</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@settings.smtp.map(_.user)"/> <input type="text" id="smtpUser" name="smtp.user" class="form-control" value="@context.settings.smtp.map(_.user)"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="smtpPassword">SMTP Password</label> <label class="control-label col-md-3" for="smtpPassword">SMTP Password</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@settings.smtp.map(_.password)"/> <input type="password" id="smtpPassword" name="smtp.password" class="form-control" value="@context.settings.smtp.map(_.password)"/>
</div> </div>
</div> </div>
<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(context.settings.smtp.flatMap(_.ssl).getOrElse(false)){ checked}/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="fromAddress">FROM Address</label> <label class="control-label col-md-3" for="fromAddress">FROM Address</label>
<div class="col-md-9"> <div class="col-md-9">
<input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@settings.smtp.map(_.fromAddress)"/> <input type="text" id="fromAddress" name="smtp.fromAddress" class="form-control" value="@context.settings.smtp.map(_.fromAddress)"/>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-md-3" for="fromName">FROM Name</label> <label class="control-label col-md-3" for="fromName">FROM Name</label>
<div class="col-md-9"> <div class="col-md-9">
<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="@context.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>
@@ -318,6 +326,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('@context.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,8 +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._ @gitbucket.core.html.main(if(account.isEmpty) "New User" else "Update User"){
@html.main(if(account.isEmpty) "New User" else "Update User"){ @gitbucket.core.admin.html.menu("users"){
@admin.html.menu("users"){ @gitbucket.core.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){@context.path/admin/users/_newuser} else {@context.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">
<fieldset class="form-group"> <fieldset class="form-group">
@@ -70,13 +70,13 @@
<div class="col-md-6"> <div class="col-md-6">
<fieldset class="form-group"> <fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @gitbucket.core.helper.html.uploadavatar(account)
</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="@context.path/admin/users" class="btn btn-default">Cancel</a>
</fieldset> </fieldset>
</form> </form>
} }

View File

@@ -1,11 +1,9 @@
@(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context) @(account: Option[gitbucket.core.model.Account], members: List[gitbucket.core.model.GroupMember])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main(if(account.isEmpty) "New Group" else "Update Group"){
@import gitbucket.core.view.helpers._ @gitbucket.core.admin.html.menu("users"){
@html.main(if(account.isEmpty) "New Group" else "Update Group"){ <form method="POST" action="@if(account.isEmpty){@context.path/admin/users/_newgroup} else {@context.path/admin/users/@account.get.userName/_editgroup}" validate="true">
@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">
<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>
@@ -28,15 +26,15 @@
</fieldset> </fieldset>
<fieldset class="form-group"> <fieldset class="form-group">
<label for="avatar" class="strong">Image (Optional)</label> <label for="avatar" class="strong">Image (Optional)</label>
@helper.html.uploadavatar(account) @gitbucket.core.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">
</ul> </ul>
@helper.html.account("memberName", 200) @gitbucket.core.helper.html.account("memberName", 200)
<input type="button" class="btn btn-default" value="Add" id="addMember"/> <input type="button" class="btn btn-default" value="Add" id="addMember"/>
<input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/> <input type="hidden" id="members" name="members" value="@members.map(member => member.userName + ":" + member.isManager).mkString(",")"/>
<div> <div>
@@ -45,9 +43,9 @@
</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="@context.path/admin/users" class="btn btn-default">Cancel</a>
</fieldset> </fieldset>
</form> </form>
} }
@@ -77,8 +75,9 @@ $(function(){
} }
// check existence // check existence
$.post('@path/_user/existence', { $.post('@context.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,22 +101,22 @@ $(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(' ')
.append($('<a>').attr('href', '@path/' + userName).text(userName)) .append($('<a>').attr('href', '@context.path/' + userName).text(userName))
.append(' ') .append(' ')
.append($('<a href="#" class="remove pull-right">(remove)</a>'))); .append($('<a href="#" class="remove pull-right">(remove)</a>')));
} }
@@ -125,9 +124,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

@@ -1,11 +1,10 @@
@(users: List[gitbucket.core.model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: gitbucket.core.controller.Context) @(users: List[gitbucket.core.model.Account], members: Map[String, List[String]], includeRemoved: Boolean)(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ @gitbucket.core.html.main("Manage Users"){
@html.main("Manage Users"){ @gitbucket.core.admin.html.menu("users"){
@admin.html.menu("users"){
<div class="pull-right" style="margin-bottom: 4px;"> <div class="pull-right" style="margin-bottom: 4px;">
<a href="@path/admin/users/_newuser" class="btn btn-default">New User</a> <a href="@context.path/admin/users/_newuser" class="btn btn-default">New User</a>
<a href="@path/admin/users/_newgroup" class="btn btn-default">New Group</a> <a href="@context.path/admin/users/_newgroup" class="btn btn-default">New Group</a>
</div> </div>
<label for="includeRemoved"> <label for="includeRemoved">
<input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/> <input type="checkbox" id="includeRemoved" name="includeRemoved" @if(includeRemoved){checked}/>
@@ -17,14 +16,14 @@
<td @if(account.isRemoved){style="background-color: #dddddd;"}> <td @if(account.isRemoved){style="background-color: #dddddd;"}>
<div class="pull-right"> <div class="pull-right">
@if(account.isGroupAccount){ @if(account.isGroupAccount){
<a href="@path/admin/users/@account.userName/_editgroup">Edit</a> <a href="@context.path/admin/users/@account.userName/_editgroup">Edit</a>
} else { } else {
<a href="@path/admin/users/@account.userName/_edituser">Edit</a> <a href="@context.path/admin/users/@account.userName/_edituser">Edit</a>
} }
</div> </div>
<div class="strong"> <div class="strong">
@avatar(account.userName, 20) @helpers.avatar(account.userName, 20)
<a href="@url(account.userName)">@account.userName</a> <a href="@helpers.url(account.userName)">@account.userName</a>
@if(account.isGroupAccount){ @if(account.isGroupAccount){
(Group) (Group)
} else { } else {
@@ -36,7 +35,7 @@
} }
@if(account.isGroupAccount){ @if(account.isGroupAccount){
@members(account.userName).map { userName => @members(account.userName).map { userName =>
@avatar(userName, 20, tooltip = true) @helpers.avatar(userName, 20, tooltip = true)
} }
} }
</div> </div>
@@ -50,10 +49,10 @@
} }
</div> </div>
<div> <div>
<span class="muted">Registered:</span> @datetime(account.registeredDate) <span class="muted">Registered:</span> @helpers.datetime(account.registeredDate)
<span class="muted">Updated:</span> @datetime(account.updatedDate) <span class="muted">Updated:</span> @helpers.datetime(account.updatedDate)
@if(!account.isGroupAccount){ @if(!account.isGroupAccount){
<span class="muted">Last Login:</span> @account.lastLoginDate.map(datetime) <span class="muted">Last Login:</span> @account.lastLoginDate.map(helpers.datetime)
} }
</div> </div>
</td> </td>
@@ -65,7 +64,7 @@
<script> <script>
$(function(){ $(function(){
$('#includeRemoved').click(function(){ $('#includeRemoved').click(function(){
location.href = '@path/admin/users?includeRemoved=' + this.checked; location.href = '@context.path/admin/users?includeRemoved=' + this.checked;
}); });
}); });
</script> </script>

View File

@@ -2,62 +2,61 @@
closedCount: Int, closedCount: Int,
condition: gitbucket.core.service.IssuesService.IssueSearchCondition, condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
groups: List[String])(implicit context: gitbucket.core.controller.Context) groups: List[String])(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._
<div id="table-issues-control"> <div id="table-issues-control">
@helper.html.dropdown("Visibility"){ @gitbucket.core.helper.html.dropdown("Visibility"){
<li> <li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)"> <a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("private")) @gitbucket.core.helper.html.checkicon(condition.visibility == Some("private"))
Private repository only Private repository only
</a> </a>
</li> </li>
<li> <li>
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)"> <a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)">
@helper.html.checkicon(condition.visibility == Some("public")) @gitbucket.core.helper.html.checkicon(condition.visibility == Some("public"))
Public repository only Public repository only
</a> </a>
</li> </li>
} }
@helper.html.dropdown("Organization"){ @gitbucket.core.helper.html.dropdown("Organization"){
@groups.map { group => @groups.map { group =>
<li> <li>
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)"> <a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
@helper.html.checkicon(condition.groups.contains(group)) @gitbucket.core.helper.html.checkicon(condition.groups.contains(group))
@avatar(group, 20) @group @helpers.avatar(group, 20) @group
</a> </a>
</li> </li>
} }
} }
@helper.html.dropdown("Sort"){ @gitbucket.core.helper.html.dropdown("Sort"){
<li> <li>
<a href="@condition.copy(sort="created", direction="desc").toURL"> <a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest @gitbucket.core.helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a> </a>
</li> </li>
<li> <li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL"> <a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest @gitbucket.core.helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a> </a>
</li> </li>
<li> <li>
<a href="@condition.copy(sort="comments", direction="desc").toURL"> <a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented @gitbucket.core.helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a> </a>
</li> </li>
<li> <li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL"> <a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented @gitbucket.core.helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a> </a>
</li> </li>
<li> <li>
<a href="@condition.copy(sort="updated", direction="desc").toURL"> <a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated @gitbucket.core.helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a> </a>
</li> </li>
<li> <li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL"> <a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated @gitbucket.core.helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a> </a>
</li> </li>
} }

View File

@@ -7,14 +7,12 @@
groups: List[String], groups: List[String],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("Issues"){
@import gitbucket.core.view.helpers._ @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
@html.main("Issues"){ @gitbucket.core.dashboard.html.tab("issues")
@sidebar(recentRepositories, userRepositories){
@dashboard.html.tab("issues")
<div class="container"> <div class="container">
@issuesnavi(filter, openCount, closedCount, condition) @gitbucket.core.dashboard.html.issuesnavi("issues", filter, openCount, closedCount, condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div> </div>
} }
} }

View File

@@ -5,15 +5,14 @@
condition: gitbucket.core.service.IssuesService.IssueSearchCondition, condition: gitbucket.core.service.IssuesService.IssueSearchCondition,
filter: String, filter: String,
groups: List[String])(implicit context: gitbucket.core.controller.Context) groups: List[String])(implicit context: gitbucket.core.controller.Context)
@import context._ @import gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._
@import gitbucket.core.service.IssuesService @import gitbucket.core.service.IssuesService
@import gitbucket.core.service.IssuesService.IssueInfo @import gitbucket.core.service.IssuesService.IssueInfo
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<thead> <thead>
<tr> <tr>
<th style="background-color: #eee;"> <th style="background-color: #eee;">
@dashboard.html.header(openCount, closedCount, condition, groups) @gitbucket.core.dashboard.html.header(openCount, closedCount, condition, groups)
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -21,11 +20,11 @@
@issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) =>
<tr> <tr>
<td style="padding-top: 12px; padding-bottom: 12px;"> <td style="padding-top: 12px; padding-bottom: 12px;">
<a href="@path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65; <a href="@context.path/@issue.userName/@issue.repositoryName">@issue.userName/@issue.repositoryName</a>&nbsp;&#xFF65;
@if(issue.isPullRequest){ @if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a> <a href="@context.path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else { } else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a> <a href="@context.path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
} }
@gitbucket.core.issues.html.commitstatus(issue, commitStatus) @gitbucket.core.issues.html.commitstatus(issue, commitStatus)
@labels.map { label => @labels.map { label =>
@@ -33,20 +32,20 @@
} }
<span class="pull-right muted"> <span class="pull-right muted">
@issue.assignedUserName.map { userName => @issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true) @helpers.avatar(userName, 20, tooltip = true)
} }
@if(commentCount > 0){ @if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count"> <a href="@context.path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount <i class="octicon octicon-comment active"></i> @commentCount
</a> </a>
} else { } else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;"> <a href="@context.path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<i class="octicon octicon-comment"></i> @commentCount <i class="octicon octicon-comment"></i> @commentCount
</a> </a>
} }
</span> </span>
<div class="small muted" style="margin-top: 2px;"> <div class="small muted" style="margin-top: 2px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) #@issue.issueId opened by @helpers.user(issue.openedUserName, styleClass="username") @helpers.datetime(issue.registeredDate)
@milestone.map { milestone => @milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span> <span style="margin: 20px;"><a href="@condition.copy(milestone = Some(Some(milestone))).toURL" class="username"><i class="octicon octicon-milestone"></i> @milestone</a></span>
} }
@@ -64,5 +63,5 @@
</tbody> </tbody>
</table> </table>
<div class="pull-right"> <div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), IssuesService.IssueLimit, 10, condition.toURL) @gitbucket.core.helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), IssuesService.IssueLimit, 10, condition.toURL)
</div> </div>

View File

@@ -1,9 +1,8 @@
@(filter: String, @(active: String,
filter: String,
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context) condition: gitbucket.core.service.IssuesService.IssueSearchCondition)(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
<ul class="nav nav-pills pull-left" style="line-height: 14px; margin-bottom: 10px;"> <ul class="nav nav-pills pull-left" style="line-height: 14px; margin-bottom: 10px;">
<li class="@(if(condition.state == "open"){"active"})"> <li class="@(if(condition.state == "open"){"active"})">
<a href="@condition.copy(state = "open").toURL">Open <span class="badge">@openCount</span></a> <a href="@condition.copy(state = "open").toURL">Open <span class="badge">@openCount</span></a>
@@ -11,15 +10,18 @@
<li class="@(if(condition.state == "closed"){"active"})"> <li class="@(if(condition.state == "closed"){"active"})">
<a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a> <a href="@condition.copy(state = "closed").toURL">Closed <span class="badge">@closedCount</span></a>
</li> </li>
@*
<li class="@if(filter == "created_by"){active}">
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
</li>
<li class="@if(filter == "assigned"){active}">
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
</li>
<li class="@if(filter == "mentioned"){active}">
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</li>
*@
</ul> </ul>
<div class="btn-group pull-right" data-toggle="buttons">
<a class="switch btn btn-default @if(filter == "created_by"){active}" href="@context.path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
<a class="switch btn btn-default @if(filter == "assigned" ){active}" href="@context.path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
<a class="switch btn btn-default @if(filter == "mentioned" ){active}" href="@context.path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
</div>
<script>
$(function(){
$('a.switch').click(function(){
location.href = $(this).attr('href');
});
})
</script>

View File

@@ -7,14 +7,12 @@
groups: List[String], groups: List[String],
recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context) userRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo])(implicit context: gitbucket.core.controller.Context)
@import context._ @gitbucket.core.html.main("Pull Requests"){
@import gitbucket.core.view.helpers._ @gitbucket.core.dashboard.html.sidebar(recentRepositories, userRepositories){
@html.main("Pull Requests"){ @gitbucket.core.dashboard.html.tab("pulls")
@sidebar(recentRepositories, userRepositories){
@dashboard.html.tab("pulls")
<div class="container"> <div class="container">
@issuesnavi(filter, openCount, closedCount, condition) @gitbucket.core.dashboard.html.issuesnavi("pulls", filter, openCount, closedCount, condition)
@issueslist(issues, page, openCount, closedCount, condition, filter, groups) @gitbucket.core.dashboard.html.issueslist(issues, page, openCount, closedCount, condition, filter, groups)
</div> </div>
} }
} }

View File

@@ -1,72 +1,65 @@
@(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo], @(recentRepositories: List[gitbucket.core.service.RepositoryService.RepositoryInfo],
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 gitbucket.core.view.helpers
@import gitbucket.core.view.helpers._ <div class="main-sidebar">
<div class="container body"> <div class="sidebar">
<div class="dashboard-sidebar"> <ul class="nav sidebar-menu">
@if(loginAccount.isEmpty){ @if(context.loginAccount.isDefined){
<div id="dashboard-signin-form">@html.signinform(settings)</div> <li class="header">
<span class="label label-primary pull-right">@userRepositories.size</span>
Your repositories
</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 == context.loginAccount.get.userName){
<ul class="list-group list-group-flush"> <a href="@helpers.url(repository)">@gitbucket.core.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="@helpers.url(repository)">@gitbucket.core.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="@helpers.url(repository)">@gitbucket.core.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

@@ -1,14 +1,12 @@
@(active: String = "")(implicit context: gitbucket.core.controller.Context) @(active: String = "")(implicit context: gitbucket.core.controller.Context)
@import context._
@import gitbucket.core.view.helpers._
<ul class="nav nav-tabs" style="margin-bottom: 20px;"> <ul class="nav nav-tabs" style="margin-bottom: 20px;">
<li @if(active == ""){ class="active"}><a href="@path/">News Feed</a></li> <li @if(active == ""){ class="active"}><a href="@context.path/">News Feed</a></li>
@if(loginAccount.isDefined){ @if(context.loginAccount.isDefined){
<li @if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li> <li @if(active == "pulls" ){ class="active"}><a href="@context.path/dashboard/pulls">Pull Requests</a></li>
<li @if(active == "issues"){ class="active"}><a href="@path/dashboard/issues">Issues</a></li> <li @if(active == "issues"){ class="active"}><a href="@context.path/dashboard/issues">Issues</a></li>
@gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab => @gitbucket.core.plugin.PluginRegistry().getDashboardTabs.map { tab =>
@tab(context).map { link => @tab(context).map { link =>
<li @if(active == link.id){ class="active"}><a href="@path/@link.path">@link.label</a></li> <li @if(active == link.id){ class="active"}><a href="@context.path/@link.path">@link.label</a></li>
} }
} }
} }

View File

@@ -1,4 +1,4 @@
@(title: String)(implicit context: gitbucket.core.controller.Context) @(title: String)(implicit context: gitbucket.core.controller.Context)
@main("Error"){ @gitbucket.core.html.main("Error"){
<h1>@title</h1> <h1>@title</h1>
} }

View File

@@ -0,0 +1,7 @@
@(repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
<!DOCTYPE html>
<html>
<head>
<meta name="go-import" content="@context.baseUrl.replaceFirst("^https?://", "")/@repository.owner/@repository.name git @repository.httpUrl" />
</head>
</html>

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