Compare commits

..

204 Commits
4.14 ... 4.17.0

Author SHA1 Message Date
Naoki Takezoe
a51b57af2a Update README.md 2017-09-24 14:23:43 +09:00
Naoki Takezoe
17ff024166 Update README.md 2017-09-24 14:14:00 +09:00
Naoki Takezoe
ed0c8e3f2c Bump gitbucket-notifications-plugin to 1.2.0 2017-09-24 14:00:33 +09:00
Naoki Takezoe
6b762b0693 Fix version as 4.17.0 2017-09-24 12:34:00 +09:00
Naoki Takezoe
62e6d0d6e8 (refs #1722)Bump markedj to 1.0.15 2017-09-24 12:30:44 +09:00
Naoki Takezoe
6947f57bd8 Merge pull request #1719 from gitbucket/drop-file-upload-limitation
Drop uploadable file type limitation
2017-09-22 01:18:19 +09:00
Naoki Takezoe
08295afb51 (refs #1714)Drop uploadable file type limitation 2017-09-21 16:51:15 +09:00
Naoki Takezoe
8d5b494785 Update doc for JRebel 2017-09-21 16:09:52 +09:00
Naoki Takezoe
8d52fc06ed Update doc for JRebel 2017-09-21 16:01:07 +09:00
Naoki Takezoe
85b83af73f Merge pull request #1717 from gitbucket/refuse-delete-default-branch
Refuse deletion of the default branch
2017-09-21 02:20:29 +09:00
Naoki Takezoe
d99898e191 Implement checking whether a deleting branch is the default branch 2017-09-21 02:08:19 +09:00
Naoki Takezoe
2044f5b838 Refuse deletion of the default branch 2017-09-20 17:46:53 +09:00
Naoki Takezoe
bb804f6597 Merge pull request #1696 from gitbucket/api_repository_ssh_url
Add ssh_url to web hook request and API response
2017-09-20 16:18:35 +09:00
Naoki Takezoe
4d3756ac0a Merge pull request #1712 from kounoike/pr-fix-1701
Allow anonymous access to github style git url redirect. fix #1701
2017-09-19 09:02:28 +09:00
Naoki Takezoe
0739b3048b Merge pull request #1713 from kounoike/pr-gc-call
repo > Settings > Danger Zone > Garbage collection doesn't executed.
2017-09-18 17:21:29 +09:00
KOUNOIKE Yuusuke
ff5a05511d git.gc() doesn't called. 2017-09-18 14:38:14 +09:00
KOUNOIKE Yuusuke
186ce769a2 allow anonymous access to git redirect. fix #1701 2017-09-18 14:03:04 +09:00
Naoki Takezoe
848c698491 Update sbt plugins 2017-09-18 02:18:28 +09:00
Naoki Takezoe
41ea2087d1 Fix guideline to accept any languages 2017-09-17 21:30:46 +09:00
Naoki Takezoe
1d77727867 Merge pull request #1711 from gitbucket/improve-mail-api
Make Mailer API more general
2017-09-17 01:32:16 +09:00
Naoki Takezoe
051d059e5c Improve Mailer API 2017-09-17 00:49:10 +09:00
Naoki Takezoe
324107beef Change the setting icon 2017-09-16 11:06:44 +09:00
Naoki Takezoe
801d71b6d2 Update Mailer.send() signature 2017-09-16 03:11:40 +09:00
Naoki Takezoe
106f7a41d8 Add Mailer.send() method without login account 2017-09-15 20:57:50 +09:00
Naoki Takezoe
1b46651c32 Merge pull request #1709 from kounoike/PR-keep-hash-at-signin
Keep hash when sign in.
2017-09-15 00:11:22 +09:00
KOUNOIKE Yuusuke
459b25e075 Change hash field to Option[String]. 2017-09-14 21:42:29 +09:00
Naoki Takezoe
e3c3a61f0b Fix NullPointerException in the first run from source code 2017-09-14 11:29:41 +09:00
KOUNOIKE Yuusuke
a311ee5ef5 Keep hash when sign in. fix #1706 2017-09-14 04:48:31 +09:00
Naoki Takezoe
b13decf0e9 (refs #1702)Implement keyboard shortcut "y"
which transfers a browser to URL with commit id.
2017-09-13 14:10:00 +09:00
Naoki Takezoe
f6b92ef40b Fix style of buttons 2017-09-13 03:36:04 +09:00
Naoki Takezoe
919b1d01e3 Fix labels and priorities styles 2017-09-13 03:23:15 +09:00
Naoki Takezoe
fe73c11611 Fix styles 2017-09-12 20:57:35 +09:00
Naoki Takezoe
c8af6c4b5a Fix styles 2017-09-12 20:54:15 +09:00
Naoki Takezoe
5d2d36dccf Fix styles 2017-09-12 20:48:37 +09:00
Naoki Takezoe
a11f711778 Merge pull request #1703 from kounoike/pr-switch-to-commitid-url
Change URL with commit ID when user selects line(s).
2017-09-12 18:52:28 +09:00
Naoki Takezoe
6920704caa Fix pull request title edit button 2017-09-12 16:19:57 +09:00
Naoki Takezoe
451a6ef359 Fix button style 2017-09-12 11:58:48 +09:00
KOUNOIKE Yuusuke
a93f4cc780 Transfer to URL with commit ID when select line(s). 2017-09-12 11:24:13 +09:00
Naoki Takezoe
f9fda26e7a Add RepositoryService#hasOwnerRole() 2017-09-11 02:27:22 +09:00
Naoki Takezoe
7435902b70 Generate submodule link only when url starts with http:// or https:// 2017-09-07 02:15:10 +09:00
Naoki Takezoe
feb57c97b9 Bump to 4.17.0-SNAPSHOT 2017-09-06 15:34:09 +09:00
Naoki Takezoe
7021942a6e Merge branch 'add_assignee_to_pr_api' 2017-09-05 20:37:03 +09:00
Naoki Takezoe
9251d64de8 Avoid database access in model
Modified to pass assignee from outside of model instead.
2017-09-05 19:47:41 +09:00
Naoki Takezoe
d0c99727e9 Fix testcases 2017-09-05 16:06:06 +09:00
Naoki Takezoe
1fbfcfb446 Add ssh_url to API response 2017-09-05 15:14:54 +09:00
Naoki Takezoe
38328b2ffe What charset should be used to make patch? 2017-09-05 12:02:23 +09:00
Naoki Takezoe
3cce4e5308 Merge pull request #1670 from gitbucket/feature/get-single-commit-api
Get single commit API
2017-09-05 11:57:04 +09:00
Naoki Takezoe
757292d670 Fix response of get single commit API
Fix response of get single commit API
2017-09-05 11:41:43 +09:00
Naoki Takezoe
ef16804b49 Make patch from DiffEntry 2017-09-05 11:27:59 +09:00
Naoki Takezoe
dafabb6278 Fix import 2017-09-05 09:31:45 +09:00
Naoki Takezoe
1f37362da4 Merge branch 'master' into feature/get-single-commit-api
# Conflicts:
#	src/main/scala/gitbucket/core/controller/ApiController.scala
2017-09-05 09:28:36 +09:00
Naoki Takezoe
1dba28d153 (refs #1643)Create issue reference comment
even if it can't relate committer’s email with GitBucket account
2017-09-04 19:17:25 +09:00
Naoki Takezoe
e83b017ef2 (refs #1695)Fix plugin controllers mapping 2017-09-04 18:46:07 +09:00
Naoki Takezoe
eeabbfd599 Fix plugin reloading from PluginWatchThread 2017-09-03 22:36:03 +09:00
Naoki Takezoe
55a8602bba (#1685)Call PullRequestHook.addedComment for PR code comment 2017-09-03 04:39:20 +09:00
Naoki Takezoe
2e80e3baaf Fix testcase 2017-09-03 03:52:00 +09:00
Naoki Takezoe
3134bb0428 (refs #1687)Bump notification plugin to 1.1.0 2017-09-02 13:43:44 +09:00
Naoki Takezoe
134b5df6a2 (refs #1675)Drop debug option because it can't be configure at settings form 2017-09-02 13:43:21 +09:00
Naoki Takezoe
aab84d9275 Update README.md 2017-09-02 01:35:13 +09:00
Naoki Takezoe
5209c29d0f Update README.md 2017-09-02 01:32:34 +09:00
Naoki Takezoe
cc975f8ffa Release 4.16.0 2017-09-02 01:30:06 +09:00
Naoki Takezoe
125040f90b (refs #1688)Fix regex which is used to extract user and repository name from URL 2017-09-01 00:58:49 +09:00
Naoki Takezoe
7009aaeb24 Merge pull request #1675 from kounoike/pr-handle-errors
Handle errors to show errors prettify and logging it.
2017-08-29 01:26:21 +09:00
Naoki Takezoe
a135c6977f Merge pull request #1683 from int128/fix-api-branch-with-slash
Fix branch API did not accept branch name with slash
2017-08-29 01:23:18 +09:00
Naoki Takezoe
55991a6f17 Merge pull request #1690 from uli-heller/blocking-slick-32-0.0.10
blocking-slick-32: 0.0.9 -> 0.0.10
2017-08-29 01:20:18 +09:00
Naoki Takezoe
17e1de6174 Bump to 4.16.0-SNAPSHOT 2017-08-28 19:24:06 +09:00
Uli Heller
12a58f393e blocking-slick-32: 0.0.9 -> 0.0.10 2017-08-25 08:18:59 +02:00
Hidetake Iwata
ebb57a80e3 Fix branch API did not accept branch name with slash 2017-08-21 17:58:22 +09:00
Naoki Takezoe
7a53bd8766 Merge pull request #1672 from kounoike/pr-support-textmsg-in-email
Add support to MIME Text part in Email notification.
2017-08-21 01:04:00 +09:00
Naoki Takezoe
d8b46b194d Merge branch 'master' into pr-support-textmsg-in-email 2017-08-20 23:39:05 +09:00
Yuusuke KOUNOIKE
5db7d863ff Merge branch 'master' into pr-handle-errors 2017-08-20 22:28:34 +09:00
Naoki Takezoe
7d7c11aa1a Merge AnonymousAccessController and GitHubCompatibleAccessController 2017-08-20 22:18:36 +09:00
Naoki Takezoe
95748a2f2f Merge pull request #1678 from int128/github-compat-url
Improve GitHub compatible URL
2017-08-20 22:02:58 +09:00
KOUNOIKE Yuusuke
e77045abe3 Merge remote-tracking branch 'upstream/master' into pr-handle-errors
# Conflicts:
#	src/main/scala/gitbucket/core/controller/SystemSettingsController.scala
#	src/main/scala/gitbucket/core/service/SystemSettingsService.scala
#	src/test/scala/gitbucket/core/view/AvatarImageProviderSpec.scala
2017-08-20 21:50:34 +09:00
Naoki Takezoe
d73ddbdbcb Merge branch 'master' into github-compat-url 2017-08-20 21:41:10 +09:00
Naoki Takezoe
8893a4a456 Merge pull request #1681 from gitbucket/drop-jdk9-build
Drop JDK9 build
2017-08-20 21:40:21 +09:00
Naoki Takezoe
608112ce22 Drop JDK9 build 2017-08-20 21:29:36 +09:00
Naoki Takezoe
2e9155fbcc Merge branch 'master' into pr-support-textmsg-in-email 2017-08-19 01:59:14 +09:00
Naoki Takezoe
f33a30c697 Merge pull request #1676 from kounoike/pr-skin-selectable
add AdminLTE skin selection feature.
2017-08-18 20:15:22 +09:00
Hidetake Iwata
e208dd5966 Implement as controller instead of filter 2017-08-18 18:56:01 +09:00
KOUNOIKE Yuusuke
a6cb71f9c3 remove helpers.skinName. (by review comment) 2017-08-18 01:55:56 +09:00
Naoki Takezoe
91deeb969b Merge branch 'master' into pr-skin-selectable 2017-08-18 01:00:20 +09:00
KOUNOIKE Yuusuke
040d812f2a add debug option to SystemSettings. and error page shows stacktrace only for admin or debug settings. 2017-08-17 17:48:53 +09:00
Hidetake Iwata
772ac80764 Improve GitHub compatible URL 2017-08-16 10:09:08 +09:00
Yuusuke KOUNOIKE
ef67c94272 Merge branch 'master' into pr-support-textmsg-in-email 2017-08-15 03:26:17 +09:00
KOUNOIKE Yuusuke
fdb4a6bdc6 change interface to textMsg: String, htmlMsg: Option[String].
SystemSettingsController calls Mailer.send directly. fix it too.
2017-08-15 03:25:27 +09:00
Naoki Takezoe
57902af87c (refs #1553)Fixup 2017-08-15 03:25:27 +09:00
KOUNOIKE Yuusuke
92fea3ff01 fix test. 2017-08-15 02:44:52 +09:00
Naoki Takezoe
cbd16169e4 (refs #1553)Fixup 2017-08-15 02:02:17 +09:00
KOUNOIKE Yuusuke
299df34bf4 add AdminLTE skin selection feature. 2017-08-14 22:12:17 +09:00
KOUNOIKE Yuusuke
48a92df719 Handle errors to show errors prettify and logging it. 2017-08-14 18:50:02 +09:00
shimamoto
b806439e2f Merge branch 'master' into pr-support-textmsg-in-email 2017-08-14 18:45:34 +09:00
Naoki Takezoe
1db586c0bd Merge pull request #1671 from kounoike/pr-send-comment-at-merged
Call addedComment hook(send notification) at PR merged.
2017-08-14 18:08:57 +09:00
KOUNOIKE Yuusuke
26e2bfbf43 Call addedComment hook(send notification) at PR merged. 2017-08-14 14:02:30 +09:00
KOUNOIKE Yuusuke
28c4ac6a19 Add support to MIME Text part. 2017-08-14 13:25:25 +09:00
Naoki Takezoe
c0d9d68fca Merge pull request #1553 from kounoike/pr-show-commitstaus-in-commits
Show CommitStatus in commits page.
2017-08-14 12:01:40 +09:00
Yuusuke KOUNOIKE
122ed1dd0f Merge branch 'master' into pr-show-commitstaus-in-commits 2017-08-14 01:14:49 +09:00
Naoki Takezoe
f18b83999a Merge remote-tracking branch 'origin/master' 2017-08-13 01:59:17 +09:00
Naoki Takezoe
fc28aacb52 (refs #1664)Bump to markedj 1.0.14 2017-08-13 01:59:00 +09:00
Naoki Takezoe
efdfe2b1b5 Implementing get single commit API 2017-08-13 01:15:18 +09:00
Naoki Takezoe
2872da4f94 Merge pull request #1669 from kounoike/pr-fix-1552
Add query string for redirect, required by git-2.11-1.
2017-08-12 20:34:03 +09:00
KOUNOIKE Yuusuke
5d3c5e7f3c Add query string for redirect, it required by git-2.12. close #1552 2017-08-12 15:16:55 +09:00
Naoki Takezoe
a86fb480b2 Merge pull request #1668 from gitbucket/thread-safe-collection
Improve concurrency of initializaton of the plugin system
2017-08-12 04:45:59 +09:00
Naoki Takezoe
7daf33c149 Merge branch 'master' into thread-safe-collection 2017-08-12 04:37:14 +09:00
Naoki Takezoe
0d9afdc939 (refs #1664)Bump to markedj 1.0.14-SNAPSHOT 2017-08-12 03:46:47 +09:00
Naoki Takezoe
a9a26193cd Improve shutdown of the plugin system 2017-08-12 03:38:07 +09:00
Naoki Takezoe
684973ea85 Merge pull request #1666 from HairyFotr/patch-8
Fix lint and typos
2017-08-09 08:18:55 +09:00
HairyFotr
0149593272 Fix lint and typos 2017-08-08 21:37:35 +02:00
Naoki Takezoe
1ae3df76bd Use thread-safe collection 2017-08-07 17:53:32 +09:00
Naoki Takezoe
d8e0a06d93 Add the support guidelines 2017-08-06 14:07:40 +09:00
Naoki Takezoe
b2d842ddd0 (refs #1654)Add --validate_password option 2017-08-06 03:49:04 +09:00
Naoki Takezoe
580374208f Merge remote-tracking branch 'origin/master' 2017-08-06 03:38:25 +09:00
Naoki Takezoe
8ab96f09cb Update ISSUES.UPDATED_DATE column when issue attriibutes are updated 2017-08-06 03:38:22 +09:00
Naoki Takezoe
f5b6728358 Merge pull request #1659 from kounoike/pr-api-pr-merged
fix getMergedComment for PR API merged field.
2017-08-06 03:20:12 +09:00
Naoki Takezoe
c7d46ee18f Merge branch 'master' into pr-api-pr-merged 2017-08-06 02:59:51 +09:00
Naoki Takezoe
eafdd0fb61 Update README.md 2017-08-05 11:45:45 +09:00
Naoki Takezoe
a14394dd88 Gist, Emoji and Pages plugin are disabled in default 2017-07-31 11:59:19 +09:00
Naoki Takezoe
e28b0394ec Update README.md 2017-07-31 09:35:43 +09:00
Naoki Takezoe
11903e9728 Update README.md 2017-07-31 09:34:04 +09:00
Naoki Takezoe
0e498d1a81 Add a special rule for pages-plugin's url 2017-07-31 01:29:13 +09:00
KOUNOIKE Yuusuke
f073112814 fixup 2017-07-30 13:52:27 +09:00
KOUNOIKE Yuusuke
9ee71e9f25 fix getMergedComment for PR API merged field. 2017-07-30 11:39:36 +09:00
kenji yoshida
0aafc16648 Merge pull request #1657 from uli-heller/scala-2.12.3
scala-2.12.3
2017-07-29 16:59:09 +09:00
Uli Heller
58246a91b0 scala-2.12.3 2017-07-28 11:51:42 +02:00
Naoki Takezoe
53ae59271a Add pages plugin 2017-07-27 07:34:42 +09:00
Naoki Takezoe
1a2e4e72bd Update bundle plugin information 2017-07-26 00:53:23 +09:00
Naoki Takezoe
2a83c1b9ba Add null check 2017-07-25 12:35:41 +09:00
Naoki Takezoe
5b245978d4 Update README.md 2017-07-25 02:14:00 +09:00
Naoki Takezoe
1463cee2a4 Log bundle plugins downloading 2017-07-24 20:29:20 +09:00
Naoki Takezoe
f4b910c268 Download bundle plugins from plugins.json 2017-07-24 20:26:30 +09:00
Naoki Takezoe
d39c371635 Bundling plugins is executed in packaging of the executable war file 2017-07-24 14:26:45 +09:00
Naoki Takezoe
19b3c2a265 Update bundled plugins 2017-07-23 01:05:43 +09:00
Naoki Takezoe
28efc38fc4 Fixup 2017-07-22 16:00:51 +09:00
Naoki Takezoe
6a7e948e89 Tweak copy button style 2017-07-22 15:40:35 +09:00
Naoki Takezoe
645d23b531 (refs #1635)Fix scrollbar issue 2017-07-22 15:08:26 +09:00
Naoki Takezoe
50ae5bb7cc Bump to blocking-slick 0.0.9 (Slick 3.2.1) 2017-07-22 02:36:39 +09:00
Naoki Takezoe
38728910cb Merge pull request #1652 from aruneko/support_ed25519_key
support ed25519 key
2017-07-22 01:42:04 +09:00
Aruneko
e2f695777d support ed25519 key 2017-07-21 22:36:03 +09:00
Naoki Takezoe
06a98d0f94 (refs #1644)Bump markedj to 1.0.13 2017-07-19 03:16:04 +09:00
Naoki Takezoe
944cbf04ed Enhance --plugin_dir option behavior
It became an option to specify an extra plugin jars directory, not to overwrite PLUGIN_HOME.
2017-07-18 13:04:32 +09:00
Naoki Takezoe
84891abc04 Merge pull request #1648 from gitbucket/travis-java9-build
Test for oraclejdk9 build on Travis
2017-07-18 01:37:03 +09:00
Naoki Takezoe
a848bb43b6 Scraping JDK file name 2017-07-18 01:13:49 +09:00
Naoki Takezoe
d57a2e5eae Test for oraclejdk9 build 2017-07-18 00:48:51 +09:00
Naoki Takezoe
d1adcb876d Merge pull request #1647 from gitbucket/feature/preview-comment-edit
Make preview-able comment editing forms
2017-07-17 23:15:12 +09:00
Naoki Takezoe
8505d8ae0e Change position of buttons in comment editing forms 2017-07-17 22:44:53 +09:00
Naoki Takezoe
3a567cb4a7 Make preview-able comment editing form 2017-07-17 22:35:53 +09:00
Naoki Takezoe
20dbba116a Bump gist-plugin to 4.9.1 2017-07-17 19:52:42 +09:00
Naoki Takezoe
f7d7b5bd7b Call prettyprint() after issue content editing 2017-07-17 14:47:30 +09:00
Naoki Takezoe
dd15420f2c (refs #1644)Bump markedj to 1.0.13-SNAPSHOT 2017-07-17 14:43:05 +09:00
Naoki Takezoe
31945533c2 (refs #1645)Ignore tagged object is not commit 2017-07-17 03:07:54 +09:00
shimamoto
9288e0abe0 Remove notification.md (move to plugins). 2017-07-13 20:35:09 +09:00
Naoki Takezoe
5641fee39a Fix plugin controller handling 2017-07-13 01:52:26 +09:00
Naoki Takezoe
db88458a14 Remove unnecessary semicolon 2017-07-12 18:42:14 +09:00
Naoki Takezoe
3ff89bc648 (refs #1484)Add --plugin_dir option 2017-07-12 18:33:43 +09:00
Naoki Takezoe
61f3d2d513 Fixup 2017-07-12 13:07:41 +09:00
Naoki Takezoe
788f253ad0 Merge pull request #1487 from gitbucket/feature/plugin-hotdeploy
Plugin hot deployment and bundle some plugins
2017-07-12 11:20:27 +09:00
Naoki Takezoe
947d93ddc7 Remove extra information from plugins.json 2017-07-09 02:31:13 +09:00
Naoki Takezoe
74063885b1 Don't check semver for unmanaged plugins 2017-07-09 01:55:00 +09:00
Naoki Takezoe
554fd6d700 Add emoji plugin 2017-07-09 01:49:57 +09:00
Naoki Takezoe
1fb6861565 Add notifications plugin 2017-07-08 20:01:16 +09:00
Naoki Takezoe
6c5350a51b Drop issue and pull request notification because it will be provided as plugin 2017-07-08 19:37:48 +09:00
Naoki Takezoe
00da7e9a82 Merge branch 'master' into feature/plugin-hotdeploy 2017-07-08 14:05:27 +09:00
Naoki Takezoe
e18bed12c0 Update version to 4.15.0-SNAPSHOT 2017-07-08 14:04:50 +09:00
Naoki Takezoe
d2bb7e912f Merge pull request #1638 from gitbucket/feature/repository-header-plugin
Add repositoryHeaderComponent extension point
2017-07-08 14:00:15 +09:00
Naoki Takezoe
73ed69a4ad Add repositoryHeaderComponent extension point 2017-07-08 13:28:44 +09:00
Naoki Takezoe
d8fe6a0a55 Capability of installing from the local repository 2017-07-07 11:47:43 +09:00
Naoki Takezoe
b278bfd159 (refs #1633)Bugfix for --max_file_size parameter 2017-07-07 01:54:20 +09:00
Naoki Takezoe
872beb777f Merge pull request #1634 from kazuki43zoo/gh-1629_add-repo-link
Add repo-link class to repository link at sidebar menu
2017-07-06 13:48:53 +09:00
Naoki Takezoe
aebcf5d183 Make possible to install bundled plugins in default
and use JSON for defining bundled plugin metadata
2017-07-06 03:25:06 +09:00
Kazuki Shimizu
aab9b71901 Add repo-link class to repository link at sidebar menu
Fixes #1629
2017-07-06 00:04:41 +09:00
Naoki Takezoe
9cbab137fc Make possible to install and uninstall bundled plugins 2017-07-05 18:01:03 +09:00
Naoki Takezoe
358bc23931 Fix compilation error 2017-07-05 16:58:28 +09:00
Naoki Takezoe
7396bf0675 Merge branch 'feature/bundle-plugins' into feature/plugin-hotdeploy 2017-07-05 16:42:20 +09:00
Naoki Takezoe
61166c4388 Add enabled flag to plugin info list 2017-07-05 16:40:35 +09:00
Naoki Takezoe
4b9f2c7728 Merge pull request #1628 from uli-heller/jgit-4.8.0.201706111038-r
Updated to jgit-4.8.0.201706111038-r
2017-07-05 10:57:00 +09:00
Naoki Takezoe
6b496bdef2 Merge branch 'master' into jgit-4.8.0.201706111038-r 2017-07-05 02:18:39 +09:00
Naoki Takezoe
0e795f58dd Remove unnecessary semicolon 2017-07-05 02:17:37 +09:00
Naoki Takezoe
e2ac8e29fe Adopt latest version if different versions of same plugin are found 2017-07-05 02:17:02 +09:00
Naoki Takezoe
bc80adc412 Merge branch 'master' into feature/bundle-plugins 2017-07-05 02:00:28 +09:00
Naoki Takezoe
2f634625ea Copy only LFS dir and insert default priorities in forking repository 2017-07-05 00:36:24 +09:00
Naoki Takezoe
d80774d8d0 Release 4.14.1 2017-07-05 00:36:12 +09:00
Naoki Takezoe
ecf3e97518 (refs #1631)Copy repository files directory only if it exists 2017-07-04 23:52:39 +09:00
Naoki Takezoe
3758d1f5ad Fix typo 2017-07-04 23:49:56 +09:00
Uli Heller
3e53008d35 Updated to jgit-4.8.0.201706111038-r 2017-07-04 06:45:48 +02:00
Naoki Takezoe
afde5c2685 (refs #1625)Copy bundled plugins in initialization process 2017-07-02 13:16:18 +09:00
Naoki Takezoe
224c355d44 (refs #1575)Use MaridDB JDBC driver instead of MySQL driver
to solve license issue
2017-07-02 04:53:57 +09:00
Naoki Takezoe
269718bfa6 Merge branch 'kounoike-pr-svg-logo' 2017-07-02 04:32:42 +09:00
KOUNOIKE Yuusuke
d145fdbb23 use svg icon for top-left logo. 2017-07-01 09:32:06 +09:00
KOUNOIKE Yuusuke
a60848b16c add svg icon to solve jaggy image in IE11. it also closes #747 (gray logo) 2017-07-01 09:32:06 +09:00
Yasuhiro Takagi
884fc5318a Add assignee entry for the result of pull request related api 2017-06-25 13:33:27 +09:00
Naoki Takezoe
45db917ee7 Create plugins directory if it does not exist 2017-05-02 15:27:07 +09:00
Naoki Takezoe
42494ce58a Improve logging 2017-05-02 15:22:25 +09:00
Naoki Takezoe
93c75a3ffd Retry copying plugin jar file if it can't get write lock
to avoid copying during file is creating or updating.
2017-05-02 15:03:47 +09:00
Naoki Takezoe
b9283fb544 Merge branch 'master' into feature/plugin-hotdeploy 2017-05-02 14:21:53 +09:00
KOUNOIKE Yuusuke
68d090f81a Show CommitStatus in commits page. 2017-04-22 21:17:37 +09:00
Naoki Takezoe
f3271846ea Merge branch 'master' into feature/plugin-hotdeploy 2017-03-23 20:45:52 +09:00
Naoki Takezoe
d39d6691e6 Added Plugin.uninstall() method to cleanup, but remove for now 2017-03-23 20:39:33 +09:00
Naoki Takezoe
832b33f949 Fix error message 2017-03-23 20:07:33 +09:00
Naoki Takezoe
1373a93c75 Check plugin duplication 2017-03-23 20:06:07 +09:00
Naoki Takezoe
b2773ff5b7 Load plugin classes from the copied jar file 2017-03-12 02:32:21 +09:00
Naoki Takezoe
8ee017c3fa Add uninstall plugin button 2017-03-11 22:59:55 +09:00
Naoki Takezoe
11fccf38a6 Automatic plugin reloading 2017-03-11 18:01:43 +09:00
Naoki Takezoe
7e7e45e794 Add the plugin reload button to the plugin list page 2017-03-11 12:13:59 +09:00
Naoki Takezoe
c760af7810 Add adapter filter for plugin controllers 2017-03-11 11:19:57 +09:00
110 changed files with 1708 additions and 949 deletions

View File

@@ -1,7 +1,6 @@
# Guideline for Issues
# The guidelines for contributing
- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- We can also support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- Write an issue in English. At least, write subject in English.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues and pull requests whether there is a same request in the past.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles. If you don't wanna waste your time to make a pull request, ask us about your idea at [gitter room](https://gitter.im/gitbucket/gitbucket) before staring your work.
- You can edit the GitBucket documentation on Wiki if you have a GitHub account. When you find any mistakes or lacks in the documentation, please update it directly.
- All your contributions are handled as [Apache Software License, Version 2.0](https://github.com/gitbucket/gitbucket/blob/master/LICENSE). When you create a pull request or update the documentation, we assume you agreed this clause.

View File

@@ -1,4 +1,4 @@
### Before submitting an issue to Gitbucket I have first:
### Before submitting an issue to GitBucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] searched for similar already existing issue
@@ -9,11 +9,10 @@
## Issue
**Impacted version**: xxxx
**Deployment mode**: *explain here how you use gitbucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
**Deployment mode**: *explain here how you use GitBucket : standalone app, under webcontainer (which one), with an http frontend (nginx, httpd, ...)*
**Problem description**:
- *be as explicit has you can*
- *describe the problem and its symptoms*
- *explain how to reproduce*
- *attach whatever information that can help understanding the context (screen capture, log files)*
- *do your best to use a correct english (re-read yourself)*

View File

@@ -1,4 +1,4 @@
### Before submitting a pull-request to Gitbucket I have first:
### Before submitting a pull-request to GitBucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/.github/CONTRIBUTING.md)
- [] rebased my branch over master

5
.github/SUPPORT.md vendored Normal file
View File

@@ -0,0 +1,5 @@
# The support guidelines
- At first, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- Write issues in English if it's possible. It enables many of contributors to help you.

View File

@@ -1,5 +1,7 @@
language: scala
sudo: true
jdk:
- oraclejdk8
script:
- sbt test
before_script:
@@ -14,33 +16,3 @@ cache:
- $HOME/.coursier
- $HOME/.embedmysql
- $HOME/.embedpostgresql
matrix:
include:
- jdk: oraclejdk8
addons:
apt:
packages:
- libaio1
- dist: trusty
group: edge
sudo: required
jdk: oraclejdk9
addons:
apt:
packages:
- libaio1
- oracle-java9-installer
script:
# https://github.com/sbt/sbt/pull/2951
- git clone https://github.com/retronym/java9-rt-export
- cd java9-rt-export/
- git checkout 1019a2873d057dd7214f4135e84283695728395d
- jdk_switcher use oraclejdk8
- sbt package
- jdk_switcher use oraclejdk9
- java -version
- mkdir -p $HOME/.sbt/0.13/java9-rt-ext; java -jar target/java9-rt-export-*.jar $HOME/.sbt/0.13/java9-rt-ext/rt.jar
- jar tf $HOME/.sbt/0.13/java9-rt-ext/rt.jar | grep java/lang/Object
- cd ..
- wget https://raw.githubusercontent.com/paulp/sbt-extras/9ade5fa54914ca8aded44105bf4b9a60966f3ccd/sbt && chmod +x ./sbt
- ./sbt -Dscala.ext.dirs=$HOME/.sbt/0.13/java9-rt-ext test

View File

@@ -57,25 +57,46 @@ GitBucket has a plug-in system that allows extra functionality. Officially the f
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
- [gitbucket-emoji-plugin](https://github.com/gitbucket/gitbucket-emoji-plugin)
- [gitbucket-pages-plugin](https://github.com/gitbucket/gitbucket-pages-plugin)
- [gitbucket-notifications-plugin](https://github.com/gitbucket/gitbucket-notifications-plugin)
You can find more plugins made by the community at [GitBucket community plugins](http://gitbucket-plugins.github.io/).
You can find more plugins made by the community at [GitBucket community plugins](https://gitbucket-plugins.github.io/).
Support
--------
- If you have any questions about GitBucket, see [Wiki](https://github.com/gitbucket/gitbucket/wiki) and check issues whether there is a same question or request in the past.
- If you can't find same question and report, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raising an issue.
- We can also provide support in Japanese other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- Write an issue in English. At least, write subject in English.
- The highest priority of GitBucket is the ease of installation and API compatibility with GitHub, so your feature request might be rejected if they go against those principles.
Release Notes
-------------
### 4.14 - 1 Jul 2017
### 4.17.0 - 30 Sep 2017
- [gitbucket-ci-plugin](https://github.com/takezoe/gitbucket-ci-plugin) is available
- Transferring to URL with commit ID
- Drop uploadable file type limitation
- Improve Mailer API
- Web API and webhook enhancement
### 4.16.0 - 2 Sep 2017
- Support AdminLTE color skin
- Improve unexpected error handling
- Show commit status on the commits list
### 4.15.0 - 5 Aug 2017
- Bundle GitBucket organization plugins
- Notifications plugin
- Plugin hot deployment
- Update Slick to 3.2.1 from 3.2.0
- Support ed25519 keys for SSH
- Markdown preview in comment editing forms
### 4.14.1 - 4 Jul 2017
- Bug fix: Possibility of error in forking repository
### 4.14 - 1 Jul 2017
- Support priority in issues and pull requests
- Show icons when the sidebar is collapsed
- Support gollumn events in web hook
- Support gollum events in web hook
- Support account (user / group) level web hook
- Add `--max_file_size` option
- Configuration by system property or environment variable

View File

@@ -1,6 +1,6 @@
val Organization = "io.github.gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "4.14.0"
val GitBucketVersion = "4.17.0"
val ScalatraVersion = "2.5.0"
val JettyVersion = "9.3.19.v20170502"
@@ -10,7 +10,7 @@ sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.12.2"
scalaVersion := "2.12.3"
// dependency settings
resolvers ++= Seq(
@@ -21,25 +21,25 @@ resolvers ++= Seq(
"amateras-snapshot" at "http://amateras.sourceforge.jp/mvn-snapshot/"
)
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.7.0.201704051617-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.7.0.201704051617-r",
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.8.0.201706111038-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.8.0.201706111038-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.5.1",
"io.github.gitbucket" %% "scalatra-forms" % "1.1.0",
"commons-io" % "commons-io" % "2.5",
"io.github.gitbucket" % "solidbase" % "1.0.2",
"io.github.gitbucket" % "markedj" % "1.0.12",
"io.github.gitbucket" % "markedj" % "1.0.15",
"org.apache.commons" % "commons-compress" % "1.13",
"org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.3",
"org.apache.sshd" % "apache-sshd" % "1.4.0" exclude("org.slf4j","slf4j-jdk14"),
"org.apache.tika" % "tika-core" % "1.14",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.8",
"com.github.takezoe" %% "blocking-slick-32" % "0.0.10",
"joda-time" % "joda-time" % "2.9.9",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.195",
"mysql" % "mysql-connector-java" % "6.0.6",
"org.mariadb.jdbc" % "mariadb-java-client" % "2.0.3",
"org.postgresql" % "postgresql" % "42.0.0",
"ch.qos.logback" % "logback-classic" % "1.2.3",
"com.zaxxer" % "HikariCP" % "2.6.1",
@@ -50,13 +50,15 @@ libraryDependencies ++= Seq(
"org.cache2k" % "cache2k-all" % "1.0.0.CR1",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.6.0-akka-2.4.x" exclude("c3p0","c3p0"),
"net.coobird" % "thumbnailator" % "0.4.8",
"com.github.zafarkhaja" % "java-semver" % "0.9.0",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "provided",
"javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",
"junit" % "junit" % "4.12" % "test",
"org.scalatra" %% "scalatra-scalatest" % ScalatraVersion % "test",
"org.mockito" % "mockito-core" % "2.7.22" % "test",
"com.wix" % "wix-embedded-mysql" % "2.1.4" % "test",
"ru.yandex.qatools.embed" % "postgresql-embedded" % "2.0" % "test"
"ru.yandex.qatools.embed" % "postgresql-embedded" % "2.0" % "test",
"net.i2p.crypto" % "eddsa" % "0.1.0"
)
// Compiler settings
@@ -143,12 +145,28 @@ executableKey := {
IO copyFile (classDir / name, temp / name)
}
// include plugins
val pluginsDir = temp / "WEB-INF" / "classes" / "plugins"
IO createDirectory (pluginsDir)
IO copyFile(Keys.baseDirectory.value / "plugins.json", pluginsDir / "plugins.json")
val json = IO read(Keys.baseDirectory.value / "plugins.json")
PluginsJson.parse(json).foreach { case (plugin, version) =>
val url = if(plugin == "gitbucket-pages-plugin"){
s"https://github.com/gitbucket/${plugin}/releases/download/v${version}/${plugin}_${scalaBinaryVersion.value}-${version}.jar"
} else {
s"https://github.com/gitbucket/${plugin}/releases/download/${version}/${plugin}_${scalaBinaryVersion.value}-${version}.jar"
}
log info s"Download: ${url}"
IO download(new java.net.URL(url), pluginsDir / s"${plugin}_${scalaBinaryVersion.value}-${version}.jar")
}
// zip it up
IO delete (temp / "META-INF" / "MANIFEST.MF")
val contentMappings = (temp.*** --- PathFinder(temp)).get pair relativeTo(temp)
val manifest = new JarManifest
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
manifest.getMainAttributes put (AttrName.MANIFEST_VERSION, "1.0")
manifest.getMainAttributes put (AttrName.MAIN_CLASS, "JettyLauncher")
val outputFile = workDir / warName
IO jar (contentMappings, outputFile, manifest)

View File

@@ -1,35 +1,25 @@
JRebel integration (optional)
=============================
[JRebel](http://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
JRebel is generally able to eliminate the need for the following slow "app restart" in sbt following a code change:
[JRebel](https://zeroturnaround.com/software/jrebel/) is a JVM plugin that makes developing web apps much faster.
JRebel is generally able to eliminate the need for the slow "app restart" per modification of codes. Alsp it's only used during development, and doesn't change your deployed app in any way.
```
> jetty:start
```
While JRebel is not open source, it does reload your code faster than the `~;copy-resources;aux-compile` way of doing things using `sbt`.
It's only used during development, and doesn't change your deployed app in any way.
JRebel used to be free for Scala developers, but that changed recently, and now there's a cost associated with usage for Scala. There are trial plans and free non-commercial licenses available if you just want to try it out.
JRebel is not open source, but we can use it free for non-commercial use.
----
## 1. Get a JRebel license
Sign up for a [usage plan](https://my.jrebel.com/). You will need to create an account.
Sign up for a [myJRebel](https://my.jrebel.com/register). You will need to create an account.
## 2. Download JRebel
Download the most recent ["nosetup" JRebel zip](http://zeroturnaround.com/software/jrebel/download/prev-releases/).
Download the most recent ["nosetup" JRebel zip](https://zeroturnaround.com/software/jrebel/download/prev-releases/).
Next, unzip the downloaded file.
## 3. Activate
Follow the [instructions on the JRebel website](http://zeroturnaround.com/software/jrebel/download/prev-releases/) to activate your downloaded JRebel.
You can use the default settings for all the configurations.
Follow `readme.txt` in the extracted directory to activate your downloaded JRebel.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
@@ -41,13 +31,13 @@ You only need to tell jvm where to find the jrebel jar.
To do so, edit your shell resource file (usually `~/.bash_profile` on Mac, and `~/.bashrc` on Linux), and add the following line:
```bash
export JREBEL=/path/to/jrebel/jrebel.jar
export JREBEL=/path/to/jrebel/legacy/jrebel.jar
```
For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash
export JREBEL=~/jrebel/jrebel.jar
export JREBEL=~/jrebel/legacy/jrebel.jar
```
Now reload your shell:
@@ -73,39 +63,26 @@ $ ./sbt
You will start the servlet container slightly differently now that you're using sbt.
```
> jetty:start
> jetty:quickstart
:
[info] starting server ...
[success] Total time: 3 s, completed Jan 3, 2016 9:47:55 PM
2016-01-03 21:47:57 JRebel:
2016-01-03 21:47:57 JRebel: A newer version '6.3.1' is available for download
2016-01-03 21:47:57 JRebel: from http://zeroturnaround.com/software/jrebel/download/
2016-01-03 21:47:57 JRebel:
2016-01-03 21:47:58 JRebel: Contacting myJRebel server ..
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/classes' will be monitored for changes.
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/scala-2.11/test-classes' will be monitored for changes.
2016-01-03 21:47:59 JRebel: Directory '/git/gitbucket/target/webapp' will be monitored for changes.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: #############################################################
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: JRebel Legacy Agent 6.2.5 (201509291538)
2016-01-03 21:48:00 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Over the last 30 days JRebel prevented
2016-01-03 21:48:00 JRebel: at least 182 redeploys/restarts saving you about 7.4 hours.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Over the last 324 days JRebel prevented
2016-01-03 21:48:00 JRebel: at least 1538 redeploys/restarts saving you about 62.4 hours.
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: Licensed to nazo king (using myJRebel).
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel:
2016-01-03 21:48:00 JRebel: #############################################################
2016-01-03 21:48:00 JRebel:
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: #############################################################
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Legacy Agent 7.0.15 (201709080836)
2017-09-21 15:46:35 JRebel: (c) Copyright ZeroTurnaround AS, Estonia, Tartu.
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Over the last 2 days JRebel prevented
2017-09-21 15:46:35 JRebel: at least 8 redeploys/restarts saving you about 0.3 hours.
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: Licensed to Naoki Takezoe (using myJRebel).
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel:
2017-09-21 15:46:35 JRebel: #############################################################
2017-09-21 15:46:35 JRebel:
:
> ~ copy-resources
[success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
> ~compile
[success] Total time: 2 s, completed 2017/09/21 15:50:06
1. Waiting for source changes... (press enter to interrupt)
```
@@ -114,12 +91,11 @@ For example, you can change the title on `src/main/twirl/gitbucket/core/main.sca
```html
:
<a class="navbar-brand" href="@path/">
<img src="@assets/common/images/gitbucket.png" style="width: 24px; height: 24px;"/>GitBucket
@defining(AutoUpdate.getCurrentVersion){ version =>
<span class="header-version">@version.majorVersion.@version.minorVersion</span>
}
<a href="@context.path/" class="logo">
<img src="@helpers.assets("/common/images/gitbucket.svg")" style="width: 24px; height: 24px; display: inline;"/>
GitBucket
change code !!!!!!!!!!!!!!!!
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
</a>
:
```
@@ -128,21 +104,17 @@ If JRebel is doing is correctly installed you will see a notice for you:
```
1. Waiting for source changes... (press enter to interrupt)
2016-01-03 21:48:42 JRebel: Reloading class 'gitbucket.core.html.main$'.
[info] Wrote rebel.xml to /git/gitbucket/target/scala-2.11/resource_managed/main/rebel.xml
[info] Compiling 1 Scala source to /git/gitbucket/target/scala-2.11/classes...
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
[info] Compiling 1 Scala source to /Users/naoki.takezoe/gitbucket/target/scala-2.12/classes...
[success] Total time: 1 s, completed 2017/09/21 15:55:40
```
And you reload browser, JRebel give notice of that it has reloaded classes:
```
[success] Total time: 3 s, completed Jan 3, 2016 9:48:55 PM
2. Waiting for source changes... (press enter to interrupt)
2016-01-03 21:49:13 JRebel: Reloading class 'gitbucket.core.html.main$'.
2017-09-21 15:55:40 JRebel: Reloading class 'gitbucket.core.html.main$'.
```
## 6. Limitations
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routes patterns, there is nothing JRebel can do, you will have to run `jetty:start`.
JRebel is nearly always able to eliminate the need to explicitly reload your container after a code change. However, if you change any of your routing patterns, there is nothing JRebel can do, you will have to restart by `jetty:quickstart`.

View File

@@ -1,24 +0,0 @@
Notification Email
========
GitBucket can send email notification to users if this feature is enabled by an administrator.
The timing of the notification are as follows:
##### at the issue registration (new issue, new pull request)
When a record is saved into the ```ISSUE``` table, GitBucket does the notification.
##### at the comment registration
Among the records in the ```ISSUE_COMMENT``` table, them to be counted as a comment (i.e. the record ```ACTION``` column value is "comment" or "close_comment" or "reopen_comment") are saved, GitBucket does the notification.
##### at the status update (close, reopen, merge)
When the ```CLOSED``` column value is updated, GitBucket does the notification.
Notified users are as follows:
* individual repository's owner
* group members of group repository
* collaborators
* participants
However, the person performing the operation is excluded from the notification.

View File

@@ -6,7 +6,6 @@ Developer's Guide
* [Authentication in Controller](authenticator.md)
* [About Action in Issue Comment](comment_action.md)
* [Activity Types](activity.md)
* [Notification Email](notification.md)
* [Automatic Schema Updating](auto_update.md)
* [Release Operation](release.md)
* [JRebel integration (optional)](jrebel.md)

54
plugins.json Normal file
View File

@@ -0,0 +1,54 @@
[
{
"id": "notifications",
"name": "Notifications Plugin",
"description": "Provides notifications feature on GitBucket.",
"versions": [
{
"version": "1.2.0",
"range": ">=4.17.0",
"file": "gitbucket-notifications-plugin_2.12-1.2.0.jar"
}
],
"default": true
},
{
"id": "emoji",
"name": "Emoji Plugin",
"description": "Provides Emoji support for GitBucket.",
"versions": [
{
"version": "4.4.0",
"range": ">=4.10.0",
"file": "gitbucket-emoji-plugin_2.12-4.4.0.jar"
}
],
"default": false
},
{
"id": "gist",
"name": "Gist Plugin",
"description": "Provides Gist feature on GitBucket.",
"versions": [
{
"version": "4.10.0",
"range": ">=4.15.0",
"file": "gitbucket-gist-plugin_2.12-4.10.0.jar"
}
],
"default": false
},
{
"id": "pages",
"name": "Pages Plugin",
"description": "Project pages for gitbucket",
"versions": [
{
"version": "1.5.0",
"range": ">=4.15.0",
"file": "gitbucket-pages-plugin_2.12-1.5.0.jar"
}
],
"default": false
}
]

View File

@@ -1,7 +1,6 @@
import java.security.MessageDigest;
import java.security.MessageDigest
import scala.annotation._
import sbt._
import sbt.Using._
object Checksums {
private val bufferSize = 2048

17
project/PluginsJson.scala Normal file
View File

@@ -0,0 +1,17 @@
import com.eclipsesource.json.Json
import scala.collection.JavaConverters._
object PluginsJson {
def parse(json: String): Seq[(String, String)] = {
val value = Json.parse(json)
value.asArray.values.asScala.map { plugin =>
val obj = plugin.asObject.get("versions").asArray.asScala.head.asObject
val pluginName = obj.get("file").asString.split("_2.12-").head
val version = obj.get("version").asString
(pluginName, version)
}
}
}

1
project/build.sbt Normal file
View File

@@ -0,0 +1 @@
libraryDependencies += "com.eclipsesource.minimal-json" % "minimal-json" % "0.9.4"

View File

@@ -1,8 +1,8 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.0")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.1")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.3.7")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.5")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.0")
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8.3")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15")
addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC11")

View File

@@ -40,7 +40,7 @@ public class JettyLauncher {
}
break;
case "--max_file_size":
System.setProperty("gitbucket.maxFileSize", dim[2]);
System.setProperty("gitbucket.maxFileSize", dim[1]);
break;
case "--gitbucket.home":
System.setProperty("gitbucket.home", dim[1]);
@@ -48,6 +48,12 @@ public class JettyLauncher {
case "--temp_dir":
tmpDirPath = dim[1];
break;
case "--plugin_dir":
System.setProperty("gitbucket.pluginDir", dim[1]);
break;
case "--validate_password":
System.setProperty("gitbucket.validate.password", dim[1]);
break;
}
}
}

View File

@@ -25,15 +25,12 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
context.getFilterRegistration("gitAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("apiAuthenticationFilter", new ApiAuthenticationFilter)
context.getFilterRegistration("apiAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
context.addFilter("ghCompatRepositoryAccessFilter", new GHCompatRepositoryAccessFilter)
context.getFilterRegistration("ghCompatRepositoryAccessFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")
context.mount(new PreProcessController, "/*")
PluginRegistry().getControllers.foreach { case (controller, path) =>
context.mount(controller, path)
}
context.addFilter("pluginControllerFilter", new PluginControllerFilter)
context.getFilterRegistration("pluginControllerFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.mount(new IndexController, "/")
context.mount(new ApiController, "/api/v3")

View File

@@ -38,5 +38,9 @@ object GitBucketCoreModule extends Module("gitbucket-core",
new Version("4.14.0",
new LiquibaseMigration("update/gitbucket-core_4.14.xml"),
new SqlMigration("update/gitbucket-core_4.14.sql")
)
),
new Version("4.14.1"),
new Version("4.15.0"),
new Version("4.16.0"),
new Version("4.17.0")
)

View File

@@ -0,0 +1,124 @@
package gitbucket.core.api
import gitbucket.core.model.Account
import gitbucket.core.util.JGitUtil.{CommitInfo, DiffInfo}
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import ApiCommits._
case class ApiCommits(
url: ApiPath,
sha: String,
html_url: ApiPath,
comment_url: ApiPath,
commit: Commit,
author: ApiUser,
committer: ApiUser,
parents: Seq[Tree],
stats: Stats,
files: Seq[File]
)
object ApiCommits {
case class Commit(
url: ApiPath,
author: ApiPersonIdent,
committer: ApiPersonIdent,
message: String,
comment_count: Int,
tree: Tree
)
case class Tree(
url: ApiPath,
sha: String
)
case class Stats(
additions: Int,
deletions: Int,
total: Int
)
case class File(
filename: String,
additions: Int,
deletions: Int,
changes: Int,
status: String,
raw_url: ApiPath,
blob_url: ApiPath,
patch: String
)
def apply(repositoryName: RepositoryName, commitInfo: CommitInfo, diffs: Seq[DiffInfo], author: Account, committer: Account,
commentCount: Int): ApiCommits = {
val files = diffs.map { diff =>
var additions = 0
var deletions = 0
diff.patch.getOrElse("").split("\n").foreach { line =>
if(line.startsWith("+")) additions = additions + 1
if(line.startsWith("-")) deletions = deletions + 1
}
File(
filename = if(diff.changeType == ChangeType.DELETE){ diff.oldPath } else { diff.newPath },
additions = additions,
deletions = deletions,
changes = additions + deletions,
status = diff.changeType match {
case ChangeType.ADD => "added"
case ChangeType.MODIFY => "modified"
case ChangeType.DELETE => "deleted"
case ChangeType.RENAME => "renamed"
case ChangeType.COPY => "copied"
},
raw_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/raw/${commitInfo.id}/${diff.newPath}")
},
blob_url = if(diff.changeType == ChangeType.DELETE){
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.parents.head}/${diff.oldPath}")
} else {
ApiPath(s"/${repositoryName.fullName}/blob/${commitInfo.id}/${diff.newPath}")
},
patch = diff.patch.getOrElse("")
)
}
ApiCommits(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
sha = commitInfo.id,
html_url = ApiPath(s"${repositoryName.fullName}/commit/${commitInfo.id}"),
comment_url = ApiPath(""),
commit = Commit(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${commitInfo.id}"),
author = ApiPersonIdent.author(commitInfo),
committer = ApiPersonIdent.committer(commitInfo),
message = commitInfo.shortMessage,
comment_count = commentCount,
tree = Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${commitInfo.id}"), // TODO This endpoint has not been implemented yet.
sha = commitInfo.id
)
),
author = ApiUser(author),
committer = ApiUser(committer),
parents = commitInfo.parents.map { parent =>
Tree(
url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/tree/${parent}"), // TODO This endpoint has not been implemented yet.
sha = parent
)
},
stats = Stats(
additions = files.map(_.additions).sum,
deletions = files.map(_.deletions).sum,
total = files.map(_.additions).sum + files.map(_.deletions).sum
),
files = files
)
}
}

View File

@@ -1,6 +1,13 @@
package gitbucket.core.api
/**
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
* Path for API url.
* If set path '/repos/aa/bb' then, expand 'http://server:port/repos/aa/bb' when converted to json.
*/
case class ApiPath(path: String)
/**
* Path for git repository via SSH.
* If set path '/aa/bb.git' then, expand 'git@server:port/aa/bb.git' when converted to json.
*/
case class SshPath(path: String)

View File

@@ -3,7 +3,6 @@ package gitbucket.core.api
import gitbucket.core.model.{Account, Issue, IssueComment, PullRequest}
import java.util.Date
/**
* https://developer.github.com/v3/pulls/
*/
@@ -19,7 +18,8 @@ case class ApiPullRequest(
merged_by: Option[ApiUser],
title: String,
body: String,
user: ApiUser) {
user: ApiUser,
assignee: Option[ApiUser]){
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -39,6 +39,7 @@ object ApiPullRequest{
headRepo: ApiRepository,
baseRepo: ApiRepository,
user: ApiUser,
assignee: Option[ApiUser],
mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest =
ApiPullRequest(
@@ -59,14 +60,16 @@ object ApiPullRequest{
merged_by = mergedComment.map { case (_, account) => ApiUser(account) },
title = issue.title,
body = issue.content.getOrElse(""),
user = user
user = user,
assignee = assignee
)
case class Commit(
sha: String,
ref: String,
repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
val label = if( baseOwner == repo.owner.login ){ ref } else { s"${repo.owner.login}:${ref}" }
val user = repo.owner
}
}

View File

@@ -24,6 +24,7 @@ case class ApiRepository(
val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
val ssh_url = Some(SshPath(s":${full_name}.git"))
}
object ApiRepository{
@@ -55,12 +56,13 @@ object ApiRepository{
def forDummyPayload(owner: ApiUser): ApiRepository =
ApiRepository(
name="dummy",
full_name=s"${owner.login}/dummy",
description="",
watchers=0,
forks=0,
`private`=false,
default_branch="master",
owner=owner)(true)
name = "dummy",
full_name = s"${owner.login}/dummy",
description = "",
watchers = 0,
forks = 0,
`private` = false,
default_branch = "master",
owner = owner
)(true)
}

View File

@@ -10,7 +10,7 @@ import scala.util.Try
object JsonFormat {
case class Context(baseUrl: String)
case class Context(baseUrl: String, sshUrl: Option[String])
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
@@ -33,23 +33,31 @@ object JsonFormat {
FieldSerializer[ApiComment]() +
FieldSerializer[ApiContents]() +
FieldSerializer[ApiLabel]() +
FieldSerializer[ApiCommits]() +
FieldSerializer[ApiCommits.Commit]() +
FieldSerializer[ApiCommits.Tree]() +
FieldSerializer[ApiCommits.Stats]() +
FieldSerializer[ApiCommits.File]() +
ApiBranchProtection.enforcementLevelSerializer
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](format =>
(
{
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
},
{
case ApiPath(path) => JString(c.baseUrl + path)
}
)
)
def apiPathSerializer(c: Context) = new CustomSerializer[ApiPath](_ => ({
case JString(s) if s.startsWith(c.baseUrl) => ApiPath(s.substring(c.baseUrl.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, {
case ApiPath(path) => JString(c.baseUrl + path)
}))
def sshPathSerializer(c: Context) = new CustomSerializer[SshPath](_ => ({
case JString(s) if c.sshUrl.exists(sshUrl => s.startsWith(sshUrl)) => SshPath(s.substring(c.sshUrl.get.length))
case JString(s) => throw new MappingException("Can't convert " + s + " to ApiPath")
}, {
case SshPath(path) => c.sshUrl.map { sshUrl => JString(sshUrl + path) } getOrElse JNothing
}))
/**
* convert object to json string
*/
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
def apply(obj: AnyRef)(implicit c: Context): String =
Serialization.write(obj)(jsonFormats + apiPathSerializer(c) + sshPathSerializer(c))
}

View File

@@ -232,6 +232,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
} getOrElse NotFound()
})
get("/captures/(.*)".r) {
multiParams("captures").head
}
get("/:userName/_delete")(oneselfOnly {
val userName = params("userName")
@@ -594,22 +598,23 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// Insert default labels
insertDefaultLabels(accountName, repository.name)
// Insert default priorities
insertDefaultPriorities(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(accountName, repository.name))
FileUtil.deleteIfExists(getRepositoryDir(accountName, repository.name)))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(accountName, repository.name))
JGitUtil.cloneRepository(getWikiRepositoryDir(repository.owner, repository.name),
FileUtil.deleteIfExists(getWikiRepositoryDir(accountName, repository.name)))
// Copy files
FileUtils.copyDirectory(
Directory.getRepositoryFilesDir(repository.owner, repository.name),
Directory.getRepositoryFilesDir(accountName, repository.name)
)
// Copy LFS files
val lfsDir = getLfsDir(repository.owner, repository.name)
if(lfsDir.exists){
FileUtils.copyDirectory(lfsDir, FileUtil.deleteIfExists(getLfsDir(accountName, repository.name)))
}
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)

View File

@@ -1,14 +0,0 @@
package gitbucket.core.controller
class AnonymousAccessController extends AnonymousAccessControllerBase
trait AnonymousAccessControllerBase extends ControllerBase {
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -5,14 +5,16 @@ import gitbucket.core.model._
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.view.helpers.{renderMarkup, isRenderable}
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util._
import gitbucket.core.view.helpers.{isRenderable, renderMarkup}
import org.eclipse.jgit.api.Git
import org.scalatra.{NoContent, UnprocessableEntity, Created}
import org.eclipse.jgit.revwalk.RevWalk
import org.scalatra.{Created, NoContent, UnprocessableEntity}
import scala.collection.JavaConverters._
class ApiController extends ApiControllerBase
@@ -49,6 +51,7 @@ trait ApiControllerBase extends ControllerBase {
with LabelsService
with MilestonesService
with PullRequestService
with CommitsService
with CommitStatusService
with RepositoryCreationService
with IssueCreationService
@@ -124,10 +127,10 @@ trait ApiControllerBase extends ControllerBase {
/**
* https://developer.github.com/v3/repos/branches/#get-branch
*/
get ("/api/v3/repos/:owner/:repo/branches/:branch")(referrersOnly { repository =>
get ("/api/v3/repos/:owner/:repo/branches/*")(referrersOnly { repository =>
//import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.contains(branch)
branch <- params.get("splat") if repository.branchList.contains(branch)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {
val protection = getProtectedBranchInfo(repository.owner, repository.name, branch)
@@ -286,10 +289,10 @@ trait ApiControllerBase extends ControllerBase {
/**
* 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/*")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.contains(branch)
branch <- params.get("splat") if repository.branchList.contains(branch)
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
br <- getBranches(repository.owner, repository.name, repository.repository.defaultBranch, repository.repository.originUserName.isEmpty).find(_.name == branch)
} yield {
@@ -381,7 +384,7 @@ trait ApiControllerBase extends ControllerBase {
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
comments = getCommentsForApi(repository.owner, repository.name, issueId)
} yield {
JsonFormat(comments.map{ case (issueComment, user, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
}) getOrElse NotFound()
@@ -498,7 +501,7 @@ trait ApiControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account)] =
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -506,13 +509,14 @@ trait ApiControllerBase extends ControllerBase {
repos = repository.owner -> repository.name
)
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
JsonFormat(issues.map { case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
@@ -529,6 +533,7 @@ trait ApiControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
JsonFormat(ApiPullRequest(
@@ -537,6 +542,7 @@ trait ApiControllerBase extends ControllerBase {
headRepo = ApiRepository(headRepo, ApiUser(headOwner)),
baseRepo = ApiRepository(repository, ApiUser(baseOwner)),
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
))
}) getOrElse NotFound()
@@ -627,6 +633,52 @@ trait ApiControllerBase extends ControllerBase {
}) getOrElse NotFound()
})
/**
* https://developer.github.com/v3/repos/commits/#get-a-single-commit
*/
get("/api/v3/repos/:owner/:repo/commits/:sha")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
val sha = params("sha")
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val objectId = repo.resolve(sha)
val commitInfo = using(new RevWalk(repo)){ revWalk =>
new CommitInfo(revWalk.parseCommit(objectId))
}
JsonFormat(ApiCommits(
repositoryName = RepositoryName(repository),
commitInfo = commitInfo,
diffs = JGitUtil.getDiffs(git, commitInfo.parents.head, commitInfo.id, false, true),
author = getAccount(commitInfo.authorName, commitInfo.authorEmailAddress),
committer = getAccount(commitInfo.committerName, commitInfo.committerEmailAddress),
commentCount = getCommitComment(repository.owner, repository.name, sha).size
))
}
})
private def getAccount(userName: String, email: String): Account = {
getAccountByMailAddress(email).getOrElse {
Account(
userName = userName,
fullName = userName,
mailAddress = email,
password = "xxx",
isAdmin = false,
url = None,
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date(),
lastLoginDate = None,
image = None,
isGroupAccount = false,
isRemoved = true,
description = None
)
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasDeveloperRole(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName

View File

@@ -26,6 +26,7 @@ import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._
import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
/**
* Provides generic features for controller implementations.
@@ -34,6 +35,8 @@ abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
private val logger = LoggerFactory.getLogger(getClass)
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
before("/api/v3/*") {
@@ -147,6 +150,20 @@ abstract class ControllerBase extends ScalatraFilter
}
}
error{
case e => {
logger.error(s"Catch unhandled error in request: ${request}", e)
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.InternalServerError()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.InternalServerError(ApiError("Internal Server Error"))
} else {
org.scalatra.InternalServerError(gitbucket.core.html.error("Internal Server Error", Some(e)))
}
}
}
override def url(path: String, params: Iterable[(String, Any)] = Iterable.empty,
includeContextPath: Boolean = true, includeServletPath: Boolean = true,
absolutize: Boolean = true, withSessionId: Boolean = true)
@@ -163,7 +180,7 @@ abstract class ControllerBase extends ScalatraFilter
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Seq[(String, String)] =
valueType.validate(name, trim(value), params, messages)
private def trim(value: String): String = if(value == null) null else value.replaceAll("\r\n", "").trim
private def trim(value: String): String = if(value == null) null else value.replace("\r\n", "").trim
}
/**

View File

@@ -48,7 +48,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}, FileUtil.isUploadableType)
}, _ => true)
}
post("/wiki/:owner/:repository"){
@@ -85,7 +85,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
fileName
}
}
}, FileUtil.isUploadableType)
}, _ => true)
}
} getOrElse BadRequest()
}
@@ -113,7 +113,7 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport with R
}
}
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
private def execute(f: (FileItem, String) => Unit , mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)

View File

@@ -19,11 +19,12 @@ trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String)
case class SignInForm(userName: String, password: String, hash: Option[String])
val signinForm = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
"password" -> trim(label("Password", text(required))),
"hash" -> trim(optional(text()))
)(SignInForm.apply)
// val searchForm = mapping(
@@ -54,7 +55,7 @@ trait IndexControllerBase extends ControllerBase {
post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account)
case Some(account) => signin(account, form.hash)
case None => {
flash += "userName" -> form.userName
flash += "password" -> form.password
@@ -86,7 +87,7 @@ trait IndexControllerBase extends ControllerBase {
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: Account) = {
private def signin(account: Account, hash: Option[String]) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
@@ -98,7 +99,7 @@ trait IndexControllerBase extends ControllerBase {
if(redirectUrl.stripSuffix("/") == request.getContextPath){
redirect("/")
} else {
redirect(redirectUrl)
redirect(redirectUrl + hash.getOrElse(""))
}
}.getOrElse {
redirect("/")

View File

@@ -193,7 +193,7 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditableContent(owner, name, comment.commentedUserName)){
updateComment(comment.commentId, form.content)
updateComment(comment.issueId, comment.commentId, form.content)
redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}")
} else Unauthorized()
} getOrElse NotFound()
@@ -204,7 +204,7 @@ trait IssuesControllerBase extends ControllerBase {
defining(repository.owner, repository.name){ case (owner, name) =>
getComment(owner, name, params("id")).map { comment =>
if(isEditableContent(owner, name, comment.commentedUserName)){
Ok(deleteComment(comment.commentId))
Ok(deleteComment(comment.issueId, comment.commentId))
} else Unauthorized()
} getOrElse NotFound()
}

View File

@@ -0,0 +1,40 @@
package gitbucket.core.controller
import org.scalatra.MovedPermanently
class PreProcessController extends PreProcessControllerBase
trait PreProcessControllerBase extends ControllerBase {
/**
* Provides GitHub compatible URLs for Git client.
*
* <ul>
* <li>git clone http://localhost:8080/owner/repo</li>
* <li>git clone http://localhost:8080/owner/repo.git</li>
* </ul>
*
* @see https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
*/
get("/*/*/info/refs") {
val query = Option(request.getQueryString).map("?" + _).getOrElse("")
halt(MovedPermanently(baseUrl + "/git" + request.getRequestURI + query))
}
/**
* Filter requests from anonymous users.
*
* If anonymous access is allowed, pass all requests.
* But if it's not allowed, demands authentication except some paths.
*/
get(!context.settings.allowAnonymousAccess, context.loginAccount.isEmpty) {
if(!context.currentPath.startsWith("/assets") && !context.currentPath.startsWith("/signin") &&
!context.currentPath.startsWith("/register") && !context.currentPath.endsWith("/info/refs")) {
Unauthorized()
} else {
pass()
}
}
}

View File

@@ -251,7 +251,7 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
val commentId = createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
@@ -282,7 +282,10 @@ trait PullRequestsControllerBase extends ControllerBase {
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// call hooks
PluginRegistry().getPullRequestHooks.foreach(_.merged(issue, repository))
PluginRegistry().getPullRequestHooks.foreach{ h =>
h.addedComment(commentId, form.message, issue, repository)
h.merged(issue, repository)
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
@@ -321,8 +324,8 @@ trait PullRequestsControllerBase extends ControllerBase {
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, originId) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifier(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner) {
@@ -408,8 +411,8 @@ trait PullRequestsControllerBase extends ControllerBase {
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(readableUsersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
val (originOwner, tmpOriginBranch) = parseCompareIdentifier(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifier(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
@@ -502,7 +505,7 @@ trait PullRequestsControllerBase extends ControllerBase {
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
private def parseCompareIdentifier(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))

View File

@@ -150,7 +150,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
get("/:owner/:repository/settings/branches")(ownerOnly { repository =>
val protecteions = getProtectedBranchList(repository.owner, repository.name)
html.branches(repository, protecteions, flash.get("info"))
});
})
/** Update default branch */
post("/:owner/:repository/settings/update_default_branch", defaultBranchForm)(ownerOnly { (form, repository) =>
@@ -355,7 +355,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getAttachedDir(form.newOwner, repository.name))
}
}
// Delere parent directory
// Delete parent directory
FileUtil.deleteDirectoryIfEmpty(getRepositoryFilesDir(repository.owner, repository.name))
// Call hooks
@@ -393,7 +393,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
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();
git.gc().call()
}
}
flash += "info" -> "Garbage collection has been executed."

View File

@@ -13,7 +13,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, WebHook}
import gitbucket.core.model.{Account, CommitState, CommitStatus, WebHook}
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
@@ -174,13 +174,24 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val (branchName, path) = repository.splitPath(multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
def getStatuses(sha: String): List[CommitStatus] = {
getCommitStatues(repository.owner, repository.name, sha)
}
def getSummary(statuses: List[CommitStatus]): (CommitState, String) = {
val stateMap = statuses.groupBy(_.state)
val state = CommitState.combine(stateMap.keySet)
val summary = stateMap.map{ case (keyState, states) => states.size+" "+keyState.name }.mkString(", ")
state -> summary
}
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
}, page, hasNext, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), getStatuses, getSummary)
case Left(_) => NotFound()
}
}
@@ -213,7 +224,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/upload", uploadForm)(writableUsersOnly { (form, repository) =>
val files = form.uploadFiles.split("\n").map { line =>
val i = line.indexOf(":")
val i = line.indexOf(':')
CommitFile(line.substring(0, i).trim, line.substring(i + 1).trim)
}
@@ -222,7 +233,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
branch = form.branch,
path = form.path,
files = files,
message = form.message.getOrElse(s"Add files via upload")
message = form.message.getOrElse("Add files via upload")
)
if(form.path.length == 0){
@@ -420,7 +431,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
try {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
JGitUtil.getDiffs(git, id) match {
JGitUtil.getDiffs(git, id, false) match {
case (diffs, oldCommitId) =>
html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
@@ -467,9 +478,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val comment = getCommitComment(repository.owner, repository.name, commentId.toString).get
form.issueId match {
case Some(issueId) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
callPullRequestReviewCommentWebHook("create", comment, repository, issueId, context.baseUrl, context.loginAccount.get)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
getPullRequest(repository.owner, repository.name, issueId).foreach { case (issue, pullRequest) =>
recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
PluginRegistry().getPullRequestHooks.foreach(_.addedComment(commentId, form.content, issue, repository))
callPullRequestReviewCommentWebHook("create", comment, repository, issue, pullRequest, context.baseUrl, context.loginAccount.get)
}
case None =>
recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(comment, hasDeveloperRole(repository.owner, repository.name, context.loginAccount), repository)
})
@@ -630,8 +645,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
case class UploadFiles(branch: String, path: String, fileIds : Map[String,String], message: String) {
lazy val isValid: Boolean = fileIds.size > 0
case class UploadFiles(branch: String, path: String, fileIds: Map[String,String], message: String) {
lazy val isValid: Boolean = fileIds.nonEmpty
}
case class CommitFile(id: String, name: String)

View File

@@ -6,7 +6,7 @@ import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, RepositoryService, SystemSettingsService}
import gitbucket.core.util.{AdminAuthenticator, Mailer}
import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.plugin.{PluginInfoBase, PluginRegistry, PluginRepository}
import SystemSettingsService._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
@@ -15,6 +15,10 @@ import gitbucket.core.util.StringUtil._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.{FileUtils, IOUtils}
import org.scalatra.i18n.Messages
import com.github.zafarkhaja.semver.{Version => Semver}
import gitbucket.core.GitBucketCoreModule
import scala.collection.JavaConverters._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with RepositoryService with AdminAuthenticator
@@ -59,7 +63,8 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(Ldap.apply)),
"skinName" -> trim(label("AdminLTE skin name", text(required)))
)(SystemSettings.apply).verifying { settings =>
Vector(
if(settings.ssh && settings.baseUrl.isEmpty){
@@ -169,9 +174,13 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
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.",
context.loginAccount.get)
new Mailer(context.settings.copy(smtp = Some(form.smtp), notification = true)).send(
to = form.testAddress,
subject = "Test message from GitBucket",
textMsg = "This is a test message from GitBucket.",
htmlMsg = None,
loginAccount = context.loginAccount
)
"Test mail has been sent to: " + form.testAddress
@@ -181,7 +190,71 @@ trait SystemSettingsControllerBase extends AccountManagementControllerBase {
})
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
// Installed plugins
val enabledPlugins = PluginRegistry().getPlugins()
val gitbucketVersion = Semver.valueOf(GitBucketCoreModule.getVersions.asScala.last.getVersion)
// Plugins in the local repository
val repositoryPlugins = PluginRepository.getPlugins()
.filterNot { meta =>
enabledPlugins.exists { plugin => plugin.pluginId == meta.id &&
Semver.valueOf(plugin.pluginVersion).greaterThanOrEqualTo(Semver.valueOf(meta.latestVersion.version))
}
}.map { meta =>
(meta, meta.versions.reverse.find { version => gitbucketVersion.satisfies(version.range) })
}.collect { case (meta, Some(version)) =>
new PluginInfoBase(
pluginId = meta.id,
pluginName = meta.name,
pluginVersion = version.version,
description = meta.description
)
}
// Merge
val plugins = enabledPlugins.map((_, true)) ++ repositoryPlugins.map((_, false))
html.plugins(plugins, flash.get("info"))
})
post("/admin/plugins/_reload")(adminOnly {
PluginRegistry.reload(request.getServletContext(), loadSystemSettings(), request2Session(request).conn)
flash += "info" -> "All plugins were reloaded."
redirect("/admin/plugins")
})
post("/admin/plugins/:pluginId/:version/_uninstall")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
PluginRegistry().getPlugins()
.collect { case plugin if (plugin.pluginId == pluginId && plugin.pluginVersion == version) => plugin }
.foreach { _ =>
PluginRegistry.uninstall(pluginId, request.getServletContext, loadSystemSettings(), request2Session(request).conn)
flash += "info" -> s"${pluginId} was uninstalled."
}
redirect("/admin/plugins")
})
post("/admin/plugins/:pluginId/:version/_install")(adminOnly {
val pluginId = params("pluginId")
val version = params("version")
/// TODO!!!!
PluginRepository.getPlugins()
.collect { case meta if meta.id == pluginId => (meta, meta.versions.find(_.version == version) )}
.foreach { case (meta, version) =>
version.foreach { version =>
// TODO Install version!
PluginRegistry.install(
new java.io.File(PluginHome, s".repository/${version.file}"),
request.getServletContext,
loadSystemSettings(),
request2Session(request).conn
)
flash += "info" -> s"${pluginId} was installed."
}
}
redirect("/admin/plugins")
})

View File

@@ -76,7 +76,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
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, false).filter(_.newPath == pageName + ".md"), repository,
isEditable(repository), flash.get("info"))
}
})
@@ -85,7 +85,7 @@ trait WikiControllerBase extends ControllerBase {
val Array(from, to) = params("commitId").split("\\.\\.\\.")
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, false), repository,
isEditable(repository), flash.get("info"))
}
})

View File

@@ -3,18 +3,20 @@ package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.model.Issue
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.model.Profile._
import profile.api._
trait IssueHook {
def created(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def created(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def addedComment(commentId: Int, content: String, issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def closed(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
def reopened(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
}
trait PullRequestHook extends IssueHook {
def merged(issue: Issue, repository: RepositoryInfo)(implicit context: Context): Unit = ()
def merged(issue: Issue, repository: RepositoryInfo)(implicit session: Session, context: Context): Unit = ()
}

View File

@@ -121,6 +121,16 @@ abstract class Plugin {
*/
def pullRequestHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[PullRequestHook] = Nil
/**
* Override to add repository headers.
*/
val repositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add repository headers.
*/
def repositoryHeaders(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[(RepositoryInfo, Context) => Option[Html]] = Nil
/**
* Override to add global menus.
*/
@@ -266,6 +276,9 @@ abstract class Plugin {
(pullRequestHooks ++ pullRequestHooks(registry, context, settings)).foreach { pullRequestHook =>
registry.addPullRequestHook(pullRequestHook)
}
(repositoryHeaders ++ repositoryHeaders(registry, context, settings)).foreach { repositoryHeader =>
registry.addRepositoryHeader(repositoryHeader)
}
(globalMenus ++ globalMenus(registry, context, settings)).foreach { globalMenu =>
registry.addGlobalMenu(globalMenu)
}
@@ -287,8 +300,8 @@ abstract class Plugin {
(dashboardTabs ++ dashboardTabs(registry, context, settings)).foreach { dashboardTab =>
registry.addDashboardTab(dashboardTab)
}
(issueSidebars ++ issueSidebars(registry, context, settings)).foreach { issueSidebar =>
registry.addIssueSidebar(issueSidebar)
(issueSidebars ++ issueSidebars(registry, context, settings)).foreach { issueSidebarComponent =>
registry.addIssueSidebar(issueSidebarComponent)
}
(assetsMappings ++ assetsMappings(registry, context, settings)).foreach { assetMapping =>
registry.addAssetsMapping((assetMapping._1, assetMapping._2, getClass.getClassLoader))
@@ -302,11 +315,17 @@ abstract class Plugin {
}
/**
* This method is invoked in shutdown of plugin system.
* This method is invoked when the plugin system is shutting down.
* If the plugin has any resources, release them in this method.
*/
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
// /**
// * This method is invoked when this plugin is uninstalled.
// * Cleanup database or any other resources in this method if necessary.
// */
// def uninstall(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
/**
* Helper method to get a resource from classpath.
*/

View File

@@ -2,13 +2,18 @@ package gitbucket.core.plugin
import java.io.{File, FilenameFilter, InputStream}
import java.net.URLClassLoader
import java.nio.file.{Files, Paths, StandardWatchEventKinds}
import java.util.Base64
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.ConcurrentHashMap
import javax.servlet.ServletContext
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.model.{Account, Issue}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.DatabaseConfig
@@ -16,55 +21,53 @@ import gitbucket.core.util.Directory._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import io.github.gitbucket.solidbase.model.Module
import org.apache.commons.io.FileUtils
import org.slf4j.LoggerFactory
import play.twirl.api.Html
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.collection.JavaConverters._
class PluginRegistry {
private val plugins = new ListBuffer[PluginInfo]
private val javaScripts = new ListBuffer[(String, String)]
private val controllers = new ListBuffer[(ControllerBase, String)]
private val images = mutable.Map[String, String]()
private val renderers = mutable.Map[String, Renderer]()
renderers ++= Seq(
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
private val accountHooks = new ListBuffer[AccountHook]
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
private val plugins = new ConcurrentLinkedQueue[PluginInfo]
private val javaScripts = new ConcurrentLinkedQueue[(String, String)]
private val controllers = new ConcurrentLinkedQueue[(ControllerBase, String)]
private val images = new ConcurrentHashMap[String, String]
private val renderers = new ConcurrentHashMap[String, Renderer]
renderers.put("md", MarkdownRenderer)
renderers.put("markdown", MarkdownRenderer)
private val repositoryRoutings = new ConcurrentLinkedQueue[GitRepositoryRouting]
private val accountHooks = new ConcurrentLinkedQueue[AccountHook]
private val receiveHooks = new ConcurrentLinkedQueue[ReceiveHook]
receiveHooks.add(new ProtectedBranchReceiveHook())
private val repositoryHooks = new ListBuffer[RepositoryHook]
private val issueHooks = new ListBuffer[IssueHook]
issueHooks += new gitbucket.core.util.Notifier.IssueHook()
private val repositoryHooks = new ConcurrentLinkedQueue[RepositoryHook]
private val issueHooks = new ConcurrentLinkedQueue[IssueHook]
private val pullRequestHooks = new ListBuffer[PullRequestHook]
pullRequestHooks += new gitbucket.core.util.Notifier.PullRequestHook()
private val pullRequestHooks = new ConcurrentLinkedQueue[PullRequestHook]
private val globalMenus = new ListBuffer[(Context) => Option[Link]]
private val repositoryMenus = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ListBuffer[(RepositoryInfo, Context) => Option[Link]]
private val profileTabs = new ListBuffer[(Account, Context) => Option[Link]]
private val systemSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val accountSettingMenus = new ListBuffer[(Context) => Option[Link]]
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
private val issueSidebars = new ListBuffer[(Issue, RepositoryInfo, Context) => Option[Html]]
private val assetsMappings = new ListBuffer[(String, String, ClassLoader)]
private val textDecorators = new ListBuffer[TextDecorator]
private val repositoryHeaders = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Html]]
private val globalMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val repositoryMenus = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Link]]
private val repositorySettingTabs = new ConcurrentLinkedQueue[(RepositoryInfo, Context) => Option[Link]]
private val profileTabs = new ConcurrentLinkedQueue[(Account, Context) => Option[Link]]
private val systemSettingMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val accountSettingMenus = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val dashboardTabs = new ConcurrentLinkedQueue[(Context) => Option[Link]]
private val issueSidebars = new ConcurrentLinkedQueue[(Issue, RepositoryInfo, Context) => Option[Html]]
private val assetsMappings = new ConcurrentLinkedQueue[(String, String, ClassLoader)]
private val textDecorators = new ConcurrentLinkedQueue[TextDecorator]
private val suggestionProviders = new ListBuffer[SuggestionProvider]
suggestionProviders += new UserNameSuggestionProvider()
private val suggestionProviders = new ConcurrentLinkedQueue[SuggestionProvider]
suggestionProviders.add(new UserNameSuggestionProvider())
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
def addPlugin(pluginInfo: PluginInfo): Unit = plugins.add(pluginInfo)
def getPlugins(): List[PluginInfo] = plugins.toList
def getPlugins(): List[PluginInfo] = plugins.asScala.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = Base64.getEncoder.encodeToString(bytes)
images += ((id, encoded))
images.put(id, encoded)
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
@@ -77,28 +80,28 @@ class PluginRegistry {
addImage(id, bytes)
}
def getImage(id: String): String = images(id)
def getImage(id: String): String = images.get(id)
def addController(path: String, controller: ControllerBase): Unit = controllers += ((controller, path))
def addController(path: String, controller: ControllerBase): Unit = controllers.add((controller, path))
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = addController(path, controller)
def getControllers(): Seq[(ControllerBase, String)] = controllers.toSeq
def getControllers(): Seq[(ControllerBase, String)] = controllers.asScala.toSeq
def addJavaScript(path: String, script: String): Unit = javaScripts += ((path, script))
def addJavaScript(path: String, script: String): Unit = javaScripts.add((path, script)) //javaScripts += ((path, script))
def getJavaScript(currentPath: String): List[String] = javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
def getJavaScript(currentPath: String): List[String] = javaScripts.asScala.filter(x => currentPath.matches(x._1)).toList.map(_._2)
def addRenderer(extension: String, renderer: Renderer): Unit = renderers += ((extension, renderer))
def addRenderer(extension: String, renderer: Renderer): Unit = renderers.put(extension, renderer)
def getRenderer(extension: String): Renderer = renderers.getOrElse(extension, DefaultRenderer)
def getRenderer(extension: String): Renderer = renderers.asScala.getOrElse(extension, DefaultRenderer)
def renderableExtensions: Seq[String] = renderers.keys.toSeq
def renderableExtensions: Seq[String] = renderers.keys.asScala.toSeq
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings += routing
def addRepositoryRouting(routing: GitRepositoryRouting): Unit = repositoryRoutings.add(routing)
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.toSeq
def getRepositoryRoutings(): Seq[GitRepositoryRouting] = repositoryRoutings.asScala.toSeq
def getRepositoryRouting(repositoryPath: String): Option[GitRepositoryRouting] = {
PluginRegistry().getRepositoryRoutings().find {
@@ -108,69 +111,73 @@ class PluginRegistry {
}
}
def addAccountHook(accountHook: AccountHook): Unit = accountHooks += accountHook
def addAccountHook(accountHook: AccountHook): Unit = accountHooks.add(accountHook)
def getAccountHooks: Seq[AccountHook] = accountHooks.toSeq
def getAccountHooks: Seq[AccountHook] = accountHooks.asScala.toSeq
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks += commitHook
def addReceiveHook(commitHook: ReceiveHook): Unit = receiveHooks.add(commitHook)
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.asScala.toSeq
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks += repositoryHook
def addRepositoryHook(repositoryHook: RepositoryHook): Unit = repositoryHooks.add(repositoryHook)
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.toSeq
def getRepositoryHooks: Seq[RepositoryHook] = repositoryHooks.asScala.toSeq
def addIssueHook(issueHook: IssueHook): Unit = issueHooks += issueHook
def addIssueHook(issueHook: IssueHook): Unit = issueHooks.add(issueHook)
def getIssueHooks: Seq[IssueHook] = issueHooks.toSeq
def getIssueHooks: Seq[IssueHook] = issueHooks.asScala.toSeq
def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks += pullRequestHook
def addPullRequestHook(pullRequestHook: PullRequestHook): Unit = pullRequestHooks.add(pullRequestHook)
def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.toSeq
def getPullRequestHooks: Seq[PullRequestHook] = pullRequestHooks.asScala.toSeq
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus += globalMenu
def addRepositoryHeader(repositoryHeader: (RepositoryInfo, Context) => Option[Html]): Unit = repositoryHeaders.add(repositoryHeader)
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.toSeq
def getRepositoryHeaders: Seq[(RepositoryInfo, Context) => Option[Html]] = repositoryHeaders.asScala.toSeq
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus += repositoryMenu
def addGlobalMenu(globalMenu: (Context) => Option[Link]): Unit = globalMenus.add(globalMenu)
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.toSeq
def getGlobalMenus: Seq[(Context) => Option[Link]] = globalMenus.asScala.toSeq
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs += repositorySettingTab
def addRepositoryMenu(repositoryMenu: (RepositoryInfo, Context) => Option[Link]): Unit = repositoryMenus.add(repositoryMenu)
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.toSeq
def getRepositoryMenus: Seq[(RepositoryInfo, Context) => Option[Link]] = repositoryMenus.asScala.toSeq
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs += profileTab
def addRepositorySettingTab(repositorySettingTab: (RepositoryInfo, Context) => Option[Link]): Unit = repositorySettingTabs.add(repositorySettingTab)
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.toSeq
def getRepositorySettingTabs: Seq[(RepositoryInfo, Context) => Option[Link]] = repositorySettingTabs.asScala.toSeq
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus += systemSettingMenu
def addProfileTab(profileTab: (Account, Context) => Option[Link]): Unit = profileTabs.add(profileTab)
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.toSeq
def getProfileTabs: Seq[(Account, Context) => Option[Link]] = profileTabs.asScala.toSeq
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus += accountSettingMenu
def addSystemSettingMenu(systemSettingMenu: (Context) => Option[Link]): Unit = systemSettingMenus.add(systemSettingMenu)
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.toSeq
def getSystemSettingMenus: Seq[(Context) => Option[Link]] = systemSettingMenus.asScala.toSeq
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs += dashboardTab
def addAccountSettingMenu(accountSettingMenu: (Context) => Option[Link]): Unit = accountSettingMenus.add(accountSettingMenu)
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.toSeq
def getAccountSettingMenus: Seq[(Context) => Option[Link]] = accountSettingMenus.asScala.toSeq
def addIssueSidebar(issueSidebar: (Issue, RepositoryInfo, Context) => Option[Html]): Unit = issueSidebars += issueSidebar
def addDashboardTab(dashboardTab: (Context) => Option[Link]): Unit = dashboardTabs.add(dashboardTab)
def getIssueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = issueSidebars.toSeq
def getDashboardTabs: Seq[(Context) => Option[Link]] = dashboardTabs.asScala.toSeq
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings += assetsMapping
def addIssueSidebar(issueSidebar: (Issue, RepositoryInfo, Context) => Option[Html]): Unit = issueSidebars.add(issueSidebar)
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.toSeq
def getIssueSidebars: Seq[(Issue, RepositoryInfo, Context) => Option[Html]] = issueSidebars.asScala.toSeq
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators += textDecorator
def addAssetsMapping(assetsMapping: (String, String, ClassLoader)): Unit = assetsMappings.add(assetsMapping)
def getTextDecorators: Seq[TextDecorator] = textDecorators.toSeq
def getAssetsMappings: Seq[(String, String, ClassLoader)] = assetsMappings.asScala.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders += suggestionProvider
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators.add(textDecorator)
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.toSeq
def getTextDecorators: Seq[TextDecorator] = textDecorators.asScala.toSeq
def addSuggestionProvider(suggestionProvider: SuggestionProvider): Unit = suggestionProviders.add(suggestionProvider)
def getSuggestionProviders: Seq[SuggestionProvider] = suggestionProviders.asScala.toSeq
}
/**
@@ -180,79 +187,237 @@ object PluginRegistry {
private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
private val instance = new PluginRegistry()
private var instance = new PluginRegistry()
private var watcher: PluginWatchThread = null
private var extraWatcher: PluginWatchThread = null
private val initializing = new AtomicBoolean(false)
/**
* Returns the PluginRegistry singleton instance.
*/
def apply(): PluginRegistry = instance
/**
* Reload all plugins.
*/
def reload(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
shutdown(context, settings)
instance = new PluginRegistry()
initialize(context, settings, conn)
}
/**
* Uninstall a specified plugin.
*/
def uninstall(pluginId: String, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
instance.getPlugins()
.collect { case plugin if plugin.pluginId == pluginId => plugin }
.foreach { plugin =>
// try {
// plugin.pluginClass.uninstall(instance, context, settings)
// } catch {
// case e: Exception =>
// logger.error(s"Error during uninstalling plugin: ${plugin.pluginJar.getName}", e)
// }
shutdown(context, settings)
plugin.pluginJar.delete()
instance = new PluginRegistry()
initialize(context, settings, conn)
}
}
/**
* Install a plugin from a specified jar file.
*/
def install(file: File, context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
shutdown(context, settings)
FileUtils.copyFile(file, new File(PluginHome, file.getName))
instance = new PluginRegistry()
initialize(context, settings, conn)
}
private def listPluginJars(dir: File): Seq[File] = {
dir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).toSeq.sortBy(_.getName).reverse
}
lazy val extraPluginDir: Option[String] = Option(System.getProperty("gitbucket.pluginDir"))
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = synchronized {
val pluginDir = new File(PluginHome)
val manager = new JDBCVersionManager(conn)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
}).sortBy(_.getName).foreach { pluginJar =>
val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
// Clean installed directory
val installedDir = new File(PluginHome, ".installed")
if(installedDir.exists){
FileUtils.deleteDirectory(installedDir)
}
installedDir.mkdirs()
// Migration
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
val pluginJars = listPluginJars(pluginDir)
val extraJars = extraPluginDir.map { extraDir => listPluginJars(new File(extraDir)) }.getOrElse(Nil)
// 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}")
(extraJars ++ pluginJars).foreach { pluginJar =>
val installedJar = new File(installedDir, pluginJar.getName)
FileUtils.copyFile(pluginJar, installedJar)
logger.info(s"Initialize ${pluginJar.getName}")
val classLoader = new URLClassLoader(Array(installedJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
try {
val plugin = classLoader.loadClass("Plugin").getDeclaredConstructor().newInstance().asInstanceOf[Plugin]
val pluginId = plugin.pluginId
// Check duplication
instance.getPlugins().find(_.pluginId == pluginId) match {
case Some(x) => {
logger.warn(s"Plugin ${pluginId} is duplicated. ${x.pluginJar.getName} is available.")
}
case None => {
// Migration
val solidbase = new Solidbase()
solidbase.migrate(conn, classLoader, DatabaseConfig.liquiDriver, new Module(plugin.pluginId, plugin.versions: _*))
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion,
description = plugin.description,
pluginClass = plugin
))
// Check database 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}")
}
} catch {
case e: Throwable => {
logger.error(s"Error during plugin initialization: ${pluginJar.getAbsolutePath}", e)
// Initialize
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
pluginVersion = plugin.versions.last.getVersion,
description = plugin.description,
pluginClass = plugin,
pluginJar = pluginJar,
classLoader = classLoader
))
}
}
} catch {
case e: Throwable => logger.error(s"Error during plugin initialization: ${pluginJar.getName}", e)
}
}
if(watcher == null){
watcher = new PluginWatchThread(context, PluginHome)
watcher.start()
}
extraPluginDir.foreach { extraDir =>
if(extraWatcher == null){
extraWatcher = new PluginWatchThread(context, extraDir)
extraWatcher.start()
}
}
}
def shutdown(context: ServletContext, settings: SystemSettings): Unit = {
instance.getPlugins().foreach { pluginInfo =>
def shutdown(context: ServletContext, settings: SystemSettings): Unit = synchronized {
instance.getPlugins().foreach { plugin =>
try {
pluginInfo.pluginClass.shutdown(instance, context, settings)
plugin.pluginClass.shutdown(instance, context, settings)
if(watcher != null){
watcher.interrupt()
watcher = null
}
if(extraWatcher != null){
extraWatcher.interrupt()
extraWatcher = null
}
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown", e)
logger.error(s"Error during plugin shutdown: ${plugin.pluginJar.getName}", e)
}
} finally {
plugin.classLoader.close()
}
}
}
}
case class Link(id: String, label: String, path: String, icon: Option[String] = None)
case class Link(
id: String,
label: String,
path: String,
icon: Option[String] = None
)
class PluginInfoBase(
val pluginId: String,
val pluginName: String,
val pluginVersion: String,
val description: String
)
case class PluginInfo(
pluginId: String,
pluginName: String,
pluginVersion: String,
description: String,
pluginClass: Plugin
)
override val pluginId: String,
override val pluginName: String,
override val pluginVersion: String,
override val description: String,
pluginClass: Plugin,
pluginJar: File,
classLoader: URLClassLoader
) extends PluginInfoBase(pluginId, pluginName, pluginVersion, description)
class PluginWatchThread(context: ServletContext, dir: String) extends Thread with SystemSettingsService {
import gitbucket.core.model.Profile.profile.blockingApi._
import scala.collection.JavaConverters._
private val logger = LoggerFactory.getLogger(classOf[PluginWatchThread])
override def run(): Unit = {
val path = Paths.get(dir)
if(!Files.exists(path)){
Files.createDirectories(path)
}
val fs = path.getFileSystem
val watcher = fs.newWatchService
val watchKey = path.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.OVERFLOW)
logger.info("Start PluginWatchThread: " + path)
try {
while (watchKey.isValid()) {
val detectedWatchKey = watcher.take()
val events = detectedWatchKey.pollEvents.asScala.filter { e =>
e.context.toString != ".installed" && !e.context.toString.endsWith(".bak")
}
if(events.nonEmpty){
events.foreach { event =>
logger.info(event.kind + ": " + event.context)
}
new Thread {
override def run(): Unit = {
gitbucket.core.servlet.Database() withTransaction { session =>
logger.info("Reloading plugins...")
PluginRegistry.reload(context, loadSystemSettings(), session.conn)
logger.info("Reloading finished.")
}
}
}.start()
}
detectedWatchKey.reset()
}
} catch {
case _: InterruptedException => watchKey.cancel()
}
logger.info("Shutdown PluginWatchThread")
}
}

View File

@@ -0,0 +1,41 @@
package gitbucket.core.plugin
import org.json4s._
import gitbucket.core.util.Directory._
import org.apache.commons.io.FileUtils
object PluginRepository {
implicit val formats = DefaultFormats
def parsePluginJson(json: String): Seq[PluginMetadata] = {
org.json4s.jackson.JsonMethods.parse(json).extract[Seq[PluginMetadata]]
}
lazy val LocalRepositoryDir = new java.io.File(PluginHome, ".repository")
lazy val LocalRepositoryIndexFile = new java.io.File(LocalRepositoryDir, "plugins.json")
def getPlugins(): Seq[PluginMetadata] = {
if(LocalRepositoryIndexFile.exists){
parsePluginJson(FileUtils.readFileToString(LocalRepositoryIndexFile, "UTF-8"))
} else Nil
}
}
// Mapped from plugins.json
case class PluginMetadata(
id: String,
name: String,
description: String,
versions: Seq[VersionDef],
default: Boolean = false
){
lazy val latestVersion: VersionDef = versions.last
}
case class VersionDef(
version: String,
file: String,
range: String
)

View File

@@ -50,7 +50,7 @@ trait HandleCommentService {
id
}
actionActivity foreach ( _ (owner, name, userName, issue.issueId, issue.title) )
actionActivity.foreach { f => f(owner, name, userName, issue.issueId, issue.title) }
// call web hooks
action match {

View File

@@ -32,8 +32,11 @@ trait IssuesService {
.list
def getMergedComment(owner: String, repository: String, issueId: Int)(implicit s: Session): Option[(IssueComment, Account)] = {
getCommentsForApi(owner, repository, issueId)
.collectFirst { case (comment, account, _) if comment.action == "merged" => (comment, account) }
IssueComments.filter(_.byIssue(owner, repository, issueId))
.filter(_.action === "merge".bind)
.join(Accounts).on { case t1 ~ t2 => t1.commentedUserName === t2.userName }
.map { case t1 ~ t2 => (t1, t2)}
.firstOption
}
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session): Option[IssueComment] = {
@@ -199,15 +202,16 @@ trait IssuesService {
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos)
.join(PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.join(Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
.join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => (t1, t5, t2.commentCount, t3, t4, t6) }
.join (PullRequests).on { case t1 ~ t2 ~ i ~ t3 => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.join (Repositories).on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byRepository(t1.userName, t1.repositoryName) }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join (Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.joinLeft(Accounts ).on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName}
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.list
}
@@ -327,6 +331,7 @@ trait IssuesService {
def createComment(owner: String, repository: String, loginUser: String,
issueId: Int, content: String, action: String)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments returning IssueComments.map(_.commentId) insert IssueComment(
userName = owner,
repositoryName = repository,
@@ -342,31 +347,33 @@ trait IssuesService {
Issues
.filter (_.byPrimaryKey(owner, repository, issueId))
.map { t => (t.title, t.content.?, t.updatedDate) }
.update (title, content, currentDate)
.update(title, content, currentDate)
}
def updateAssignedUserName(owner: String, repository: String, issueId: Int, assignedUserName: Option[String])(implicit s: Session): Int = {
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.assignedUserName?).update (assignedUserName)
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.assignedUserName?, t.updatedDate)).update(assignedUserName, currentDate)
}
def updateMilestoneId(owner: String, repository: String, issueId: Int, milestoneId: Option[Int])(implicit s: Session): Int = {
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId)
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.milestoneId?, t.updatedDate)).update(milestoneId, currentDate)
}
def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int])(implicit s: Session): Int = {
Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.priorityId?).update (priorityId)
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.priorityId?, t.updatedDate)).update(priorityId, currentDate)
}
def updateComment(commentId: Int, content: String)(implicit s: Session): Int = {
IssueComments.filter (_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
def updateComment(issueId: Int, commentId: Int, content: String)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate)
}
def deleteComment(commentId: Int)(implicit s: Session): Int = {
IssueComments filter (_.byPrimaryKey(commentId)) delete
def deleteComment(issueId: Int, commentId: Int)(implicit s: Session): Int = {
Issues.filter(_.issueId === issueId.bind).map(_.updatedDate).update(currentDate)
IssueComments.filter(_.byPrimaryKey(commentId)).delete
}
def updateClosed(owner: String, repository: String, issueId: Int, closed: Boolean)(implicit s: Session): Int = {
(Issues filter (_.byPrimaryKey(owner, repository, issueId)) map(t => (t.closed, t.updatedDate))).update((closed, currentDate))
Issues.filter(_.byPrimaryKey(owner, repository, issueId)).map(t => (t.closed, t.updatedDate)).update(closed, currentDate)
}
/**
@@ -449,9 +456,8 @@ trait IssuesService {
def createIssueComment(owner: String, repository: String, commit: CommitInfo)(implicit s: Session): Unit = {
extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
val userName = getAccountByMailAddress(commit.committerEmailAddress).map(_.userName).getOrElse(commit.committerName)
createComment(owner, repository, userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
@@ -497,14 +503,14 @@ object IssuesService {
).flatten ++
labels.map(label => s"label:${label}") ++
List(
milestone.map { _ match {
milestone.map {
case Some(x) => s"milestone:${x}"
case None => "no:milestone"
}},
priority.map { _ match {
},
priority.map {
case Some(x) => s"priority:${x}"
case None => "no:priority"
}},
},
(sort, direction) match {
case ("created" , "desc") => None
case ("created" , "asc" ) => Some("sort:created-asc")

View File

@@ -163,7 +163,7 @@ object MergeService{
case e: NoMergeBaseException => true
}
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
val committer = mergeTipCommit.getCommitterIdent;
val committer = mergeTipCommit.getCommitterIdent
def updateBranch(treeId:ObjectId, message:String, branchName:String){
// creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message)

View File

@@ -18,10 +18,11 @@ trait ProtectedBranchService {
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
.headOption
.map { p => p._1 -> p._2.flatMap(_._2) }
.map { case (t1, contexts) =>
new ProtectedBranchInfo(t1.userName, t1.repositoryName, true, contexts, t1.statusCheckAdmin)
}.headOption
}
def getProtectedBranchInfo(owner: String, repository: String, branch: String)(implicit session: Session): ProtectedBranchInfo =
getProtectedBranchInfoOpt(owner, repository, branch).getOrElse(ProtectedBranchInfo.disabled(owner, repository))
@@ -45,12 +46,17 @@ trait ProtectedBranchService {
object ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService with RepositoryService with AccountService {
override def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = {
val branch = command.getRefName.stripPrefix("refs/heads/")
if(branch != command.getRefName){
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
val repositoryInfo = getRepository(owner, repository)
if(command.getType == ReceiveCommand.Type.DELETE && repositoryInfo.exists(_.repository.defaultBranch == branch)){
Some(s"refusing to delete the branch: ${command.getRefName}.")
} else {
getProtectedBranchInfo(owner, repository, branch).getStopReason(receivePack.isAllowNonFastForwards, command, pusher)
}
} else {
None
}

View File

@@ -94,9 +94,9 @@ trait PullRequestService { self: IssuesService with CommitsService =>
/**
* for repository viewer.
* 1. find pull request from from `branch` to othre branch on same repository
* 1. find pull request from `branch` to other branch on same repository
* 1. return if exists pull request to `defaultBranch`
* 2. return if exists pull request to othre branch
* 2. return if exists pull request to other branch
* 2. return None
*/
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
@@ -230,7 +230,7 @@ trait PullRequestService { self: IssuesService with CommitsService =>
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true, false)
(commits, diffs)
}
@@ -256,7 +256,7 @@ object PullRequestService {
val statuses: List[CommitStatus] =
commitStatues ++ (branchProtection.contexts.toSet -- commitStatues.map(_.context).toSet).map(CommitStatus.pending(branchProtection.owner, branchProtection.repository, _))
val hasRequiredStatusProblem = needStatusCheck && branchProtection.contexts.exists(context => statuses.find(_.context == context).map(_.state) != Some(CommitState.SUCCESS))
val hasProblem = hasRequiredStatusProblem || hasConflict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val hasProblem = hasRequiredStatusProblem || hasConflict || (statuses.nonEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(CommitState, String) = {

View File

@@ -67,7 +67,7 @@ trait RepositorySearchService { self: IssuesService =>
files.map { case (path, text) =>
val (highlightText, lineNumber) = getHighlightText(text, query)
FileSearchResult(
path.replaceFirst("\\.md$", ""),
path.stripSuffix(".md"),
commits(path).getCommitterIdent.getWhen,
highlightText,
lineNumber)

View File

@@ -135,7 +135,7 @@ trait RepositoryService { self: AccountService =>
repositoryName = newRepositoryName
)) :_*)
// TODO Drop transfered owner from collaborators?
// TODO Drop transferred owner from collaborators?
Collaborators.insertAll(collaborators.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update activity messages
@@ -413,6 +413,15 @@ trait RepositoryService { self: AccountService =>
q1.union(q2).list.filter { x => filter.isEmpty || filter.exists(_.name == x._2) }.map(_._1)
}
def hasOwnerRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {
case Some(a) if(a.isAdmin) => true
case Some(a) if(a.userName == owner) => true
case Some(a) if(getGroupMembers(owner).exists(_.userName == a.userName)) => true
case Some(a) if(getCollaboratorUserNames(owner, repository, Seq(Role.ADMIN)).contains(a.userName)) => true
case _ => false
}
}
def hasDeveloperRole(owner: String, repository: String, loginAccount: Option[Account])(implicit s: Session): Boolean = {
loginAccount match {

View File

@@ -54,6 +54,7 @@ trait SystemSettingsService {
ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
}
}
props.setProperty(SkinName, settings.skinName.toString)
using(new java.io.FileOutputStream(GitBucketConf)){ out =>
props.store(out, null)
}
@@ -111,7 +112,8 @@ trait SystemSettingsService {
getOptionValue(props, LdapKeystore, None)))
} else {
None
}
},
getValue(props, SkinName, "skin-blue")
)
}
}
@@ -136,18 +138,18 @@ object SystemSettingsService {
useSMTP: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
ldap: Option[Ldap],
skinName: String){
def sshAddress:Option[SshAddress] =
for {
host <- sshHost if ssh
}
yield SshAddress(
host,
sshPort.getOrElse(DefaultSshPort),
"git"
)
def baseUrl(request: HttpServletRequest): String = baseUrl.fold {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
} (_.stripSuffix("/"))
def sshAddress:Option[SshAddress] = sshHost.collect { case host if ssh =>
SshAddress(host, sshPort.getOrElse(DefaultSshPort), "git")
}
}
case class Ldap(
@@ -219,6 +221,7 @@ object SystemSettingsService {
private val LdapTls = "ldap.tls"
private val LdapSsl = "ldap.ssl"
private val LdapKeystore = "ldap.keystore"
private val SkinName = "skinName"
private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
getSystemProperty(key).getOrElse(getEnvironmentVariable(key).getOrElse {

View File

@@ -228,16 +228,18 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest){
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -273,12 +275,14 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._
for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -296,16 +300,17 @@ trait WebHookPullRequestService extends WebHookService {
trait WebHookPullRequestReviewCommentService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService with CommitsService =>
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
def callPullRequestReviewCommentWebHook(action: String, comment: CommitComment, repository: RepositoryService.RepositoryInfo,
issue: Issue, pullRequest: PullRequest, baseUrl: String, sender: Account)
(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
val users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
for{
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName => getAccountByUserName(userName, false) }
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestReviewCommentPayload(
@@ -313,6 +318,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment,
issue = issue,
issueUser = issueUser,
assignee = assignee,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -367,9 +373,9 @@ object WebHookService {
repository: ApiRepository
) extends FieldSerializable with WebHookPayload {
val compare = commits.size match {
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initalied repository
case 0 => ApiPath(s"/${repository.full_name}") // maybe test hook on un-initialized repository
case 1 => ApiPath(s"/${repository.full_name}/commit/${after}")
case _ if before.filterNot(_=='0').isEmpty => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ if before.forall(_=='0') => ApiPath(s"/${repository.full_name}/compare/${commits.head.id}^...${after}")
case _ => ApiPath(s"/${repository.full_name}/compare/${before}...${after}")
}
val head_commit = commits.lastOption
@@ -424,6 +430,7 @@ object WebHookService {
def apply(action: String,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -441,6 +448,7 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
)
@@ -495,6 +503,7 @@ object WebHookService {
comment: CommitComment,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -502,7 +511,7 @@ object WebHookService {
baseOwner: Account,
sender: Account,
mergedComment: Option[(IssueComment, Account)]
) : WebHookPullRequestReviewCommentPayload = {
): WebHookPullRequestReviewCommentPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
@@ -521,6 +530,7 @@ object WebHookService {
headRepo = headRepoPayload,
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
assignee = assignee.map(ApiUser.apply),
mergedComment = mergedComment
),
repository = baseRepoPayload,

View File

@@ -237,7 +237,7 @@ trait WikiService {
builder.finish()
val newHeadId = JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, committer.fullName, committer.mailAddress,
if(message.trim.length == 0) {
if(message.trim.isEmpty) {
if(removed){
s"Rename ${currentPageName} to ${newPageName}"
} else if(created){

View File

@@ -1,37 +0,0 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.service.SystemSettingsService
/**
* A controller to provide GitHub compatible URL for Git clients.
*/
class GHCompatRepositoryAccessFilter extends Filter with SystemSettingsService {
/**
* Pattern of GitHub compatible repository URL.
* <code>/:user/:repo.git/</code>
*/
private val githubRepositoryPattern = """^/[^/]+/[^/]+\.git/.*""".r
override def init(filterConfig: FilterConfig) = {}
override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) = {
implicit val request = req.asInstanceOf[HttpServletRequest]
val agent = request.getHeader("USER-AGENT")
val response = res.asInstanceOf[HttpServletResponse]
val requestPath = request.getRequestURI.substring(request.getContextPath.length)
requestPath match {
case githubRepositoryPattern() if agent != null && agent.toLowerCase.indexOf("git") >= 0 =>
response.sendRedirect(baseUrl + "/git" + requestPath)
case _ =>
chain.doFilter(req, res)
}
}
override def destroy() = {}
}

View File

@@ -74,7 +74,7 @@ class GitAuthenticationFilter extends Filter with RepositoryService with Account
val action = request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) =>
Database() withSession { implicit session =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
getRepository(repositoryOwner, repositoryName.replaceFirst("(\\.wiki)?\\.git$", "")) match {
case Some(repository) => {
val execute = if (!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess) {
// Authentication is not required

View File

@@ -156,9 +156,13 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
logger.debug("repository:" + owner + "/" + repository)
val settings = loadSystemSettings()
val baseUrl = settings.baseUrl(request)
val sshUrl = settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" }
if(!repository.endsWith(".wiki")){
defining(request) { implicit r =>
val hook = new CommitLogHook(owner, repository, pusher, baseUrl)
val hook = new CommitLogHook(owner, repository, pusher, baseUrl, sshUrl)
receivePack.setPreReceiveHook(hook)
receivePack.setPostReceiveHook(hook)
}
@@ -166,7 +170,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
if(repository.endsWith(".wiki")){
defining(request) { implicit r =>
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.replaceFirst("\\.wiki$", ""), pusher, baseUrl))
receivePack.setPostReceiveHook(new WikiCommitHook(owner, repository.stripSuffix(".wiki"), pusher, baseUrl, sshUrl))
}
}
}
@@ -178,7 +182,7 @@ class GitBucketReceivePackFactory extends ReceivePackFactory[HttpServletRequest]
import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService with CommitsService {
@@ -219,7 +223,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
implicit val apiContext = api.JsonFormat.Context(baseUrl)
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
@@ -320,7 +324,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String)
class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl: String, sshUrl: Option[String])
extends PostReceiveHook with WebHookService with AccountService with RepositoryService {
private val logger = LoggerFactory.getLogger(classOf[WikiCommitHook])
@@ -329,7 +333,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
Database() withTransaction { implicit session =>
try {
commands.asScala.headOption.foreach { command =>
implicit val apiContext = api.JsonFormat.Context(baseUrl)
implicit val apiContext = api.JsonFormat.Context(baseUrl, sshUrl)
val refName = command.getRefName.split("/")
val commitIds = if (refName(1) == "tags") {
None
@@ -347,7 +351,7 @@ class WikiCommitHook(owner: String, repository: String, pusher: String, baseUrl:
diffs._1.collect { case diff if diff.newPath.toLowerCase.endsWith(".md") =>
val action = if(diff.changeType == ChangeType.ADD) "created" else "edited"
val fileName = diff.newPath
println(action + " - " + fileName + " - " + commit.id)
//println(action + " - " + fileName + " - " + commit.id)
(action, fileName, commit.id)
}
}

View File

@@ -1,25 +1,30 @@
package gitbucket.core.servlet
import java.io.File
import java.io.{File, FileOutputStream}
import akka.event.Logging
import com.typesafe.config.ConfigFactory
import gitbucket.core.GitBucketCoreModule
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.plugin.{PluginRegistry, PluginRepository}
import gitbucket.core.service.{ActivityService, SystemSettingsService}
import gitbucket.core.util.DatabaseConfig
import gitbucket.core.util.Directory._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.util.JDBCUtil._
import gitbucket.core.model.Profile.profile.blockingApi._
import io.github.gitbucket.solidbase.Solidbase
import io.github.gitbucket.solidbase.manager.JDBCVersionManager
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextEvent, ServletContextListener}
import org.apache.commons.io.{FileUtils, IOUtils}
import org.slf4j.LoggerFactory
import akka.actor.{Actor, Props, ActorSystem}
import akka.actor.{Actor, ActorSystem, Props}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import com.github.zafarkhaja.semver.{Version => Semver}
import scala.collection.JavaConverters._
/**
* Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing.
@@ -54,44 +59,11 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
val manager = new JDBCVersionManager(conn)
// Check version
val versionFile = new File(GitBucketHome, "version")
if(versionFile.exists()){
val version = FileUtils.readFileToString(versionFile, "UTF-8")
if(version == "3.14"){
// Initialization for GitBucket 3.14
logger.info("Migration to GitBucket 4.x start")
// Backup current data
val dataMvFile = new File(GitBucketHome, "data.mv.db")
if(dataMvFile.exists) {
FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
}
val dataTraceFile = new File(GitBucketHome, "data.trace.db")
if(dataTraceFile.exists) {
FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
}
// Change form
manager.initialize()
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
}
conn.update("DROP TABLE PLUGIN")
versionFile.delete()
logger.info("Migration to GitBucket 4.x completed")
} else {
throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
}
}
checkVersion(manager, conn)
// Run normal migration
logger.info("Start schema update")
val solidbase = new Solidbase()
solidbase.migrate(conn, Thread.currentThread.getContextClassLoader, DatabaseConfig.liquiDriver, GitBucketCoreModule)
new 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
@@ -106,6 +78,9 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
throw new IllegalStateException(s"Initialization failed. GitBucket version is ${gitbucketVersion}, but database version is ${databaseVersion}.")
}
// Install bundled plugins
extractBundledPlugins(gitbucketVersion)
// Load plugins
logger.info("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
@@ -117,7 +92,76 @@ class InitializeListener extends ServletContextListener with SystemSettingsServi
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
private def checkVersion(manager: JDBCVersionManager, conn: java.sql.Connection): Unit = {
logger.info("Check version")
val versionFile = new File(GitBucketHome, "version")
if(versionFile.exists()){
val version = FileUtils.readFileToString(versionFile, "UTF-8")
if(version == "3.14"){
// Initialization for GitBucket 3.14
logger.info("Migration to GitBucket 4.x start")
// Backup current data
val dataMvFile = new File(GitBucketHome, "data.mv.db")
if(dataMvFile.exists) {
FileUtils.copyFile(dataMvFile, new File(GitBucketHome, "data.mv.db_3.14"))
}
val dataTraceFile = new File(GitBucketHome, "data.trace.db")
if(dataTraceFile.exists) {
FileUtils.copyFile(dataTraceFile, new File(GitBucketHome, "data.trace.db_3.14"))
}
// Change form
manager.initialize()
manager.updateVersion(GitBucketCoreModule.getModuleId, "4.0.0")
conn.select("SELECT PLUGIN_ID, VERSION FROM PLUGIN"){ rs =>
manager.updateVersion(rs.getString("PLUGIN_ID"), rs.getString("VERSION"))
}
conn.update("DROP TABLE PLUGIN")
versionFile.delete()
logger.info("Migration to GitBucket 4.x completed")
} else {
throw new Exception("GitBucket can't migrate from this version. Please update to 3.14 at first.")
}
}
}
private def extractBundledPlugins(gitbucketVersion: String): Unit = {
logger.info("Extract bundled plugins")
val cl = Thread.currentThread.getContextClassLoader
try {
using(cl.getResourceAsStream("plugins/plugins.json")){ pluginsFile =>
if(pluginsFile != null){
val pluginsJson = IOUtils.toString(pluginsFile, "UTF-8")
FileUtils.forceMkdir(PluginRepository.LocalRepositoryDir)
FileUtils.write(PluginRepository.LocalRepositoryIndexFile, pluginsJson, "UTF-8")
val plugins = PluginRepository.parsePluginJson(pluginsJson)
plugins.foreach { plugin =>
plugin.versions.sortBy { x => Semver.valueOf(x.version) }.reverse.zipWithIndex.foreach { case (version, i) =>
val file = new File(PluginRepository.LocalRepositoryDir, version.file)
if(!file.exists) {
logger.info(s"Copy ${plugin} to ${file.getAbsolutePath}")
FileUtils.forceMkdirParent(file)
using(cl.getResourceAsStream("plugins/" + version.file), new FileOutputStream(file)){ case (in, out) => IOUtils.copy(in, out) }
if(plugin.default && i == 0){
logger.info(s"Enable ${file.getName} in default")
FileUtils.copyFile(file, new File(PluginHome, version.file))
}
}
}
}
}
}
} catch {
case e: Exception => logger.error("Error in extracting bundled plugin", e)
}
}
override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown Quartz scheduler
@@ -146,4 +190,4 @@ class DeleteOldActivityActor extends Actor with SystemSettingsService with Activ
}
}
}
}
}

View File

@@ -19,7 +19,7 @@ class PluginAssetsServlet extends HttpServlet {
.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))
Option(classLoader.getResourceAsStream(resourcePath.stripPrefix("/") + resourceName))
}
.map { in =>
try {

View File

@@ -0,0 +1,45 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.HttpServletRequest
import gitbucket.core.controller.ControllerBase
import gitbucket.core.plugin.PluginRegistry
class PluginControllerFilter extends Filter {
private var filterConfig: FilterConfig = null
override def init(filterConfig: FilterConfig): Unit = {
this.filterConfig = filterConfig
}
override def destroy(): Unit = {
PluginRegistry().getControllers().foreach { case (controller, _) =>
controller.destroy()
}
}
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
val controller = PluginRegistry().getControllers().filter { case (_, path) =>
val requestUri = request.asInstanceOf[HttpServletRequest].getRequestURI
val start = path.replaceFirst("/\\*$", "/")
(requestUri + "/").startsWith(start)
}
val filterChainWrapper = controller.foldLeft(chain){ case (chain, (controller, _)) =>
new FilterChainWrapper(controller, chain)
}
filterChainWrapper.doFilter(request, response)
}
class FilterChainWrapper(controller: ControllerBase, chain: FilterChain) extends FilterChain {
override def doFilter(request: ServletRequest, response: ServletResponse): Unit = {
if(controller.config == null){
controller.init(filterConfig)
}
controller.doFilter(request, response, chain)
}
}
}

View File

@@ -19,7 +19,7 @@ import org.apache.sshd.server.scp.UnknownCommand
import org.eclipse.jgit.errors.RepositoryNotFoundException
object GitCommand {
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+).git'\Z""".r
val DefaultCommandRegex = """\Agit-(upload|receive)-pack '/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-\+_.]+).git'\Z""".r
val SimpleCommandRegex = """\Agit-(upload|receive)-pack '/(.+\.git)'\Z""".r
}
@@ -154,7 +154,7 @@ class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCo
}
}
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String, sshUrl: Option[String]) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService with DeployKeyService {
override protected def runTask(authType: AuthType): Unit = {
@@ -169,7 +169,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
val repository = git.getRepository
val receive = new ReceivePack(repository)
if (!repoName.endsWith(".wiki")) {
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl)
val hook = new CommitLogHook(owner, repoName, userName(authType), baseUrl, sshUrl)
receive.setPreReceiveHook(hook)
receive.setPostReceiveHook(hook)
}
@@ -216,7 +216,7 @@ class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) exte
}
class GitCommandFactory(baseUrl: String) extends CommandFactory {
class GitCommandFactory(baseUrl: String, sshUrl: Option[String]) extends CommandFactory {
private val logger = LoggerFactory.getLogger(classOf[GitCommandFactory])
override def createCommand(command: String): Command = {
@@ -227,7 +227,7 @@ class GitCommandFactory(baseUrl: String) extends CommandFactory {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl)
case DefaultCommandRegex("receive", owner, repoName) => new DefaultGitReceivePack(owner, repoName, baseUrl, sshUrl)
case _ => new UnknownCommand(command)
}
}

View File

@@ -6,7 +6,7 @@ import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.{Directory}
import gitbucket.core.util.Directory
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
@@ -22,7 +22,7 @@ object SshServer {
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator(sshAddress.genericUser))
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setCommandFactory(new GitCommandFactory(baseUrl, Some(s"${sshAddress.genericUser}@${sshAddress.host}:${sshAddress.port}")))
server.setShellFactory(new NoShell(sshAddress))
}

View File

@@ -92,7 +92,7 @@ object DatabaseType {
}
object MySQL extends DatabaseType {
val jdbcDriver = "com.mysql.jdbc.Driver"
val jdbcDriver = "org.mariadb.jdbc.Driver"
val slickDriver = BlockingMySQLDriver
val liquiDriver = new MySQLDatabase()
}

View File

@@ -90,4 +90,4 @@ object Directory {
def getWikiRepositoryDir(owner: String, repository: String): File =
new File(s"${RepositoryHome}/${owner}/${repository}.wiki.git")
}
}

View File

@@ -28,8 +28,6 @@ object FileUtil {
def isImage(name: String): Boolean = getMimeType(name).startsWith("image/")
def isUploadableType(name: String): Boolean = mimeTypeWhiteList contains getMimeType(name)
def isLarge(size: Long): Boolean = (size > 1024 * 1000)
def isText(content: Array[Byte]): Boolean = !content.contains(0)
@@ -53,24 +51,29 @@ object FileUtil {
}
}
val mimeTypeWhiteList: Array[String] = Array(
"application/pdf",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"image/gif",
"image/jpeg",
"image/png",
"text/plain")
def getLfsFilePath(owner: String, repository: String, oid: String): String =
Directory.getLfsDir(owner, repository) + "/" + oid
def readableSize(size: Long): String = FileUtils.byteCountToDisplaySize(size)
/**
* Delete the given directory if it's empty.
* Do nothing if the given File is not a directory or not empty.
*/
def deleteDirectoryIfEmpty(dir: File): Unit = {
if(dir.isDirectory() && dir.list().isEmpty) {
FileUtils.deleteDirectory(dir)
}
}
/**
* Delete file or directory forcibly.
*/
def deleteIfExists(file: java.io.File): java.io.File = {
if(file.exists){
FileUtils.forceDelete(file)
}
file
}
}

View File

@@ -22,7 +22,8 @@ object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context =
JsonFormat.Context(context.baseUrl, context.settings.sshAddress.map { x => s"${x.genericUser}@${x.host}:${x.port}" })
implicit class RichSeq[A](private val seq: Seq[A]) extends AnyVal {
@@ -77,11 +78,6 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^" + quote(request.getContextPath) + "/git/", "/")
def baseUrl:String = {
val url = request.getRequestURL.toString
val len = url.length - (request.getRequestURI.length - request.getContextPath.length)
url.substring(0, len).stripSuffix("/")
}
}
implicit class RichSession(private val session: HttpSession) extends AnyVal {

View File

@@ -75,7 +75,7 @@ object JDBCUtil {
var stringLiteral = false
while({ length = in.read(bytes); length != -1 }){
for(i <- 0 to length - 1){
for(i <- 0 until length){
val c = bytes(i)
if(c == '\''){
stringLiteral = !stringLiteral
@@ -146,13 +146,11 @@ object JDBCUtil {
}
}
val columnValues = values.map { value =>
value match {
case x: String => "'" + x.replace("'", "''") + "'"
case x: Timestamp => "'" + dateFormat.format(x) + "'"
case null => "NULL"
case x => x
}
val columnValues = values.map {
case x: String => "'" + x.replace("'", "''") + "'"
case x: Timestamp => "'" + dateFormat.format(x) + "'"
case null => "NULL"
case x => x
}
sb.append(columnValues.mkString(", "))
sb.append(");\n")

View File

@@ -1,5 +1,7 @@
package gitbucket.core.util
import java.io.ByteArrayOutputStream
import gitbucket.core.service.RepositoryService
import org.eclipse.jgit.api.Git
import Directory._
@@ -14,7 +16,7 @@ import org.eclipse.jgit.revwalk.filter._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import org.eclipse.jgit.errors.{ConfigInvalidException, IncorrectObjectTypeException, MissingObjectException}
import org.eclipse.jgit.transport.RefSpec
import java.util.Date
import java.util.concurrent.TimeUnit
@@ -22,6 +24,7 @@ import java.util.function.Consumer
import org.cache2k.{Cache2kBuilder, CacheEntry}
import org.eclipse.jgit.api.errors.{InvalidRefNameException, JGitInternalException, NoHeadException, RefAlreadyExistsException}
import org.eclipse.jgit.diff.{DiffEntry, DiffFormatter}
import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory
@@ -93,7 +96,7 @@ object JGitUtil {
val summary = getSummaryMessage(fullMessage, shortMessage)
val description = defining(fullMessage.trim.indexOf("\n")){ i =>
val description = defining(fullMessage.trim.indexOf('\n')){ i =>
if(i >= 0){
Some(fullMessage.trim.substring(i).trim)
} else None
@@ -114,7 +117,8 @@ object JGitUtil {
newObjectId: Option[String],
oldMode: String,
newMode: String,
tooLarge: Boolean
tooLarge: Boolean,
patch: Option[String]
)
/**
@@ -226,9 +230,14 @@ object JGitUtil {
ref.getName.stripPrefix("refs/heads/")
}.toList,
// tags
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
git.tagList.call.asScala.flatMap { ref =>
try {
val revCommit = getRevCommitFromId(git, ref.getObjectId)
Some(TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName))
} catch {
case _: IncorrectObjectTypeException =>
None
}
}.sortBy(_.time).toList
)
} catch {
@@ -288,7 +297,7 @@ object JGitUtil {
@tailrec
def findLastCommits(result:List[(ObjectId, FileMode, String, String, Option[String], RevCommit)],
restList:List[((ObjectId, FileMode, String, String, Option[String]), Map[RevCommit, RevCommit])],
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] ={
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, String, Option[String], RevCommit)] = {
if(restList.isEmpty){
result
} else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
@@ -359,9 +368,9 @@ object JGitUtil {
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0
case _ => file1.name.compareTo(file2.name) < 0
}
}.toList
}
}
}
@@ -369,7 +378,7 @@ object JGitUtil {
* Returns the first line of the commit message.
*/
private def getSummaryMessage(fullMessage: String, shortMessage: String): String = {
defining(fullMessage.trim.indexOf("\n")){ i =>
defining(fullMessage.trim.indexOf('\n')){ i =>
defining(if(i >= 0) fullMessage.trim.substring(0, i).trim else fullMessage){ firstLine =>
if(firstLine.length > shortMessage.length) shortMessage else firstLine
}
@@ -510,9 +519,10 @@ object JGitUtil {
}
/**
* Returns the tuple of diff of the given commit and the previous commit id.
* Returns the tuple of diff of the given commit and parent commit ids.
* DiffInfos returned from this method don't include the patch property.
*/
def getDiffs(git: Git, id: String, fetchContent: Boolean = true): (List[DiffInfo], Option[String]) = {
def getDiffs(git: Git, id: String, fetchContent: Boolean): (List[DiffInfo], Option[String]) = {
@scala.annotation.tailrec
def getCommitLog(i: java.util.Iterator[RevCommit], logs: List[RevCommit]): List[RevCommit] =
i.hasNext match {
@@ -533,7 +543,7 @@ object JGitUtil {
} else {
commits(1)
}
(getDiffs(git, oldCommit.getName, id, fetchContent), Some(oldCommit.getName))
(getDiffs(git, oldCommit.getName, id, fetchContent, false), Some(oldCommit.getName))
} else {
// initial commit
@@ -546,7 +556,7 @@ object JGitUtil {
buffer.append((if(!fetchContent){
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
oldPath = "",
newPath = treeWalk.getPathString,
oldContent = None,
newContent = None,
@@ -556,12 +566,13 @@ object JGitUtil {
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
tooLarge = false,
patch = None
)
} else {
DiffInfo(
changeType = ChangeType.ADD,
oldPath = null,
oldPath = "",
newPath = treeWalk.getPathString,
oldContent = None,
newContent = JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
@@ -571,7 +582,8 @@ object JGitUtil {
newObjectId = Option(treeWalk.getObjectId(0)).map(_.name),
oldMode = treeWalk.getFileMode(0).toString,
newMode = treeWalk.getFileMode(0).toString,
tooLarge = false
tooLarge = false,
patch = None
)
}))
}
@@ -581,7 +593,7 @@ object JGitUtil {
}
}
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean): List[DiffInfo] = {
def getDiffs(git: Git, from: String, to: String, fetchContent: Boolean, makePatch: Boolean): List[DiffInfo] = {
val reader = git.getRepository.newObjectReader
val oldTreeIter = new CanonicalTreeParser
oldTreeIter.reset(reader, git.getRepository.resolve(from + "^{tree}"))
@@ -607,7 +619,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = true
tooLarge = true,
patch = None
)
} else {
val oldIsImage = FileUtil.isImage(diff.getOldPath)
@@ -625,7 +638,8 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
tooLarge = false,
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
)
} else {
DiffInfo(
@@ -640,13 +654,23 @@ object JGitUtil {
newObjectId = Option(diff.getNewId).map(_.name),
oldMode = diff.getOldMode.toString,
newMode = diff.getNewMode.toString,
tooLarge = false
tooLarge = false,
patch = (if(makePatch) Some(makePatchFromDiffEntry(git, diff)) else None)
)
}
}
}.toList
}
private def makePatchFromDiffEntry(git: Git, diff: DiffEntry): String = {
val out = new ByteArrayOutputStream()
using(new DiffFormatter(out)){ formatter =>
formatter.setRepository(git.getRepository)
formatter.format(diff)
val patch = new String(out.toByteArray) // TODO charset???
patch.split("\n").drop(4).mkString("\n")
}
}
/**
* Returns the list of branch names of the specified commit.
@@ -994,13 +1018,13 @@ object JGitUtil {
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id)).map{ commitId =>
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository)
blamer.setStartCommit(commitId)
blamer.setFilePath(path)
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
val commits = 0.to(blame.getResultContents().size() - 1).map{ i =>
val c = blame.getSourceCommit(i)
if(!blameMap.contains(c.name)){
blameMap += c.name -> JGitUtil.BlameInfo(
@@ -1010,7 +1034,7 @@ object JGitUtil {
c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name),
if(blame.getSourcePath(i)==path){ None }else{ Some(blame.getSourcePath(i)) },
if(blame.getSourcePath(i)==path){ None } else { Some(blame.getSourcePath(i)) },
c.getCommitterIdent.getWhen,
c.getShortMessage,
Set.empty)

View File

@@ -0,0 +1,68 @@
package gitbucket.core.util
import gitbucket.core.model.Account
import gitbucket.core.service.SystemSettingsService
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import SystemSettingsService.SystemSettings
class Mailer(settings: SystemSettings){
def send(to: String, subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
email.addTo(to).send
}
}
def sendBcc(bcc: Seq[String], subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Unit = {
createMail(subject, textMsg, htmlMsg, loginAccount).foreach { email =>
bcc.foreach { address =>
email.addBcc(address)
}
email.send()
}
}
def createMail(subject: String, textMsg: String, htmlMsg: Option[String] = None, loginAccount: Option[Account] = None): Option[HtmlEmail] = {
if(settings.notification == true){
settings.smtp.map { smtp =>
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)
if(ssl == true) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}
smtp.starttls.foreach { starttls =>
email.setStartTLSEnabled(starttls)
email.setStartTLSRequired(starttls)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(loginAccount.map(_.userName).getOrElse("GitBucket")))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.map(_.userName).getOrElse("GitBucket")))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setTextMsg(textMsg)
htmlMsg.foreach { msg =>
email.setHtmlMsg(msg)
}
email
}
} else None
}
}
//class MockMailer extends Notifier {
// def toNotify(subject: String, textMsg: String, htmlMsg: Option[String] = None)
// (recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
//}

View File

@@ -1,212 +0,0 @@
package gitbucket.core.util
import gitbucket.core.model.{Session, Issue, Account}
import gitbucket.core.model.Profile.profile.blockingApi._
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, SystemSettingsService}
import gitbucket.core.servlet.Database
import gitbucket.core.view.Markdown
import scala.concurrent._
import scala.util.{Success, Failure}
import ExecutionContext.Implicits.global
import org.apache.commons.mail.{DefaultAuthenticator, HtmlEmail}
import org.slf4j.LoggerFactory
import gitbucket.core.controller.Context
import SystemSettingsService.Smtp
/**
* The trait for notifications.
* This is used by notifications plugin, which provides notifications feature on GitBucket.
* Please see the plugin for details.
*/
trait Notifier {
def toNotify(subject: String, msg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit
}
object Notifier {
def apply(): Notifier = new SystemSettingsService {}.loadSystemSettings match {
case settings if (settings.notification && settings.useSMTP) => new Mailer(settings.smtp.get)
case _ => new MockMailer
}
// TODO This class is temporary keeping the current feature until Notifications Plugin is available.
class IssueHook extends gitbucket.core.plugin.IssueHook
with RepositoryService with AccountService with IssuesService {
override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message(issue.content getOrElse "", r)(content => s"""
|$content<br/>
|--<br/>
|<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">View it on GitBucket</a>
""".stripMargin)
)(recipients(issue))
}
override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message(content, r)(content => s"""
|$content<br/>
|--<br/>
|<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}#comment-$commentId"}">View it on GitBucket</a>
""".stripMargin)
)(recipients(issue))
}
override def closed(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message("close", r)(content => s"""
|$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">#${issue.issueId}</a>
""".stripMargin)
)(recipients(issue))
}
override def reopened(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message("reopen", r)(content => s"""
|$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/issues/${issue.issueId}"}">#${issue.issueId}</a>
""".stripMargin)
)(recipients(issue))
}
protected def subject(issue: Issue, r: RepositoryService.RepositoryInfo): String =
s"[${r.owner}/${r.name}] ${issue.title} (#${issue.issueId})"
protected def message(content: String, r: RepositoryService.RepositoryInfo)(msg: String => String)(implicit context: Context): String =
msg(Markdown.toHtml(
markdown = content,
repository = r,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = false,
enableLineBreaks = false
))
protected val recipients: Issue => Account => Session => Seq[String] = {
issue => loginAccount => implicit session =>
(
// individual repository's owner
issue.userName ::
// group members of group repository
getGroupMembers(issue.userName).map(_.userName) :::
// collaborators
getCollaboratorUserNames(issue.userName, issue.repositoryName) :::
// participants
issue.openedUserName ::
getComments(issue.userName, issue.repositoryName, issue.issueId).map(_.commentedUserName)
)
.distinct
.withFilter ( _ != loginAccount.userName ) // the operation in person is excluded
.flatMap (
getAccountByUserName(_)
.filterNot (_.isGroupAccount)
.filterNot (LDAPUtil.isDummyMailAddress)
.map (_.mailAddress)
)
}
}
// TODO This class is temporary keeping the current feature until Notifications Plugin is available.
class PullRequestHook extends IssueHook with gitbucket.core.plugin.PullRequestHook {
override def created(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
val url = s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}"
Notifier().toNotify(
subject(issue, r),
message(issue.content getOrElse "", r)(content => s"""
|$content<hr/>
|View, comment on, or merge it at:<br/>
|<a href="$url">$url</a>
""".stripMargin)
)(recipients(issue))
}
override def addedComment(commentId: Int, content: String, issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message(content, r)(content => s"""
|$content<br/>
|--<br/>
|<a href="${s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}#comment-$commentId"}">View it on GitBucket</a>
""".stripMargin)
)(recipients(issue))
}
override def merged(issue: Issue, r: RepositoryService.RepositoryInfo)(implicit context: Context): Unit = {
Notifier().toNotify(
subject(issue, r),
message("merge", r)(content => s"""
|$content <a href="${s"${context.baseUrl}/${r.owner}/${r.name}/pull/${issue.issueId}"}">#${issue.issueId}</a>
""".stripMargin)
)(recipients(issue))
}
}
}
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(subject: String, msg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = {
context.loginAccount.foreach { loginAccount =>
val database = Database()
val f = Future {
database withSession { session =>
recipients(loginAccount)(session) foreach { to =>
send(to, subject, msg, loginAccount)
}
}
"Notifications Successful."
}
f.onComplete {
case Success(s) => logger.debug(s)
case Failure(t) => logger.error("Notifications Failed.", t)
}
}
}
def send(to: String, subject: String, msg: String, loginAccount: Account): 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)
if(ssl == true) {
email.setSslSmtpPort(smtp.port.get.toString)
}
}
smtp.starttls.foreach { starttls =>
email.setStartTLSEnabled(starttls)
email.setStartTLSRequired(starttls)
}
smtp.fromAddress
.map (_ -> smtp.fromName.getOrElse(loginAccount.userName))
.orElse (Some("notifications@gitbucket.com" -> loginAccount.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 {
def toNotify(subject: String, msg: String)
(recipients: Account => Session => Seq[String])(implicit context: Context): Unit = ()
}

View File

@@ -24,7 +24,7 @@ trait Validations {
*/
def password: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(!value.matches("[a-zA-Z0-9\\-_.]+")){
if(System.getProperty("gitbucket.validate.password") != "false" && !value.matches("[a-zA-Z0-9\\-_.]+")){
Some(s"${name} contains invalid character.")
} else {
None

View File

@@ -128,7 +128,7 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean, enableAnchor: Boolean)(implicit context: Context): Html = {
val fileName = filePath.reverse.head.toLowerCase
val fileName = filePath.last.toLowerCase
val extension = FileUtil.getExtension(fileName)
val renderer = PluginRegistry().getRenderer(extension)
renderer.render(RenderRequest(filePath, fileContent, branch, repository, enableWikiLink, enableRefsLink, enableAnchor, context))

View File

@@ -1,18 +1,32 @@
@(plugins: List[gitbucket.core.plugin.PluginInfo])(implicit context: gitbucket.core.controller.Context)
@(plugins: List[(gitbucket.core.plugin.PluginInfoBase, Boolean)], info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Plugins"){
@gitbucket.core.admin.html.menu("plugins") {
<h1>Installed plugins</h1>
@gitbucket.core.helper.html.information(info)
<form action="@context.path/admin/plugins/_reload" method="POST" class="pull-right">
<input type="submit" value="Reload plugins" class="btn btn-default">
</form>
<h1>Plugins</h1>
@if(plugins.size > 0) {
<ul>
@plugins.map { plugin =>
@plugins.map { case (plugin, enabled) =>
<li><a href="#@plugin.pluginId">@plugin.pluginId:@plugin.pluginVersion</a></li>
}
</ul>
@plugins.map { plugin =>
@plugins.map { case (plugin, enabled) =>
<div class="panel panel-default">
<div class="panel-heading strong" id="@plugin.pluginId">@plugin.pluginName</div>
<div class="panel-heading strong" id="@plugin.pluginId">
@if(enabled){
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_uninstall" method="POST" class="pull-right uninstall-form">
<input type="submit" value="Uninstall" class="btn btn-danger btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form>
} else {
<form action="@{context.path}/admin/plugins/@{plugin.pluginId}/@{plugin.pluginVersion}/_install" method="POST" class="pull-right install-form">
<input type="submit" value="Install" class="btn btn-success btn-sm" style="position: relative; top: -5px; left: 10px;" data-name="@plugin.pluginName">
</form>
}
@plugin.pluginName
</div>
<div class="panel-body">
<div class="row">
<label class="col-md-2">Id</label>
@@ -38,3 +52,16 @@
}
}
}
<script>
$(function(){
$('.uninstall-form').click(function(e){
var name = $(e.target).data('name');
return confirm('Uninstall ' + name + '. Are you sure?');
});
$('.install-form').click(function(e){
var name = $(e.target).data('name');
return confirm('Install ' + name + '. Are you sure?');
});
});
</script>

View File

@@ -1,5 +1,6 @@
@(info: Option[Any])(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.DatabaseConfig
@import gitbucket.core.view.helpers
@gitbucket.core.html.main("System settings"){
@gitbucket.core.admin.html.menu("system"){
@gitbucket.core.helper.html.information(info)
@@ -344,6 +345,36 @@
</div>
</div>
*@
<!--====================================================================-->
<!-- AdminLTE SkinName -->
<!--====================================================================-->
<hr>
<label class="strong">
AdminLTE skin name
</label>
<div class="form-group">
<label class="control-label col-md-3" for="skinName">Skin name</label>
<div class="col-md-9">
<select id="skinName" name="skinName">
@Seq(
"skin-black",
"skin-black-light",
"skin-blue",
"skin-blue-light",
"skin-green",
"skin-green-light",
"skin-purple",
"skin-purple-light",
"skin-red",
"skin-red-light",
"skin-yellow",
"skin-yellow-light",
).map{ skin =>
<option @if(skin == context.settings.skinName){selected}>@skin</option>
}
</select>
</div>
</div>
</div>
</div>
<div class="align-right" style="margin-top: 20px;">

View File

@@ -14,7 +14,7 @@
} else {
<li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li>
@userRepositories.zipWithIndex.map { case (repository, i) =>
<li class="menu-item-hover">
<li class="repo-link menu-item-hover">
@if(repository.owner == context.loginAccount.get.userName){
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span class="strong">@repository.name</span></a>
} else {
@@ -30,7 +30,7 @@
} else {
<li><form class="sidebar-form"><input type="text" id="filter-box" class="form-control input-sm" placeholder="Find repository"/></form></li>
@recentRepositories.zipWithIndex.map { case (repository, i) =>
<li class="menu-item-hover">
<li class="repo-link menu-item-hover">
<a href="@helpers.url(repository)">@gitbucket.core.helper.html.repositoryicon(repository, false) <span>@repository.owner/<span class="strong">@repository.name</span></span></a>
</li>
}

View File

@@ -1,8 +1,22 @@
@(title: String)(implicit context: gitbucket.core.controller.Context)
@(title: String, e: Option[Throwable]=None)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.html.main("Error"){
<div class="content-wrapper main-center">
<div class="content body">
<h1>@title</h1>
@if(context.loginAccount.map{_.isAdmin}.getOrElse(false)){
@e.map { ex =>
<h2>@ex.getMessage</h2>
<table class="table table-condensed table-striped table-hover">
<tbody>
@ex.getStackTrace.map{ st =>
<tr><td>@st</td></tr>
}
</tbody>
</table>
}
} else {
<div>Please contact your administrator.</div>
}
</div>
</div>
}
}

View File

@@ -65,8 +65,6 @@ $(function(){
url: '@context.path/upload/file/@repository.owner/@repository.name',
maxFilesize: 10,
clickable: @clickable,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) +

View File

@@ -3,12 +3,12 @@
<div class="input-group" style="margin-bottom: 0px;">
@html
<span class="input-group-btn">
<span id="@copyButtonId" class="btn btn-default" @if(style.nonEmpty){style="@style"}
<span id="@copyButtonId" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
</span>
</div>
} else {
<span id="@copyButtonId" class="btn btn-default" @if(style.nonEmpty){style="@style"}
<span id="@copyButtonId" class="btn btn-sm btn-default" @if(style.nonEmpty){style="@style"}
data-clipboard-text="@value" data-placement="bottom" title="copy to clipboard"><i class="octicon octicon-clippy"></i></span>
}
<script>
@@ -18,7 +18,7 @@
if (document.queryCommandSupported('copy')) {
var title = $('#@copyButtonId').attr('title');
$('#@copyButtonId').tooltip({
@* if default container is used then 2 lines tooltip text is displayd because tooptip width is narrow. *@
@* if default container is used then 2 lines tooltip text is displayed because tooptip width is narrow. *@
container: 'body'
});
$('#@copyButtonId').on('click', function() {

View File

@@ -2,10 +2,10 @@
@import gitbucket.core.view.helpers
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
<id>tag:@context.host,2013:gitbucket</id>
<title>Gitbucket's activities</title>
<title>GitBucket's activities</title>
<link type="application/atom+xml" rel="self" href="@context.baseUrl/activities.atom"/>
<author>
<name>Gitbucket</name>
<name>GitBucket</name>
<uri>@context.baseUrl</uri>
</author>
<updated>@helpers.datetimeRFC3339(if(activities.isEmpty) new java.util.Date else activities.map(_.activityDate).max)</updated>

View File

@@ -16,7 +16,7 @@
<div class="tabbable">
<ul class="nav nav-tabs fill-width" style="margin-bottom: 10px;">
<li class="active"><a href="#tab@uid" data-toggle="tab">Write</a></li>
<li><a href="#tab@(uid+1)" data-toggle="tab" id="preview@uid">Preview</a></li>
<li><a href="#tab@(uid + 1)" data-toggle="tab" id="preview@uid">Preview</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" style="margin-top: 4px;" id="tab@uid">
@@ -32,7 +32,7 @@
generateScript = !enableWikiLink
)(textarea)
</div>
<div class="tab-pane" id="tab@(uid+1)">
<div class="tab-pane" id="tab@(uid + 1)">
<div class="markdown-body" id="preview-area@uid">
</div>
</div>

View File

@@ -1,12 +1,22 @@
@(content: String, commentId: Int,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
<span id="error-edit-content-@commentId" class="error"></span>
@gitbucket.core.helper.html.attached(repository, "issues"){
<textarea id="edit-content-@commentId" class="form-control">@content</textarea>
}
<div>
<input type="button" id="cancel-comment-@commentId" class="btn btn-danger" value="Cancel"/>
<input type="button" id="update-comment-@commentId" class="btn btn-default pull-right" value="Update comment"/>
@gitbucket.core.helper.html.preview(
repository = repository,
content = content,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true,
completionContext = "issues",
style = "",
elastic = true,
tabIndex = 1
)
<div class="pull-right">
<input type="button" id="cancel-comment-@commentId" class="btn btn-default" value="Cancel"/>
<input type="button" id="update-comment-@commentId" class="btn btn-success" value="Update comment"/>
</div>
<script>
$(function(){
@@ -17,13 +27,14 @@ $(function(){
};
$('#update-comment-@commentId').click(function(){
var content = $(this).parent().parent().find('textarea[name=content]').val();
$('#update-comment-@commentId, #cancel-comment-@commentId').attr('disabled', 'disabled');
$.ajax({
url: '@context.path/@repository.owner/@repository.name/issue_comments/edit/@commentId',
type: 'POST',
data: {
issueId : 0, // TODO
content : $('#edit-content-@commentId').val()
content : content
}
}).done(
callback

View File

@@ -1,27 +1,37 @@
@(content: Option[String], issueId: Int,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
@gitbucket.core.helper.html.attached(repository, "issues"){
<textarea id="edit-content" class="form-control">@content.getOrElse("")</textarea>
}
<div>
<input type="button" id="cancel-issue" class="btn btn-danger" value="Cancel"/>
<input type="button" id="update-issue" class="btn btn-default pull-right" value="Update comment"/>
@gitbucket.core.helper.html.preview(
repository = repository,
content = content.getOrElse(""),
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true,
completionContext = "issues",
style = "",
elastic = true,
tabIndex = 1
)
<div class="pull-right">
<input type="button" id="cancel-issue" class="btn btn-default" value="Cancel"/>
<input type="button" id="update-issue" class="btn btn-success" value="Update comment"/>
</div>
<script>
$(function(){
var callback = function(data){
$('#update, #cancel').removeAttr('disabled');
$('#issueContent').empty().html(data.content);
prettyPrint();
};
$('#update-issue').click(function(){
var content = $(this).parent().parent().find('textarea[name=content]').val();
$('#update, #cancel').attr('disabled', 'disabled');
$.ajax({
url: '@context.path/@repository.owner/@repository.name/issues/edit/@issueId',
type: 'POST',
data: {
content : $('#edit-content').val()
}
data: { content : content }
}).done(
callback
).fail(function(req) {

View File

@@ -148,8 +148,8 @@
<input type="hidden" name="assignedUserName" value=""/>
}
@issue.map { issue =>
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebar =>
@sidebar(issue, repository, context)
@gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent =>
@sidebarComponent(issue, repository, context)
}
<hr/>
<div style="margin-bottom: 14px;">

View File

@@ -4,9 +4,9 @@
@defining(label.map(_.labelId).getOrElse("new")){ labelId =>
<div id="edit-label-area-@labelId">
<form class="form-inline">
<input type="text" id="labelName-@labelId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<input type="text" id="labelName-@labelId" style="width: 300px; float: left; margin-right: 4px;" class="form-control input-sm" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<div id="label-color-@labelId" class="input-group color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
<input type="text" class="form-control" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" style="width: 100px;">
<input type="text" class="form-control input-sm" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" style="width: 100px;">
<span class="input-group-addon"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div>
<script>
@@ -14,8 +14,8 @@
</script>
<span class="pull-right">
<span id="label-error-@labelId" class="error"></span>
<input type="button" id="cancel-@labelId" class="btn btn-default label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
<input type="button" id="cancel-@labelId" class="btn btn-sm btn-default label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-sm btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
</span>
</form>
</div>

View File

@@ -7,9 +7,9 @@
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row" id="label-@label.labelId">
<div class="col-md-8">
<div style="margin-top: 6px">
<div>
<a href="@helpers.url(repository)/issues?labels=@helpers.urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; border-radius: 4px;">
<i class="octicon octicon-tag" style="color: #@label.fontColor;"></i>
@label.labelName
</span>

View File

@@ -16,9 +16,7 @@
<table class="table table-bordered table-hover table-issues">
<thead>
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
<th>@labels.size labels</th>
</tr>
</thead>
<tbody>

View File

@@ -13,15 +13,13 @@
<table class="table table-bordered table-hover table-issues">
<thead>
<tr>
<th style="background-color: #eee;">
<span class="small">
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</span>
<th>
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
@milestones.count(_._1.closedDate.isEmpty) Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
@milestones.count(_._1.closedDate.isDefined) Closed
</a>
</th>
</tr>
</thead>

View File

@@ -4,19 +4,19 @@
@defining(priority.map(_.priorityId).getOrElse("new")){ priorityId =>
<div id="edit-priority-area-@priorityId">
<form class="form-inline">
<input type="text" id="priorityName-@priorityId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
<input type="text" id="priorityName-@priorityId" style="width: 200px; float: left; margin-right: 4px;" class="form-control input-sm" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
<div id="priority-color-@priorityId" class="input-group color bscp" data-color="#@priority.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
<input type="text" class="form-control" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
<input type="text" class="form-control input-sm" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
<span class="input-group-addon"><i style="background-color: #@priority.map(_.color).getOrElse("888888");"></i></span>
</div>
<script>
$('div#priority-color-@priorityId').colorpicker({format: "hex"});
</script>
<input type="text" id="description-@priorityId" style="width: 500px; float: left; margin-left: 4px;" class="form-control" value="@priority.flatMap(_.description).getOrElse("")" placeholder="Description..." />
<input type="text" id="description-@priorityId" style="width: 500px; float: left; margin-left: 4px;" class="form-control input-sm" value="@priority.flatMap(_.description).getOrElse("")" placeholder="Description..." />
<span class="pull-right">
<span id="priority-error-@priorityId" class="error"></span>
<input type="button" id="cancel-@priorityId" class="btn btn-default priority-edit-cancel" value="Cancel">
<input type="button" id="submit-@priorityId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(priorityId == "new") "Create priority" else "Save changes")"/>
<input type="button" id="cancel-@priorityId" class="btn btn-sm btn-default priority-edit-cancel" value="Cancel">
<input type="button" id="submit-@priorityId" class="btn btn-sm btn-success" style="margin-bottom: 0px;" value="@(if(priorityId == "new") "Create priority" else "Save changes")"/>
</span>
</form>
</div>

View File

@@ -16,9 +16,7 @@
<table id="priorities-table" class="table table-bordered table-hover table-issues">
<thead>
<tr id="priority-row-header">
<th style="background-color: #eee;">
<span class="small">@priorities.size priorities</span>
</th>
<th>@priorities.size priorities</th>
</tr>
</thead>
<tbody>

View File

@@ -10,9 +10,9 @@
@if(hasWritePermission) {
<div class="pull-left priority-sort-handle" style="margin-top: 3px"><i class="octicon octicon-grabber"></i></div>
}
<div style="margin-top: 6px">
<div>
<a href="@helpers.url(repository)/issues?priority=@helpers.urlEncode(priority.priorityName)&state=open" id="priority-row-content-@priority.priorityId">
<span style="background-color: #@priority.color; color: #@priority.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<span style="background-color: #@priority.color; color: #@priority.fontColor; padding: 8px; border-radius: 4px;">
<i class="octicon octicon-flame" style="color: #@priority.fontColor;"></i>&nbsp;@priority.priorityName
</span>
</a>

View File

@@ -16,7 +16,7 @@
<link href="@helpers.assets("/vendors/google-code-prettify/prettify.css")" type="text/css" rel="stylesheet"/>
<link href="@helpers.assets("/vendors/facebox/facebox.css")" rel="stylesheet"/>
<link href="@helpers.assets("/vendors/AdminLTE-2.3.11/css/AdminLTE.min.css")" rel="stylesheet">
<link href="@helpers.assets("/vendors/AdminLTE-2.3.11/css/skins/skin-blue.min.css")" rel="stylesheet">
<link href="@helpers.assets(s"/vendors/AdminLTE-2.3.11/css/skins/${context.settings.skinName}.min.css")" rel="stylesheet">
<link href="@helpers.assets("/vendors/font-awesome-4.6.3/css/font-awesome.min.css")" rel="stylesheet">
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.min.css")" rel="stylesheet">
<link href="@helpers.assets("/vendors/jquery-ui/jquery-ui.structure.min.css")" rel="stylesheet">
@@ -42,11 +42,11 @@
}
<script src="@helpers.assets("/vendors/AdminLTE-2.3.11/js/app.js")" type="text/javascript"></script>
</head>
<body class="skin-blue page-load @if(body.toString.contains("menu-item-hover")){sidebar-mini} @if(context.sidebarCollapse){sidebar-collapse}">
<body class="@context.settings.skinName page-load @if(body.toString.contains("menu-item-hover")){sidebar-mini} @if(context.sidebarCollapse){sidebar-collapse}">
<div class="wrapper">
<header class="main-header">
<a href="@context.path/" class="logo">
<img src="@helpers.assets("/common/images/gitbucket.png")" style="width: 24px; height: 24px; display: inline;"/>
<img src="@helpers.assets("/common/images/gitbucket.svg")" style="width: 24px; height: 24px; display: inline;"/>
GitBucket
<span class="header-version">@gitbucket.core.GitBucketCoreModule.getVersions.last.getVersion</span>
</a>

View File

@@ -57,7 +57,7 @@
@menuitem("/network/members", "fork", "Forks", "repo-forked", repository.forkedCount)
}
@if(context.loginAccount.isDefined && (context.loginAccount.get.isAdmin || repository.managers.contains(context.loginAccount.get.userName))){
@menuitem("/settings", "settings", "Settings", "tools")
@menuitem("/settings", "settings", "Settings", "gear")
}
@gitbucket.core.plugin.PluginRegistry().getRepositoryMenus.map { menu =>
@menu(repository, context).map { link =>
@@ -74,6 +74,11 @@
@gitbucket.core.helper.html.information(info)
@gitbucket.core.helper.html.error(error)
<div class="head">
<div class="pull-right">
@gitbucket.core.plugin.PluginRegistry().getRepositoryHeaders.map { repositoryHeaderComponent =>
@repositoryHeaderComponent(repository, context)
}
</div>
@gitbucket.core.helper.html.repositoryicon(repository, true)
<a href="@helpers.url(repository.owner)">@repository.owner</a> / <a href="@helpers.url(repository)" class="strong">@repository.name</a>
@@ -83,9 +88,6 @@
forked from <a href="@context.path/@x.parentUserName/@x.parentRepositoryName">@x.parentUserName/@x.parentRepositoryName</a>
</div>
}
@x.description.map { description =>
<div class="normal muted" style="margin-left: 36px; font-size: 80%;">@Html(helpers.detectAndRenderLinks(description, repository))</div>
}
}
</div>
</div>

View File

@@ -20,7 +20,7 @@
@status.statusesAndRequired.map { case (status, required) =>
<div class="build-status-item">
<div class="pull-right">
@if(required){ <span class="label">Required</span> }
@if(required){ <span class="label label-success">Required</span> }
@status.targetUrl.map { url => <a href="@url">Details</a> }
</div>
<span class="build-status-icon text-@{status.state.name}">@helpers.commitStateIcon(status.state)</span>

View File

@@ -23,7 +23,7 @@
<div>
<div class="show-title pull-right">
@if(isManageable || context.loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn" href="#" id="edit">Edit</a>
<a class="btn btn-default" href="#" id="edit">Edit</a>
}
@if(context.loginAccount.isDefined){
<a class="btn btn-success" href="@helpers.url(repository)/compare">New pull request</a>

View File

@@ -11,6 +11,7 @@
@gitbucket.core.html.menu("files", repository){
<div class="head">
<div class="pull-right hide-if-blame"><div class="btn-group">
<a href="@helpers.url(repository)/blob/@latestCommit.id/@pathList.mkString("/")" data-hotkey="y" style="display: none;">Transfer to URL with SHA</a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t">Find file</a>
</div></div>
<div class="line-age-legend">
@@ -29,6 +30,7 @@
</ol>
<span>Older</span>
</div>
<div id="branchCtrlWrapper" style="display:inline;">
@gitbucket.core.helper.html.branchcontrol(
branch,
repository,
@@ -38,6 +40,7 @@
<li><a href="@helpers.url(repository)/blob/@helpers.encodeRefName(x)/@pathList.mkString("/")">@gitbucket.core.helper.html.checkicon(x == branch) @x</a></li>
}
}
</div>
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch)">@repository.name</a> /
@pathList.zipWithIndex.map { case (section, i) =>
@if(i == pathList.length - 1){
@@ -71,7 +74,7 @@
</div>
</div>
@if(content.viewType == "text"){
@defining(helpers.isRenderable(pathList.reverse.head)){ isRenderable =>
@defining(helpers.isRenderable(pathList.last)){ isRenderable =>
@if(!isBlame && isRenderable) {
<div class="box-content-bottom markdown-body" style="padding-left: 20px; padding-right: 20px;">
@helpers.renderMarkup(pathList, content.content.get, branch, repository, false, false, true)
@@ -130,14 +133,18 @@ $(window).load(function(){
}
var line = pos[i].id.replace(/^L/,'');
var hash = location.hash;
var commitUrl = '@helpers.url(repository)/blob/@latestCommit.id/@pathList.mkString("/")';
if(e.shiftKey == true && hash.match(/#L\d+(-L\d+)?/)){
var lines = hash.split('-');
location.hash = lines[0] + '-L' + line;
window.history.pushState('', '', commitUrl + lines[0] + '-L' + line);
} else {
var p = $("#L"+line).attr('id',"");
location.hash = '#L' + line;
window.history.pushState('', '', commitUrl + '#L' + line);
p.attr('id','L'+line);
}
$("#branchCtrlWrapper .btn .muted").text("tree:");
$("#branchCtrlWrapper .btn .strong").text("@latestCommit.id.substring(0, 10)");
updateHighlighting();
}).appendTo(pre);
}
}

View File

@@ -7,7 +7,7 @@
<table class="table table-hover branches">
<thead>
<tr>
<th style="background: #f5f5f5;color: #666;" colspan="3">All branches</th>
<th colspan="3">All branches</th>
</tr>
</thead>
<tbody>
@@ -15,7 +15,7 @@
<tr>
<td class="branch-details">
@if(isProtected){ <span class="octicon octicon-shield" title="This branch is protected"></span> }
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch.name)" class="branch-name">@branch.name</a>
<a href="@helpers.url(repository)/tree/@helpers.encodeRefName(branch.name)">@branch.name</a>
<span class="branch-meta">
<span>Updated @gitbucket.core.helper.html.datetimeago(branch.commitTime, false)
by <span>@helpers.user(branch.committerName, branch.committerEmailAddress, "muted-link")</span>
@@ -36,7 +36,7 @@
}
}
</td>
<td>
<td class="text-right">
<div class="branch-action">
@if(repository.repository.defaultBranch != branch.name){
@branch.mergeInfo.map{ info =>

View File

@@ -15,7 +15,7 @@
<tr>
<th class="box-header">
<div class="pull-right align-right">
<a href="@helpers.url(repository)/tree/@commit.id" class="btn btn-small">Browse code</a>
<a href="@helpers.url(repository)/tree/@commit.id" class="btn btn-default">Browse code</a>
</div>
<div class="commit-log">@helpers.link(commit.summary, repository)</div>
@if(commit.description.isDefined){

View File

@@ -4,7 +4,9 @@
commits: Seq[Seq[gitbucket.core.util.JGitUtil.CommitInfo]],
page: Int,
hasNext: Boolean,
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
hasWritePermission: Boolean,
getStatuses: String => List[gitbucket.core.model.CommitStatus],
getSummary: List[gitbucket.core.model.CommitStatus] => (gitbucket.core.model.CommitState, String))(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.view.helpers
@gitbucket.core.html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@gitbucket.core.html.menu("files", repository){
@@ -63,6 +65,30 @@
}
@helpers.user(commit.committerName, commit.committerEmailAddress, "username")
<span class="muted">committed @gitbucket.core.helper.html.datetimeago(commit.commitTime)</span>
@defining({
val statuses = getStatuses(commit.id)
val (summary, summaryText) = getSummary(statuses)
(statuses, summary, summaryText)
}){ case (statuses, summaryState, summaryText) =>
@if(statuses.nonEmpty){
@helpers.commitStateIcon(summaryState)
<strong class="text-@{summaryState.name}">@helpers.commitStateText(summaryState, commit.id)</strong>
<span class="text-@{summaryState.name}">- @summaryText checks</span>
<a href="#" class="toggle-check">Show all checks</a>
<div style="display: none;">
@statuses.map{ status =>
<div class="build-status-item">
<span class="build-status-icon text-@{status.state.name}">@helpers.commitStateIcon(status.state)</span>
<strong>@status.context</strong>
@status.description.map { desc => <span class="muted">- @desc</span> }
<span>
@status.targetUrl.map { url => - <a href="@url">Details</a> }
</span>
</div>
}
</div>
}
}
</div>
</div>
</div>
@@ -86,5 +112,19 @@
}
</ul>
</nav>
<script>
$(function () {
$('.toggle-check').click(function(){
var div = $(this).next('div');
if(div.is(':visible')){
$(this).text('Show all checks');
} else {
$(this).text('Hide all checks');
}
div.toggle();
return false;
});
})
</script>
}
}

View File

@@ -1,12 +1,22 @@
@(content: String, commentId: Int,
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
<span class="error-edit-content-@commentId error"></span>
@gitbucket.core.helper.html.attached(repository, "issues"){
<textarea style="height: 100px;" id="edit-content-@commentId" class="form-control">@content</textarea>
}
<div>
<input type="button" class="cancel-comment-@commentId btn btn-small btn-danger" value="Cancel"/>
<input type="button" class="update-comment-@commentId btn btn-small btn-default pull-right" value="Update comment"/>
@gitbucket.core.helper.html.preview(
repository = repository,
content = content,
enableWikiLink = false,
enableRefsLink = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = true,
completionContext = "issues",
style = "",
elastic = true,
tabIndex = 1
)
<div class="pull-right">
<input type="button" class="cancel-comment-@commentId btn btn-small btn-default" value="Cancel"/>
<input type="button" class="update-comment-@commentId btn btn-small btn-success" value="Update comment"/>
</div>
<script>
$(function(){
@@ -19,13 +29,14 @@ $(function(){
}
$(document).on('click', '.update-comment-@commentId', function(){
var content = $(this).parent().parent().find('textarea[name=content]').val();
$box = $(this).closest('.commit-comment-box');
$('.update-comment-@commentId, .cancel-comment-@commentId', $box).attr('disabled', 'disabled');
$.ajax({
url: '@context.path/@repository.owner/@repository.name/commit_comments/edit/@commentId',
type: 'POST',
data: {
content : $('#edit-content-@commentId', $box).val()
content : content
}
}).done(
curriedCallback($box)

View File

@@ -22,9 +22,17 @@
s"${(repository.name :: pathList).mkString("/")} at ${helpers.encodeRefName(branch)} - ${repository.owner}/${repository.name}"
}, Some(repository)) {
@gitbucket.core.html.menu("files", repository, Some(branch), info, error){
@if(pathList.isEmpty) {
@repository.repository.description.map { description =>
<p class="pc" style="margin-bottom: 10px;">
<span class="normal muted">@Html(helpers.detectAndRenderLinks(description, repository))</span>
</p>
}
}
<div class="head" style="height: 24px;">
<div class="pull-right">
<div class="btn-group">
<a href="@helpers.url(repository)/tree/@latestCommit.id/@pathList.mkString("/")" data-hotkey="y" style="display: none;">Transfer to URL with SHA</a>
<a href="@helpers.url(repository)/find/@helpers.encodeRefName(branch)" class="btn btn-sm btn-default" data-hotkey="t"><i class="octicon octicon-search"></i></a>
<a href="@helpers.url(repository)/commits/@helpers.encodeRefName(branch)/@pathList.mkString("/")" class="btn btn-sm btn-default"><i class="octicon octicon-history"></i> @if(commitCount > 10000){10000+} else {@commitCount} @helpers.plural(commitCount, "commit")</a>
</div>
@@ -102,8 +110,8 @@
<tr>
<th colspan="4" class="latest-commit">
<div>
<div class="pull-right align-right monospace" style="line-height: 18px;">
<a href="@helpers.url(repository)/commit/@latestCommit.id" class="commit-id"><span class="muted">latest commit</span> @latestCommit.id.substring(0, 10)</a>
<div class="pull-right align-right" style="line-height: 18px;">
<a href="@helpers.url(repository)/commit/@latestCommit.id" class="commit-id"><span class="muted">latest commit</span> <span class="monospace">@latestCommit.id.substring(0, 10)</span></a>
</div>
<div class="author-info">
<div class="author">
@@ -145,21 +153,35 @@
</td>
<td class="ellipsis-cell" style="width: 20%; min-width: 160px;">
@if(file.isDirectory){
@if(file.linkUrl.isDefined){
<a href="@file.linkUrl">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
} else {
<a href="@helpers.url(repository)/tree@{(branch :: pathList).map(helpers.encodeRefName).mkString("/", "/", "/")}@{helpers.encodeRefName(file.name)}">
<span class="simplified-path">@file.name.split("/").toList.init match {
case Nil => {}
case list => {@list.mkString("", "/", "/")}
}</span>@file.name.split("/").toList.last
</a>
}
@{file.linkUrl match {
case Some(linkUrl) if linkUrl.startsWith("http://") || linkUrl.startsWith("https://") => {
<a href={linkUrl}>
<span class="simplified-path">{file.name.split("/").toList.init match {
case Nil => ""
case list => list.mkString("", "/", "/")
}}</span>
{file.name.split("/").toList.last}
</a>
}
case Some(_) => {
<span>
<span class="simplified-path">{file.name.split("/").toList.init match {
case Nil => ""
case list => list.mkString("", "/", "/")
}}</span>
{file.name.split("/").toList.last}
</span>
}
case None => {
<a href={helpers.url(repository) + "/tree" + (branch :: pathList).map(helpers.encodeRefName).mkString("/", "/", "/") + helpers.encodeRefName(file.name)}>
<span class="simplified-path">{file.name.split("/").toList.init match {
case Nil => ""
case list => list.mkString("", "/", "/")
}}</span>
{file.name.split("/").toList.last}
</a>
}
}}
} else {
<a href="@helpers.url(repository)/blob@{(branch :: pathList).map(helpers.encodeRefName).mkString("/", "/", "/")}@{helpers.encodeRefName(file.name)}">@file.name</a>
}
@@ -175,7 +197,7 @@
</table>
@readme.map { case(filePath, content) =>
<div id="readme" class="panel panel-default">
<div class="panel-heading strong">@filePath.reverse.head</div>
<div class="panel-heading strong">@filePath.last</div>
<div class="panel-body markdown-body" style="padding-left: 20px; padding-right: 20px;">@helpers.renderMarkup(filePath, content, branch, repository, false, false, true)</div>
</div>
}

View File

@@ -12,7 +12,7 @@
</tr>
</thead>
<tbody>
@repository.tags.reverse.map { tag =>
@repository.tags.reverseMap { tag =>
<tr>
<td><a href="@helpers.url(repository)/tree/@helpers.encodeRefName(tag.name)">@tag.name</a></td>
<td>@gitbucket.core.helper.html.datetimeago(tag.time, false)</td>

View File

@@ -16,7 +16,8 @@
<span id="error-password" class="error"></span>
<input type="password" name="password" id="password" class="form-control" value="@password"/>
</div>
<input type="submit" class="btn btn-success" value="Sign in"/>
<input type="hidden" name="hash"/>
<input type="submit" class="btn btn-success" value="Sign in" onClick="this.form.hash.value = window.location.hash;"/>
@if(systemSettings.allowAccountRegistration){
or <a href="@context.path/register">Create new account</a>
}

View File

@@ -50,8 +50,6 @@ $(function(){
url: '@context.path/upload/wiki/@repository.owner/@repository.name',
maxFilesize: 10,
clickable: false,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
@@ -62,8 +60,6 @@ $(function(){
$('.clickable').dropzone({
url: '@context.path/upload/wiki/@repository.owner/@repository.name',
maxFilesize: 10,
acceptedFiles: @Html(FileUtil.mimeTypeWhiteList.mkString("'", ",", "'")),
dictInvalidFileType: 'Unfortunately, we don\'t support that file type. Try again with a PNG, GIF, JPG, DOCX, PPTX, XLSX, TXT, or PDF.',
previewTemplate: "<div class=\"dz-preview\">\n <div class=\"dz-progress\"><span class=\"dz-upload\" data-dz-uploadprogress>Uploading your files...</span></div>\n <div class=\"dz-error-message\"><span data-dz-errormessage></span></div>\n</div>",
success: function(file, id) {
var attachFile = (file.type.match(/image\/.*/) ? '\n![' + file.name.split('.')[0] : '\n[' + file.name) + '](' + file.name + ')';
@@ -78,7 +74,7 @@ $(function(){
}
$('#delete').click(function(){
return confirm('Are you sure you want to delete this page?');
return confirm('Are you sure you want to delete this page?');
});
});
</script>

View File

@@ -1,6 +1,8 @@
/****************************************************************************/
/* Common */
/****************************************************************************/
.wrapper { position: static; }
body {
color: #333;
}
@@ -390,8 +392,8 @@ a.btn-danger:hover .octicon {
/* Head Menu */
/****************************************************************************/
div.headbar {
padding-top: 19px;
margin-bottom: 20px;
padding-top: 4px;
margin-bottom: 8px;
}
/****************************************************************************/
@@ -436,8 +438,9 @@ div.repository-content {
margin-left: 40px;
}
table.branches>thead>tr>th, table.branches>tbody>tr>td{
table.branches>tbody>tr>td{
padding: 12px;
line-height: 30px;
}
.branches .muted-link{
@@ -448,15 +451,6 @@ table.branches>thead>tr>th, table.branches>tbody>tr>td{
color: #4183c4;
}
.branches .branch-name{
color: #4183c4;
display: inline-block;
padding: 2px 6px;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
background-color: rgba(209,227,237,0.5);
border-radius: 3px;
}
.branches .branch-meta{
color: #aaa;
line-height: 20px;
@@ -562,17 +556,6 @@ pre.commit-description {
font-family: monospace;
}
#repository-url-copy {
/*height: 18px;*/
padding-top: 2px;
}
#repository-url-copy > i {
margin-left: -4px;
margin-right: -4px;
}
ul#commit-file-list {
list-style-type: none;
padding-left: 0px;

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