Compare commits

...

335 Commits
3.8 ... 3.12

Author SHA1 Message Date
Naoki Takezoe
977f856854 Update README.md 2016-02-27 04:52:36 +09:00
Naoki Takezoe
da2a7bf77d Fix comment editing in pull request diff view 2016-02-27 04:48:46 +09:00
Naoki Takezoe
3da3a048f0 Fix width of previewable editing forms 2016-02-27 02:56:22 +09:00
Naoki Takezoe
7b5b453e56 (refs #1123)Fix page list style 2016-02-26 21:48:49 +09:00
Naoki Takezoe
c18f95edf8 Merge pull request #1120 from lidice/adjust-image-width
Adjust img width in content box to max-width:100%
2016-02-25 13:23:24 +09:00
Naoki Takezoe
71cf043f56 Merge pull request #1119 from lidice/fix-unmatched-tag
(fixes #1118)Remove duplicated <script>
2016-02-25 13:22:17 +09:00
lidice
a31e4b5897 Adjust img width in content box to max-width:100% 2016-02-23 19:52:44 +09:00
lidice
1679da4abe (fixes #1118)Remove duplicated <script> 2016-02-22 11:55:31 +09:00
Naoki Takezoe
505bc71f9a Fix width of wiki page editing form 2016-02-22 08:57:23 +09:00
Naoki Takezoe
4bc057c653 Fix broken presentation 2016-02-22 02:17:41 +09:00
Naoki Takezoe
8eee13d7aa Merge some controllers because a large amount mapping causes performance issue 2016-02-22 01:33:38 +09:00
Naoki Takezoe
8981e339b4 Disable new pull request button if user does not signed-in 2016-02-21 16:00:14 +09:00
Naoki Takezoe
e1dd5dd057 Merged branch master into master 2016-02-21 15:58:46 +09:00
Naoki Takezoe
cb64f8eab8 Replace new issue button with new pull request button 2016-02-21 15:56:12 +09:00
Naoki Takezoe
c47d50d0df Fix pull request guide 2016-02-21 15:36:41 +09:00
Naoki Takezoe
1f46da2273 Merge pull request #1116 from McFoggy/templates
addition of issues & PR templates
2016-02-21 12:28:40 +09:00
Matthieu Brouillard
06fc26cd06 addition of issues & PR templates 2016-02-20 21:45:18 +01:00
Naoki Takezoe
3a4f9b9027 Merge pull request #1112 from oohira/fix/margin-after-octicon
Add margin after octicon
2016-02-21 03:51:11 +09:00
Naoki Takezoe
f98c849c7c Merge pull request #1115 from lidice/fix-label-color-format
(fixes #1114)Add Colorpicker options that to force hex format
2016-02-21 03:49:42 +09:00
Naoki Takezoe
aa0bd5b34a Update version to 3.12 2016-02-20 23:02:00 +09:00
Naoki Takezoe
b52e904ed1 Update CONTRIBUTING.md 2016-02-20 20:52:56 +09:00
lidice
70e0dcf99d (fixes #1114)Add Colorpicker options that to force hex format 2016-02-19 20:20:26 +09:00
Naoki Takezoe
56bb20dfd2 (refs #1113)Improve printing styles 2016-02-19 15:48:14 +09:00
oohira
7d7d2f488d Add margin after octicon 2016-02-17 23:42:51 +09:00
Naoki Takezoe
72affd67b9 Merge pull request #1108 from gitbucket/new-ui
New GitHub UI and Mobile support
2016-02-17 03:04:55 +09:00
Naoki Takezoe
0cf1f43deb Adjust issue / comment form 2016-02-16 17:34:21 +09:00
Naoki Takezoe
8494c682a7 Fix search box style for mobile 2016-02-16 15:02:34 +09:00
Naoki Takezoe
1af5611159 Mobile view improvement 2016-02-16 02:51:09 +09:00
Naoki Takezoe
4d39f63ef7 Tweak header buttons 2016-02-16 02:36:45 +09:00
Naoki Takezoe
120d1c2fff Implement repository url selector 2016-02-16 01:41:48 +09:00
Naoki Takezoe
62e9c0358a Adjust new file, new pull request button and others 2016-02-15 23:26:56 +09:00
Naoki Takezoe
5a90848c75 Remove unused code 2016-02-15 09:20:36 +09:00
Naoki Takezoe
760d443f74 Tweak top margin of contents 2016-02-15 09:11:57 +09:00
Naoki Takezoe
5ee0e75dfe Implementing new header parts 2016-02-15 02:21:00 +09:00
Naoki Takezoe
3b4d2d6f91 Fix header style 2016-02-15 01:02:35 +09:00
Naoki Takezoe
dfaabeb41d Move sidemenu to header 2016-02-14 23:41:07 +09:00
Naoki Takezoe
0fae2dac35 Merge pull request #1097 from ritschwumm/patch-3
Java 8 is a new requirement
2016-02-13 10:23:47 +09:00
Naoki Takezoe
4db4fe28b4 Merge pull request #1102 from ritschwumm/wip/name
rename file to the name of the type within
2016-02-13 10:19:08 +09:00
Naoki Takezoe
5b87efa032 (refs #1084)Remove RepositoryUrls 2016-02-13 10:16:11 +09:00
Herr Ritschwumm
3ad609bad7 rename file to the name of the type within 2016-02-13 01:24:04 +01:00
Naoki Takezoe
8145cba111 Merge branch 'wip/baseurl' of https://github.com/ritschwumm/gitbucket into ritschwumm-wip/baseurl 2016-02-12 23:15:59 +09:00
Herr Ritschwumm
24b9a9a12c remove RepoBase by moving RepositoryUrls construction into Context 2016-02-11 23:24:09 +01:00
Herr Ritschwumm
ee7220ebd2 move SshAddress into the SystemSettingsService object 2016-02-11 22:55:22 +01:00
Naoki Takezoe
8fb72fd55e (refs #982)Provide fixed url for pull request tabs 2016-02-09 18:14:03 +09:00
Naoki Takezoe
a1eded2d9a Merge pull request #1091 from oohira/fix/review-comment-box-border
Fix bug that border of review comment box is not shown
2016-02-09 13:58:18 +09:00
Naoki Takezoe
7f184e1126 Merge pull request #1100 from oohira/fix/label-duplicate-error
Fix bug that label duplicate check is wrong
2016-02-09 13:43:47 +09:00
oohira
09aafbcce1 Fix wrong query to find the specified label 2016-02-08 23:40:17 +09:00
Naoki Takezoe
7f5024a746 Change javac option to require Java8 2016-02-08 01:11:54 +09:00
Naoki Takezoe
8fec0870a8 Merge pull request #1098 from nus/fix-hidden-pull-requests
Fix some hidden pull requests
2016-02-08 01:03:41 +09:00
Naoki Takezoe
a8d2afaff7 Merge pull request #1099 from gitbucket/scalatest
Move to ScalaTest from Specs2
2016-02-07 00:02:46 +09:00
Naoki Takezoe
8fd92f1c2f Fix compilation error in testcase 2016-02-06 23:39:57 +09:00
Naoki Takezoe
419ea16ead Remove Specs2 dependency 2016-02-06 22:27:23 +09:00
Naoki Takezoe
e72d808a3c Migrating testcase to ScalaTest 2016-02-06 22:10:20 +09:00
Naoki Takezoe
44e8c0a9be Migrating testcase to ScalaTest 2016-02-05 23:00:35 +09:00
Naoki Takezoe
e2c39d7815 Merged branch master into scalatest 2016-02-04 01:51:29 +09:00
Yota Ichino
687cd54f9a Fix some hidden pull requests
IssuesService.IssueLimit value is equalized with
PullRequestService.PullRequestLimit value by this commit.

If without this, some pull requests are hidden.
For example, inspite of 30 pull requests are exists,
pull request page shows 25.
2016-02-03 16:43:49 +00:00
Naoki Takezoe
911754e1dc Migrating testcase to ScalaTest 2016-02-04 00:40:58 +09:00
Naoki Takezoe
0067cbce6f Fix failed test 2016-02-04 00:37:50 +09:00
Naoki Takezoe
f40f8427aa Replace === with == 2016-02-04 00:27:00 +09:00
oohira
98ceff2391 Fix bug that border of review comment box is not shown 2016-02-04 00:04:07 +09:00
Naoki Takezoe
642a51a208 Merge pull request #1096 from nus/fix-invisible-closed-label
Fix invisible closed label
2016-02-03 23:41:37 +09:00
Naoki Takezoe
9ec7c321d8 Merge pull request #1095 from oohira/link-avatar-image-to-profile-page
Link avatar image to the user's profile page
2016-02-03 23:33:57 +09:00
Naoki Takezoe
a3c419b6f5 Merge pull request #1086 from oohira/fix/invalid-repos-owner-url
Fix repository owner link URL
2016-02-03 23:08:55 +09:00
Naoki Takezoe
15c28cffa4 Merge branch 'ritschwumm-patch-1' 2016-02-03 22:37:40 +09:00
Naoki Takezoe
f4d0f16481 (refs #1083)Bump sbt launcher 2016-02-03 22:37:09 +09:00
Naoki Takezoe
45535e4fdf Merge branch 'patch-1' of https://github.com/ritschwumm/gitbucket into ritschwumm-patch-1 2016-02-03 22:34:30 +09:00
ritschwumm
64635c5dc6 Java 8 is a new requirement 2016-02-03 01:19:29 +01:00
Yota Ichino
2fd95c7f1a Fix invisible closed label
Closed label has label-important attribute which
was not defined in gitbucket.css, so that define
the attribute.
2016-02-02 15:35:45 +00:00
oohira
eb6da85183 Link avatar image to the user's profile page 2016-02-02 23:24:05 +09:00
Naoki Takezoe
bcc05f021c Migrating testcases to ScalaTest from Specs2 2016-02-02 00:36:08 +09:00
Naoki Takezoe
d58ed55c3a Merged branch ritschwumm-wip/escape into master 2016-01-31 12:26:37 +09:00
Naoki Takezoe
057f029c80 (refs #1085)Remove var by replacing for expression with foldLeft 2016-01-31 12:26:09 +09:00
Naoki Takezoe
c9a12ff913 Fix version extraction 2016-01-31 10:33:07 +09:00
oohira
66bf00b5d3 Fix repository owner link URL 2016-01-31 08:32:59 +09:00
Herr Ritschwumm
7ba3ca6f15 properly escape html characters in a description 2016-01-30 16:25:50 +01:00
Herr Ritschwumm
a1bacccc09 add tests, failing right now 2016-01-30 16:25:44 +01:00
ritschwumm
333eeb4bad update sbt to newest version 2016-01-30 11:24:23 +01:00
Herr Ritschwumm
3f2935612d feature: add settings for the ssh host 2016-01-30 11:15:07 +01:00
Herr Ritschwumm
4c87bdd959 refactor: make the settings alone responsible for ssh server location 2016-01-30 11:14:13 +01:00
Herr Ritschwumm
3543073150 cleanup: wiki urls could have been simpler 2016-01-30 11:12:11 +01:00
Herr Ritschwumm
e50fe604c2 cleanup: ... which in turns lets us avoid passing around the base url where we shouldn't need to know about it at all 2016-01-30 11:12:11 +01:00
Herr Ritschwumm
63369258bd preparation: move url generation from RepositoryInfo towards the gui 2016-01-30 11:12:07 +01:00
Herr Ritschwumm
e7c3376303 cleanup: baseUrl is not used here at all 2016-01-30 07:31:40 +01:00
Herr Ritschwumm
86163f66ce cleanup: jgit repo info does not have to know about urls in the gui 2016-01-30 07:31:26 +01:00
Herr Ritschwumm
518f0bfc28 cleanup: derive baseUrl from http request outside the SettingsService 2016-01-30 07:29:57 +01:00
Herr Ritschwumm
0a759f6127 cleanup: don't repeat yourself, calculate effective ssh port in one place only 2016-01-30 07:29:21 +01:00
Naoki Takezoe
afa79d01b1 3.11 Release 2016-01-30 09:51:30 +09:00
Naoki Takezoe
860fc8ef4c (refs #1080)Fix commit count presentation when over 10000 2016-01-30 02:01:04 +09:00
Naoki Takezoe
5a2224623b Merge pull request #1077 from ritschwumm/patch-1
adapt how_to_run.md to changes in build.sbt
2016-01-29 01:53:10 +09:00
ritschwumm
dda3458e80 adapt how_to_run.md to changes in build.sbt 2016-01-28 12:10:32 +01:00
Naoki Takezoe
df06f509f5 Merge pull request #1064 from lidice/add-labels-api
Extend API to allow CRUD labels
2016-01-27 01:18:16 +09:00
Naoki Takezoe
0b4d0be1b4 Merge pull request #1074 from ritschwumm/fix-concurrent-access
these fields are accessed from multiple threads
2016-01-27 01:17:02 +09:00
Naoki Takezoe
0bef8d29d5 Merge pull request #1076 from oohira/issue/1055-make-link-anchor-clickable
refs #1055 Make a header link anchor icon clickable
2016-01-27 01:03:10 +09:00
oohira
6265faa14f refs #1055 Make a header link anchor icon clickable 2016-01-26 23:53:07 +09:00
Naoki Takezoe
7593c97ad3 Fix document 2016-01-26 23:06:21 +09:00
Naoki Takezoe
7c8de834bc Merge executable.sbt with build.sbt 2016-01-26 23:01:12 +09:00
Naoki Takezoe
637e9f906b Replace build.scala with build.sbt 2016-01-26 22:08:33 +09:00
Naoki Takezoe
97035be2e5 Remove resources which are required to create executable war 2016-01-26 21:41:09 +09:00
Naoki Takezoe
80fedbd335 Merged branch master into master 2016-01-26 19:33:36 +09:00
Naoki Takezoe
3f6ebc1164 (refs #1065)Fixup 2016-01-26 19:32:46 +09:00
Naoki Takezoe
27b99319d3 Merge branch 'issue/963' of https://github.com/ritschwumm/gitbucket into ritschwumm-issue/963 2016-01-26 17:13:54 +09:00
Naoki Takezoe
a46d1ecf69 Merge pull request #1069 from oohira/limit-image-max-width
Limit maximum width of image
2016-01-26 16:54:17 +09:00
Naoki Takezoe
fb531526bd Merge pull request #1075 from ritschwumm/fix-filename
fix typo in a file name
2016-01-26 16:53:11 +09:00
Herr Ritschwumm
21d6143e40 fix typo in a file name 2016-01-26 01:54:01 +01:00
Herr Ritschwumm
129b424a3a these fields are accessed from multiple threads 2016-01-26 01:32:39 +01:00
Herr Ritschwumm
689334a599 use a constant for the jetty version 2016-01-25 21:54:42 +01:00
Naoki Takezoe
b3ee2222f3 (refs #1058)Bump markedj to 1.0.6 to solve table rendering bug 2016-01-26 01:21:58 +09:00
oohira
da5e4fe5b1 Limit maximum width of image 2016-01-24 23:40:25 +09:00
Naoki Takezoe
f3b7318453 Fix margin of icon and caret of dowpdown menu in the global header 2016-01-24 22:44:39 +09:00
Naoki Takezoe
9874eb7243 Merge pull request #1068 from x-way/api_json_contenttype
Set Content-Type to json for /api/v3/* (fix #1056)
2016-01-24 18:15:09 +09:00
Naoki Takezoe
cc6f4d70da Merge pull request #1066 from oohira/fix-typo
Fix typo in docs
2016-01-24 15:35:00 +09:00
Andreas Jaggi
9b06bfaaf5 Set Content-Type to json for /api/v3/* (fix #1056) 2016-01-23 18:40:49 +01:00
Naoki Takezoe
cdf0d06bcc (refs #1051)Don't render README folder 2016-01-23 21:35:36 +09:00
oohira
501f542982 Fix typo 2016-01-22 23:31:29 +09:00
Naoki Takezoe
1a3504e885 Merge pull request #1050 from x-way/limit_recent_repositories
Limit recent updated repositories list. #1011
2016-01-22 20:32:25 +09:00
Naoki Takezoe
fa617badc1 Merge pull request #1063 from lidice/fix/#1062
(fixes #1062) Remove a wrong link
2016-01-22 20:31:55 +09:00
Herr Ritschwumm
1a1267dc60 issue #963: build executable war with sbt, fetch jetty jars at build time. 2016-01-22 01:42:25 +01:00
lidice
361babd327 Fix labels api
Fixed invalid json format
* List all labels for this repository

Fixed wrong http status
* 200 -> 204
2016-01-22 09:33:41 +09:00
lidice
64cacb18a4 Extend API to allow CRUD labels
Add Labels API
    * List all labels for this repository
    * Get a single label
    * Create a label
    * Update a label
    * Delete a label

    Reject duplicated label name

    Add test case for LabelsService
2016-01-22 07:44:35 +09:00
lidice
833cfc3465 (fixes #1062) Remove a wrong link 2016-01-22 01:50:33 +09:00
Naoki Takezoe
95746de5aa Merge pull request #1061 from ShunsukeTadokoro/master
Fix styles
2016-01-21 12:05:01 +09:00
田所駿佑
4f3c780d05 Fix styles 2016-01-21 03:18:08 +09:00
Naoki Takezoe
33a079e55f Merge pull request #1052 from team-lab/change-issue-action-comment-style
Change action-comment styles that follow github.
2016-01-20 03:04:08 +09:00
Naoki Takezoe
27eb1c36bd Update README.md 2016-01-20 01:09:01 +09:00
Naoki Takezoe
5c12cca7f5 Merge branch 'team-lab-feature/protected-branch' 2016-01-19 15:17:52 +09:00
Naoki Takezoe
206d597d9b Fix testcase 2016-01-19 12:50:54 +09:00
Naoki Takezoe
1b4c621fef Change CommitHook to ReceiveHook 2016-01-19 12:26:20 +09:00
Naoki Takezoe
03d4b9e9c6 Change CommitHook interface 2016-01-18 19:46:30 +09:00
nazoking
82a9d9f7cf Change action-comment styles that follow github. 2016-01-18 19:36:34 +09:00
Andreas Jaggi
3e79dcf7a4 Limit recent updated repositories list. #1011 2016-01-17 13:20:37 +01:00
Naoki Takezoe
c9f3ec12a7 Update README.md 2016-01-17 15:24:11 +09:00
Naoki Takezoe
b781696803 Update README.md 2016-01-17 15:20:54 +09:00
Naoki Takezoe
8dcb8c2ecb Fix testcase 2016-01-17 01:48:56 +09:00
Naoki Takezoe
b599219852 Add commit hook extension point for plugins 2016-01-17 01:38:11 +09:00
Naoki Takezoe
58078989f8 Small fix about code style 2016-01-17 01:08:18 +09:00
Naoki Takezoe
4276c0970b Fix version and view 2016-01-17 00:18:23 +09:00
Naoki Takezoe
4abd4a4bac Merge branch 'feature/protected-branch' of https://github.com/team-lab/gitbucket into team-lab-feature/protected-branch 2016-01-17 00:01:18 +09:00
Naoki Takezoe
2b2ae718f7 Fix preview bug in online editor of repository viewer 2016-01-16 22:37:40 +09:00
Naoki Takezoe
ef201e3319 Update haeding style to bold 2016-01-16 22:37:07 +09:00
Naoki Takezoe
0ab28e6daa (refs #1031)Fix issue link processing in Markdown 2016-01-16 22:14:45 +09:00
Naoki Takezoe
9b3e8bd22b Fix Markdown preview style
However styles in markdown-body affect tab. So there are invalid margin at the top of the tab.
2016-01-16 22:00:29 +09:00
Naoki Takezoe
15e8527e01 (refs #984)Fix special character problem in repository viewer 2016-01-16 14:27:44 +09:00
Naoki Takezoe
f991f3b454 Merge branch 'cubdesign-name-truncate' 2016-01-16 14:18:12 +09:00
Naoki Takezoe
d6a39403e2 (refs #1035)Fixup 2016-01-16 14:18:03 +09:00
Naoki Takezoe
aa065fd030 Merge branch 'name-truncate' of https://github.com/cubdesign/gitbucket into cubdesign-name-truncate 2016-01-16 14:15:42 +09:00
Naoki Takezoe
b1b132dfc3 Merge pull request #1029 from team-lab/add-jrebel
Add jrebel
2016-01-15 01:23:41 +09:00
Naoki Takezoe
90c379b3b9 Update README.md 2016-01-13 14:01:40 +09:00
Naoki Takezoe
b71ff6f179 Update README.md 2016-01-13 13:55:59 +09:00
Takeo Tamura
f63542dbd0 delete white-space 2016-01-07 16:55:39 +09:00
Takeo Tamura
63e605d99c fix long file name text-overflow 2016-01-07 16:54:08 +09:00
Takeo Tamura
9e313bb2b3 fix long file name text-overflow 2016-01-07 16:40:44 +09:00
Naoki Takezoe
a03fc4cf4a Merge pull request #1032 from cubdesign/issues-badge
show side menu badge
2016-01-06 23:09:03 +09:00
Naoki Takezoe
d18f92cfbf Merge pull request #1033 from cubdesign/issues-pagination
fix pagination layout
2016-01-06 23:05:33 +09:00
Takeo Tamura
b96821ca44 fix pagination layout 2016-01-06 22:16:52 +09:00
Takeo Tamura
c61ecdef7a show badge 2016-01-06 22:07:20 +09:00
Naoki Takezoe
b50ca63c2b Merge pull request #1030 from team-lab/fix/image-diff
Fix/image diff
2016-01-05 01:09:15 +09:00
Naoki Takezoe
f6624c0002 Merge pull request #1027 from moccos/add_path_to_title
Add path and branch name to the page title
2016-01-05 01:07:47 +09:00
nazoking
d62fc6e135 fix image-diff style 2016-01-04 20:17:52 +09:00
nazoking
926c4d948f remove debug info 2016-01-04 17:11:53 +09:00
nazoking
5fd87ca764 add jrebel document 2016-01-03 22:46:15 +09:00
nazoking
1042c50cc3 add jrebel plugin 2016-01-03 22:09:52 +09:00
moccos
adff36c44b Use repository.repository.defaultBranch instead of "master" 2016-01-03 20:02:27 +09:00
moccos
e8f6d2b990 Add path and branch name to the page title 2016-01-03 05:00:02 +09:00
Naoki Takezoe
a3b3262ad5 Merge pull request #1026 from team-lab/fix/test-merge-service-more-likely-to-succeed
MergeServiceSpec more likely to succeed
2016-01-03 00:43:45 +09:00
Naoki Takezoe
9efba82e94 Merge pull request #1024 from team-lab/fix/test-specs2-junit
fix test ( add specs2-junit )
2016-01-02 23:41:51 +09:00
nazoking
781d465f8d MergeServiceSpec more likely to succeed 2016-01-02 20:58:44 +09:00
nazoking
766867f341 add test 2016-01-02 20:41:23 +09:00
nazoking
e237a44f56 move MergeStatus to service 2016-01-02 20:23:07 +09:00
nazoking
249430449b use common mapper 2016-01-02 20:23:07 +09:00
nazoking
73d935efe3 fix typo 2016-01-02 20:23:07 +09:00
nazoking
79251ef1d1 updete bootstrap 3 2016-01-02 20:23:07 +09:00
nazoking
1081c0f16c remove blank line from basic-template 2016-01-02 20:23:07 +09:00
nazoking
3302b607f1 use real database 2016-01-02 20:23:07 +09:00
nazoking
f13a3ee580 for bootstrap 3 2016-01-02 20:23:07 +09:00
nazoking
927969ac17 remove println(for debug) 2016-01-02 20:23:07 +09:00
nazoking
2d8aa4f8b5 update button 2016-01-02 20:23:07 +09:00
nazoking
645af4d2c0 add protected-branch feature on pull-request page
* show commit-status if context is require status checks to pass.
  * disable merge if new commit-id has not `commit-status` ok on `Status-checkes`.
  * if some status includes required is not success, merge button is disabled.
  * if any required status is success, and some status not includes required, merge button is active, but button color is white.
  * if any required status is success, merge button is active, and button color is green.
2016-01-02 20:20:24 +09:00
nazoking
34240d16b5 Protected mark(octicon-shield) on Branches page 2016-01-02 20:20:24 +09:00
nazoking
89210985d4 Readonly on online-editor 2016-01-02 20:20:24 +09:00
nazoking
9d6258861a add protected branch list on branch setting page. 2016-01-02 20:20:24 +09:00
nazoking
6661314be5 Add git proection api and form 2016-01-02 20:20:24 +09:00
nazoking
e187d026cc create repository setting branches page and move default branch setting to there. 2016-01-02 20:20:24 +09:00
nazoking
100b34085c fix 'cannot create a JUnit XML printer. Please check that specs2-junit.jar is on the classpath' 2016-01-02 18:54:56 +09:00
Naoki Takezoe
ba75079409 Fix repository creation error 2016-01-02 03:08:09 +09:00
Naoki Takezoe
cbb847704b Fix compilation error in testcase 2016-01-02 03:07:53 +09:00
Naoki Takezoe
739c99edce (refs #970)Display permission change 2016-01-02 02:42:38 +09:00
Naoki Takezoe
af2e2f5fd1 Merge branch 'viliamjr-wiki-sidebar' 2016-01-01 22:46:31 +09:00
Naoki Takezoe
09fcf9c003 Merge branch 'wiki-sidebar' of https://github.com/viliamjr/gitbucket into viliamjr-wiki-sidebar
# Conflicts:
#	src/main/twirl/gitbucket/core/wiki/page.scala.html
2016-01-01 22:45:54 +09:00
Naoki Takezoe
5dad0777d7 Deploy assembly jar to snapshot repository if version ends with "-SNAPSHOT" 2016-01-01 19:57:17 +09:00
Naoki Takezoe
be3aa21651 Fix compilation error caused by JGit update 2016-01-01 17:49:32 +09:00
Naoki Takezoe
abd729e45f Update depended libraries 2016-01-01 17:43:22 +09:00
Naoki Takezoe
162e9a9feb Update version to 3.11.0-SNAPSHOT 2016-01-01 15:12:06 +09:00
Naoki Takezoe
7500d017d5 Fix dropdown bug 2016-01-01 14:42:08 +09:00
Naoki Takezoe
e8c4004d5a Remove file appender 2016-01-01 13:13:30 +09:00
Naoki Takezoe
a6f4d9d7cf Upgrade scalatra-forms to 1.0.0 that supports Scalatra 2.4.0 2015-12-31 02:33:11 +09:00
Naoki Takezoe
3399278925 (refs #738)Revert content type detection
because Scalatra automatic charset detection issue has been fixed in Scalatra 2.4.0.
https://github.com/scalatra/scalatra/pull/500
2015-12-30 10:58:06 +09:00
Naoki Takezoe
4457a317e6 (refs #815)Upgrade Scalatra to 2.4.0 2015-12-30 05:27:09 +09:00
Naoki Takezoe
990d082422 3.10 release 2015-12-30 01:18:13 +09:00
Naoki Takezoe
dfe3fbc02f Bump up H2 to 1.4.190 2015-12-29 01:37:52 +09:00
Naoki Takezoe
c077dd0046 Rename logback-test.xml to logback-dev.xml 2015-12-27 14:40:54 +09:00
Naoki Takezoe
9cc6beead7 (refs #990)Provides path for raw contents instead of ?raw=true 2015-12-26 22:48:28 +09:00
Naoki Takezoe
92f6792ad9 Adjust format 2015-12-26 21:20:09 +09:00
Naoki Takezoe
25031eadfb Merge pull request #1017 from swaldman/minor-c3p0-tweaks
Upgrade c3p0 and tweak default config to avoid Tomcat issues
2015-12-26 17:00:33 +09:00
Naoki Takezoe
a7024615e1 Merge pull request #1020 from P1tt187/sshd-RSA-keyprovider
update sshd dependency, rsa keyprovider as default
2015-12-26 16:59:30 +09:00
Naoki Takezoe
8191a4bedb (refs #1004)Fix edit webhook link url 2015-12-26 03:25:49 +09:00
Naoki Takezoe
7e1f5aa353 Fix branch control dropdown 2015-12-25 10:26:30 +09:00
Fabian Markert
44e161f6ec update version 2015-12-24 17:16:20 +01:00
Fabian Markert
7bd17ed038 update sshd dependency
migrate to new sshd api
change the default key provider algorithm to RSA
2015-12-24 17:11:40 +01:00
Naoki Takezoe
a47404130b Don't break lines in Wiki or markdown files on the repository viewer 2015-12-24 02:58:39 +09:00
Naoki Takezoe
669949aa8c Update docs 2015-12-23 23:30:10 +09:00
Naoki Takezoe
c773bb90f6 Update docs 2015-12-23 23:26:59 +09:00
Naoki Takezoe
18f525523b Update docs 2015-12-23 23:06:22 +09:00
Naoki Takezoe
3bfd228df1 Update docs 2015-12-23 23:03:32 +09:00
Naoki Takezoe
c072ab2dc5 Update docs 2015-12-23 22:44:20 +09:00
Naoki Takezoe
53e06c5e86 (refs #1018)Move to xsbt-web-plugin from scalatra-sbt 2015-12-23 21:37:25 +09:00
Steve Waldman
b0a749bdc1 Upgrade c3p0 and tweak default config to avoid Tomcat issues
Adds a standard typesafe-config reference.conf file that sets c3p0
config to prevents Tomcat memory leaks on hot-redeploy/shutdown.

See
   http://www.mchange.com/projects/c3p0/#configuring_to_avoid_memory_leaks_on_redeploy
   http://www.mchange.com/projects/c3p0/#tomcat-specific

This new config can be overridden in an application.conf or
c3p0.properties file.

( See http://www.mchange.com/projects/c3p0/#configuration_precedence )

Updates c3p0 version to c3p0-0.9.5.2, which fixes that leads to
unnecessary String allocation when logging via slf4j.
2015-12-21 22:51:12 -08:00
Naoki Takezoe
809443a46f Zip logback.xml into the root of gitbucket.war 2015-12-22 07:59:47 +09:00
Naoki Takezoe
59ef44dd71 Update logging configuration 2015-12-22 07:59:15 +09:00
Naoki Takezoe
d77973a29e Remove IDE plugins and add logback-classic 2015-12-22 07:58:30 +09:00
Naoki Takezoe
be6bc16d3d Fix global header style 2015-12-21 15:14:51 +09:00
Naoki Takezoe
407e926abe Fix styles in file editing form in the repository viewer 2015-12-21 15:14:21 +09:00
Naoki Takezoe
117655b011 (refs #1002)Upgrade markedj to 1.0.6-SNAPSHOT to solve IndexOutOfBoundsException in the table 2015-12-19 12:56:34 +09:00
Naoki Takezoe
2ea4a5a7ef (refs #1007)Fix max length constraint for repository owner and name at the repository creation form 2015-12-18 01:46:25 +09:00
Naoki Takezoe
30b0279efe (refs #1005)Fix typo 2015-12-18 01:40:32 +09:00
Naoki Takezoe
7511701bb2 Merge pull request #1009 from team-lab/fix/webhook-push-pusher
(fixes #1008) pusher of webhook push event is not same as github's it
2015-12-18 01:32:18 +09:00
Naoki Takezoe
a919ae3430 Merge pull request #1010 from team-lab/suppot-legacy-statuses-api
support legacy route of List Statuses for a specific Ref api
2015-12-18 01:31:28 +09:00
nazoking
c30ee24f8d support legacy route of List Statuses for a specific Ref api 2015-12-16 21:54:47 +09:00
nazoking
10b5de571e (fixes #1008) pusher of webhook push event is not same as github's it 2015-12-16 21:41:14 +09:00
Naoki Takezoe
c6f4ec7250 Fix header-link style 2015-12-15 19:51:16 +09:00
Naoki Takezoe
b229058726 Fix fork button group style 2015-12-15 19:39:18 +09:00
Naoki Takezoe
21363794b6 Merge pull request #999 from gitbucket/css_bootstrap3
Move to Bootstrap3 with GitHub theme
2015-12-15 15:58:54 +09:00
Naoki Takezoe
1754d37ac0 Fix style of circle icons 2015-12-15 15:13:54 +09:00
Naoki Takezoe
1c36b185ec Fix width and font of the dropzone 2015-12-15 14:56:24 +09:00
Naoki Takezoe
1a4b94e5df Fix modal dialog 2015-12-15 11:46:47 +09:00
Naoki Takezoe
31bd8e1741 Fix button size 2015-12-14 18:48:22 +09:00
Naoki Takezoe
e8824ad1ac Imporve styles for mobile 2015-12-14 01:56:15 +09:00
Naoki Takezoe
17636a5ecf Fixup 2015-12-14 01:11:42 +09:00
Naoki Takezoe
8f36ff60de Use bootstrap panel instead of custom box styles 2015-12-14 00:16:18 +09:00
Naoki Takezoe
7ad605afba Fix style of issue comment form and more 2015-12-13 23:59:42 +09:00
Naoki Takezoe
f080e8693e Fix right margin of account search box 2015-12-12 13:20:05 +09:00
Naoki Takezoe
e27bf7868f Tweak pagination padding 2015-12-10 17:17:18 +09:00
Naoki Takezoe
db55fe9ff3 Fixup 2015-12-10 16:02:58 +09:00
Naoki Takezoe
de4db0764c Use Bootstrap panel instead of custom CSS 2015-12-10 14:53:19 +09:00
Naoki Takezoe
9d644b4625 Fix buttons of webhook list 2015-12-10 14:04:44 +09:00
Naoki Takezoe
354dc27e84 Use Bootstrap panel instead of custom CSS 2015-12-10 12:13:22 +09:00
Naoki Takezoe
1c639474d3 Fix milestone list and progress bar 2015-12-10 12:03:22 +09:00
Naoki Takezoe
4d8b47732e Remove unused bootstrap files 2015-12-10 02:03:46 +09:00
Naoki Takezoe
8650abf679 Fix milestone creation / editing form 2015-12-10 02:00:42 +09:00
Naoki Takezoe
27dcc2ef48 Fix table vertical border 2015-12-10 01:16:17 +09:00
Naoki Takezoe
94a12cd28c Fix label editing form 2015-12-09 19:16:34 +09:00
Naoki Takezoe
a982b64fd4 Use Bootstrap panels instead of GitBucket original styles 2015-12-09 18:29:41 +09:00
Naoki Takezoe
ee5b4bbf2e Fix styles 2015-12-09 02:48:10 +09:00
Naoki Takezoe
d09ddd8d07 Fix tables and some pages 2015-12-08 19:21:08 +09:00
Naoki Takezoe
c7bf47820c Use customized Bootstrap3 theme 2015-12-08 09:33:22 +09:00
Naoki Takezoe
4dfb01d59e Fix typo 2015-12-08 02:35:52 +09:00
Naoki Takezoe
54bef6505f Fix issue search box 2015-12-08 02:26:11 +09:00
Naoki Takezoe
2aa71ed217 Fix borderd table style 2015-12-08 01:59:31 +09:00
Viliam Dias
ef3b02b718 A simple wiki page (markdown file "_Footer.md") is used as footer for the wiki.
Github specification:
https://help.github.com/articles/creating-a-footer/

- the footer is rendered below the wiki page content.
- a direct link to edit the footer.
2015-12-07 13:45:37 -02:00
Naoki Takezoe
d90938421f Fix ZeroClipboard button 2015-12-07 23:19:30 +09:00
Naoki Takezoe
e4992bbd17 Fix styles of repository setting pages 2015-12-07 16:56:38 +09:00
Naoki Takezoe
7853b22522 Fix sidebar style 2015-12-07 16:01:03 +09:00
Naoki Takezoe
9475215fa6 Fix styles of plugins page 2015-12-07 15:20:06 +09:00
Naoki Takezoe
8622bbd2ac Fix styles of account and group creation page 2015-12-07 14:38:18 +09:00
Naoki Takezoe
0cdcc252e0 Merge branch 'master' into css_bootstrap3
Conflicts:
	src/main/twirl/gitbucket/core/account/edit.scala.html
2015-12-07 08:33:22 +09:00
Naoki Takezoe
9477cca8e8 Fix box style 2015-12-05 19:54:32 +09:00
Naoki Takezoe
176e427058 Update README.md for 3.9 release 2015-12-05 19:48:44 +09:00
Naoki Takezoe
1bf26cacb9 Update version to 3.9.0 2015-12-05 19:46:29 +09:00
Naoki Takezoe
38e8247483 Fix styles of system administration page 2015-12-05 19:45:07 +09:00
Naoki Takezoe
44aa12108c Fix styles of the repository creation page and more 2015-12-05 14:54:50 +09:00
Naoki Takezoe
dd73405aa9 Fix styles 2015-12-05 12:41:55 +09:00
Naoki Takezoe
6710393a53 Merge branch 'master' into css_bootstrap3 2015-12-05 11:53:15 +09:00
Naoki Takezoe
711aee1c8b Fix markedj version as 1.0.5 2015-12-05 03:54:51 +09:00
Naoki Takezoe
51be1048d5 (refs #987)Remove user related data when users delete themselves 2015-12-05 03:45:26 +09:00
Naoki Takezoe
50166f04d8 Disable auto completion in the select user field 2015-12-05 03:43:57 +09:00
Naoki Takezoe
7eb6fea08b Fix typo in comment 2015-12-05 03:12:21 +09:00
Naoki Takezoe
8e3c054da4 Merge pull request #983 from team-lab/feature/webhook-review-comment
Send webhook on create pull request review comment
2015-12-05 01:14:54 +09:00
nazoking
5249b67df1 Merge branch 'master' into feature/webhook-review-comment 2015-12-03 20:09:46 +09:00
Naoki Takezoe
583830c1c2 Fix styles 2015-12-01 02:47:09 +09:00
Naoki Takezoe
bb75f1c034 Merge branch 'css_bootstrap3' of https://github.com/takezoe/gitbucket into css_bootstrap3 2015-12-01 02:12:08 +09:00
Naoki Takezoe
b7a9236283 Merge branch 'master' of https://github.com/takezoe/gitbucket into css_bootstrap3 2015-12-01 01:37:14 +09:00
Naoki Takezoe
7e0e172d37 Fix fork button 2015-11-30 23:22:16 +09:00
Naoki Takezoe
ea37a34476 Fix styles of issues and pull requests 2015-11-30 01:16:27 +09:00
Naoki Takezoe
fb5564de07 Fix diff styles 2015-11-27 12:53:10 +09:00
Naoki Takezoe
cead7e9e4e Fix commit list style 2015-11-27 12:37:26 +09:00
Naoki Takezoe
7cf23b6c56 Fix pull request creation form style 2015-11-27 12:37:16 +09:00
Naoki Takezoe
1080821862 Fix issue styles 2015-11-27 10:16:17 +09:00
Naoki Takezoe
0bf843ce2a Fix styles 2015-11-27 10:10:15 +09:00
Naoki Takezoe
b8bd8f5512 Fix branch list styles 2015-11-27 09:43:17 +09:00
Naoki Takezoe
12fba30ef8 Fix styles 2015-11-25 10:44:55 +09:00
Naoki Takezoe
dc5cb74f4d Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-11-18 01:48:47 +09:00
Naoki Takezoe
446a524b5a Upgrade scalatra-forms to 0.2.0 to solve performance issue 2015-11-18 01:48:40 +09:00
Naoki Takezoe
121110aede Fix markdown header style 2015-11-17 23:19:08 +09:00
Naoki Takezoe
c76a8562ea Merge pull request #995 from uli-heller/patch-2
extensibility is probably the right word?
2015-11-17 19:28:00 +09:00
uli-heller
495d8069f5 extensibility is probably the right word? 2015-11-17 09:51:50 +01:00
Naoki Takezoe
70af6d0c35 Update README.md 2015-11-17 16:04:58 +09:00
Viliam Dias
9777d543b1 Remove the getWikiSideBar service
And use getWikiPage to avoid code repetition.
2015-11-16 16:35:07 -02:00
Naoki Takezoe
abfc6eea1e Fix repository setting pages presentation 2015-11-17 01:52:14 +09:00
Naoki Takezoe
fd2f3e252c Fix webhook editing presentation and performance issue 2015-11-17 01:51:55 +09:00
Naoki Takezoe
3b6d0065a2 Once delete editHooks.scala.html to rename 2015-11-17 01:48:13 +09:00
Viliam Dias
51acf72e0d A sidebar page for the wiki
A simple wiki page (markdown file "_Sidebar.md") is used as a sidebar menu for the wiki.

Github specification:
https://help.github.com/articles/creating-a-sidebar/

- the sidebar is rendered below the pages index.
- if you click on page index "counter" you can collapse or expand the
  page list.
2015-11-16 14:33:02 -02:00
Viliam Dias
6ad724d80c A service to get the wiki sidebar page
- getWikiSideBar: a new service to get the wiki sidebar file "_Sidebar.md".
- getFileContent: must ignore MD files which starts with "_".
2015-11-16 14:25:22 -02:00
Naoki Takezoe
7ffe2ab864 Fix global header style 2015-11-15 12:54:34 +09:00
Naoki Takezoe
2f6febb748 Adjust styles for Bootstrap3 2015-11-15 02:27:55 +09:00
Naoki Takezoe
092c897150 Fix sidebar style 2015-11-15 02:05:24 +09:00
Naoki Takezoe
30f072f925 Merge pull request #992 from team-lab/fix/lost-webhook-events-on-rename-repository
fix lost web hook events on repository renamed.
2015-11-14 15:02:41 +09:00
nazoking
72d0282613 Merge branch 'master' into feature/webhook-review-comment 2015-11-14 13:27:40 +09:00
nazoking
a56f766497 fix lost web hook events on repository renamed. 2015-11-14 13:16:13 +09:00
Naoki Takezoe
31b5583896 Fix header styles 2015-11-13 02:32:04 +09:00
Naoki Takezoe
7c95bfc77e Merge branch 'master' into css_bootstrap3 2015-11-12 03:45:00 +09:00
Naoki Takezoe
c6804fe589 Add bootstrap-theme-github 2015-11-12 03:44:37 +09:00
Naoki Takezoe
fc41e282ce Merge pull request #973 from gitbucket/commit_comment_count
Count COMMIT_COMMENT as issue comment
2015-11-10 11:48:20 +09:00
Naoki Takezoe
d3b21a4e39 Drop PULL_REQUEST column from COMMIT_COMMENT table 2015-11-10 11:35:59 +09:00
Naoki Takezoe
7b6d9850a2 Moving to Bootstrap3 2015-11-10 11:06:11 +09:00
nazoking
84d97817e3 add comment 2015-11-09 13:57:37 +09:00
nazoking
2c6d2176bb fix space 2015-11-09 13:57:26 +09:00
nazoking
7840d28ed2 add webhook pull-request-review-comment 2015-11-09 12:20:07 +09:00
Naoki Takezoe
d4fd2e6cd6 Remove bottom margin of inline comment in diff view 2015-11-09 01:20:44 +09:00
Naoki Takezoe
70e8385246 (refs #974)Not hide add-comment icon when adding comment form is active. 2015-11-09 01:15:04 +09:00
Naoki Takezoe
3fd23f1749 Fix markdown preview style 2015-11-08 22:26:43 +09:00
Naoki Takezoe
a3e3aea4e0 Fix styles 2015-11-08 19:08:05 +09:00
Naoki Takezoe
dcfb8ad8eb (refs #976)Enable GFM line breaks 2015-11-08 18:03:44 +09:00
Naoki Takezoe
35f82e91bc (refs #976)Upgrade markedj to 1.0.5-SNAPSHOT 2015-11-08 17:49:25 +09:00
Naoki Takezoe
f1533cb168 Count COMMIT_COMMENT as issue comment 2015-11-08 04:06:14 +09:00
Naoki Takezoe
763fbec0ca Update README.md 2015-11-08 02:58:54 +09:00
Naoki Takezoe
111c893d94 Merge pull request #968 from justinpitts/master
Correct typo on system administration page.
2015-11-07 03:35:39 +09:00
Naoki Takezoe
80c38a45c5 Merge pull request #941 from team-lab/feature/webhook-scope
feature/can select event trigger of webhook.
2015-11-07 03:35:09 +09:00
nazoking
548860607b update migration version to 3.9 from 3.8 2015-11-06 14:52:36 +09:00
nazoking
66b5fe7337 Merge branch 'master' into feature/webhook-scope 2015-11-06 14:48:03 +09:00
Justin Pitts
2b2a117912 Correct typo on system administration page. 2015-11-04 08:51:48 -05:00
Naoki Takezoe
39a10fd167 Update README.md 2015-11-04 02:00:14 +09:00
Naoki Takezoe
93f8dbd42a Fix markedj version 2015-11-02 08:58:29 +09:00
Naoki Takezoe
4f280d0df8 Fix blockquote font size 2015-11-02 02:33:06 +09:00
Naoki Takezoe
5e9ff3278a Fix the default number of displayed repositories 2015-11-02 00:32:46 +09:00
Naoki Takezoe
b4b68f0e17 Update README.md 2015-10-31 11:55:16 +09:00
nazoking
a08a212fdc feature/can select webhook events 2015-10-13 18:48:02 +09:00
247 changed files with 27401 additions and 14361 deletions

7
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# Guideline for Issues
- At first, See [FAQ](https://github.com/gitbucket/gitbucket/wiki/FAQ) 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 Japaneses 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.
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.

19
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,19 @@
### Before submitting an issue to Gitbucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
- [] searched for similar already existing issue
- [] read the documentation and [wiki](https://github.com/gitbucket/gitbucket/wiki)
*(if you have performed all the above, remove the paragraph and continue describing the issue with template below)*
## Issue
**Impacted version**: xxxx
**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)*

8
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,8 @@
### Before submitting a pull-request to Gitbucket I have first:
- [] read the [contribution guidelines](https://github.com/gitbucket/gitbucket/blob/master/CONTRIBUTING.md)
- [] rebased my branch over master
- [] verified that project is compiling
- [] verified that tests are passing
- [] squashed my commits as appropriate *(keep several commits if it is relevant to understand the PR)*
- [] [marked as closed](https://help.github.com/articles/closing-issues-via-commit-messages/) all issue ID that this PR should correct

View File

@@ -1,7 +0,0 @@
# Guideline for Issues
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
- Make sure check whether there is a same question or request in the past.
- When raise a new issue, write subject in **English** at least.
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.

View File

@@ -1,8 +1,7 @@
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket)
=========
GitBucket is the easily installable GitHub clone powered by Scala.
GitBucket is a GitHub clone powered by Scala which has easy installation and high extensibility.
Features
--------
@@ -10,28 +9,21 @@ The current version of GitBucket provides a basic features below:
- Public / Private Git repository (http and ssh access)
- Repository viewer and online file editing
- Repository search (Code and Issues)
- Wiki
- Issues
- Fork / Pull request
- Issues / Pull request
- Email notification
- Activity timeline
- Simple user and group management with LDAP integration
- Gravatar support
- Plug-in system
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki).
If you want to try the development version of GitBucket, see [Developer's Guide](https://github.com/gitbucket/gitbucket/blob/master/doc/how_to_run.md).
Installation
--------
GitBucket requires **Java8**. You have to install beforehand when it's not installed.
1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases).
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx)
The default administrator account is **root** and password is **root**.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser and logged-in with **root** / **root**.
or you can start GitBucket by `java -jar gitbucket.war` without servlet container. In this case, GitBucket URL is **http://[hostname]:8080/**. You can specify following options.
@@ -40,37 +32,9 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
- --host=[HOSTNAME]
- --gitbucket.home=[DATA_DIR]
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
To upgrade GitBucket, only replace gitbucket.war after stop GitBucket. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X
#### Installing Via Homebrew
```
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
To have launchd start gitbucket at login:
ln -sfv /usr/local/opt/gitbucket/*.plist ~/Library/LaunchAgents
Then to load gitbucket now:
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.gitbucket.plist
Or, if you don't want/need launchctl, you can just run:
java -jar /usr/local/opt/gitbucket/libexec/gitbucket.war
==> Summary
/usr/local/Cellar/gitbucket/1.10: 3 files, 42M, built in 11 seconds
```
#### Manual Installation
On OS X, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
About installation on Mac or Windows Server (with IIS), configuration of Apache or Nginx and also integration with other tools or services such as Jenkins or Slack, see [Wiki](https://github.com/gitbucket/gitbucket/wiki).
Plug-ins
--------
@@ -81,6 +45,7 @@ GitBucket has the plug-in system to extend GitBucket from outside of GitBucket.
- [gitbucket-h2-backup-plugin](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin)
- [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin)
- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin)
- [gitbucket-asciidoctor-plugin](https://github.com/lefou/gitbucket-asciidoctor-plugin)
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
@@ -95,8 +60,35 @@ Support
Release Notes
--------
### 3.12 - 27 Feb 2016
- New GitHub UI
- Improve mobile view
- Improve printing style
- Individual URL for pull request tabs
- SSH host configuration is separated from HTTP base URL
### 3.11 - 30 Jan 2016
- Upgrade Scalatra to 2.4
- Sidebar and Footer for Wiki
- Branch protection and receive hook extension point for plug-in
- Limit recent updated repositories list
- Issue actions look-alike GitHub
- Web API for labels
- Requires Java 8
### 3.10 - 30 Dec 2015
- Move to Bootstrap3
- New URL for raw contents (`raw/master/doc/activity.md` instead of `blob/master/doc/activity.md?raw=true`)
- Update xsbt-web-plugin
- Update H2 database
### 3.9 - 5 Dec 2015
- GFM inline breaks support in Markdown
- WebHook on create review comment is available
- WebHook event trigger is selectable
### 3.8 - 31 Oct 2015
- Moved to organization
- Moved to GitHub organization
- Omit diff view for large differences
- Repository creation API
- Render url as link in repository description
@@ -331,7 +323,3 @@ Release Notes
### 1.0 - 04 Jul 2013
- This is a first public release
Sponsors
--------
[![IntelliJ IDEA](https://www.jetbrains.com/idea/docs/logo_intellij_idea.png)](https://www.jetbrains.com/idea/)

159
build.sbt Normal file
View File

@@ -0,0 +1,159 @@
val Organization = "gitbucket"
val Name = "gitbucket"
val GitBucketVersion = "3.12.0"
val ScalatraVersion = "2.4.0"
val JettyVersion = "9.3.6.v20151106"
lazy val root = (project in file(".")).enablePlugins(SbtTwirl, JettyPlugin)
sourcesInBase := false
organization := Organization
name := Name
version := GitBucketVersion
scalaVersion := "2.11.7"
// dependency settings
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
)
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "4.1.1.201511131810-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "4.1.1.201511131810-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.3.0",
"io.github.gitbucket" %% "scalatra-forms" % "1.0.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.6",
"org.apache.commons" % "commons-compress" % "1.10",
"org.apache.commons" % "commons-email" % "1.4",
"org.apache.httpcomponents" % "httpclient" % "4.5.1",
"org.apache.sshd" % "apache-sshd" % "1.0.0",
"org.apache.tika" % "tika-core" % "1.11",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.190",
"ch.qos.logback" % "logback-classic" % "1.1.1",
"com.mchange" % "c3p0" % "0.9.5.2",
"com.typesafe" % "config" % "1.3.0",
"com.typesafe.akka" %% "akka-actor" % "2.3.14",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.4.0-akka-2.3.x" exclude("c3p0","c3p0"),
"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.scalaz" %% "scalaz-core" % "7.2.0" % "test"
)
// Twirl settings
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._"
// Compiler settings
scalacOptions := Seq("-deprecation", "-language:postfixOps")
javacOptions in compile ++= Seq("-target", "8", "-source", "8")
javaOptions in Jetty += "-Dlogback.configurationFile=/logback-dev.xml"
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console")
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test"
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() )
fork in Test := true
packageOptions += Package.MainClass("JettyLauncher")
// Assembly settings
test in assembly := {}
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) =>
(xs map {_.toLowerCase}) match {
case ("manifest.mf" :: Nil) => MergeStrategy.discard
case _ => MergeStrategy.discard
}
case x => MergeStrategy.first
}
// JRebel
jrebel.webLinks += (target in webappPrepare).value
jrebel.enabled := System.getenv().get("JREBEL") != null
javaOptions in Jetty ++= Option(System.getenv().get("JREBEL")).toSeq.flatMap { path =>
Seq("-noverify", "-XX:+UseConcMarkSweepGC", "-XX:+CMSClassUnloadingEnabled", s"-javaagent:${path}")
}
jrebelSettings
// Create executable war file
val executableConfig = config("executable").hide
Keys.ivyConfigurations += executableConfig
libraryDependencies ++= Seq(
"org.eclipse.jetty" % "jetty-security" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-webapp" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-continuation" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-server" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-xml" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-http" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-servlet" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-io" % JettyVersion % "executable",
"org.eclipse.jetty" % "jetty-util" % JettyVersion % "executable"
)
val executableKey = TaskKey[File]("executable")
executableKey := {
import org.apache.ivy.util.ChecksumHelper
import java.util.jar.{ Manifest => JarManifest }
import java.util.jar.Attributes.{ Name => AttrName }
val workDir = Keys.target.value / "executable"
val warName = Keys.name.value + ".war"
val log = streams.value.log
log info s"building executable webapp in ${workDir}"
// initialize temp directory
val temp = workDir / "webapp"
IO delete temp
// include jetty classes
val jettyJars = Keys.update.value select configurationFilter(name = executableConfig.name)
jettyJars foreach { jar =>
IO unzip (jar, temp, (name:String) =>
(name startsWith "javax/") ||
(name startsWith "org/")
)
}
// include original war file
val warFile = (Keys.`package`).value
IO unzip (warFile, temp)
// include launcher classes
val classDir = (Keys.classDirectory in Compile).value
val launchClasses = Seq("JettyLauncher.class" /*, "HttpsSupportConnector.class" */)
launchClasses foreach { name =>
IO copyFile (classDir / name, temp / name)
}
// 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")
val outputFile = workDir / warName
IO jar (contentMappings, outputFile, manifest)
// generate checksums
Seq("md5", "sha1") foreach { algorithm =>
IO.write(
workDir / (warName + "." + algorithm),
ChecksumHelper computeAsString (outputFile, algorithm)
)
}
// done
log info s"built executable webapp ${outputFile}"
outputFile
}
/*
Keys.artifact in (Compile, executableKey) ~= {
_ copy (`type` = "war", extension = "war"))
}
addArtifact(Keys.artifact in (Compile, executableKey), executableKey)
*/

60
doc/authenticator.md Normal file
View File

@@ -0,0 +1,60 @@
Authentication in Controller
========
GitBucket provides many [authenticators](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/util/Authenticator.scala) to access controlling in the controller.
For example, in the case of `RepositoryViwerController`,
it references three authenticators: `ReadableUsersAuthenticator`, `ReferrerAuthenticator` and `CollaboratorsAuthenticator`.
```scala
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService =>
...
```
Authenticators provides a method to add guard to actions in the controller:
- `ReadableUsersAuthenticator` provides `readableUsersOnly` method
- `ReferrerAuthenticator` provides `referrersOnly` method
- `CollaboratorsAuthenticator` provides `collaboratorsOnly` method
These methods are available in each actions as below:
```scala
// Allows only the repository owner (or manager for group repository) and administrators.
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
...
})
// Allows only collaborators and administrators.
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
...
})
// Allows only signed in users which can access the repository.
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
...
})
```
Currently, GitBucket provides below authenticators:
|Trait | Method | Description |
|--------------------------|-----------------|--------------------------------------------------------------------------------------|
|OneselfAuthenticator |oneselfOnly |Allows only oneself and administrators. |
|OwnerAuthenticator |ownerOnly |Allows only the repository owner and administrators. |
|UsersAuthenticator |usersOnly |Allows only signed in users. |
|AdminAuthenticator |adminOnly |Allows only administrators. |
|CollaboratorsAuthenticator|collaboratorsOnly|Allows only collaborators and administrators. |
|ReferrerAuthenticator |referrersOnly |Allows only the repository owner (or manager for group repository) and administrators.|
|ReadableUsersAuthenticator|readableUsersOnly|Allows only signed in users which can access the repository. |
|GroupManagerAuthenticator |managersOnly |Allows only the group managers. |
Of course, if you make a new plugin, you can define a your own authenticator according to requirement in your plugin.

View File

@@ -8,7 +8,7 @@ To release a new version of GitBucket, add the version definition to the [gitbuc
object AutoUpdate {
...
/**
* The history of versions. A head of this sequence is the current BitBucket version.
* The history of versions. A head of this sequence is the current GitBucket version.
*/
val versions = Seq(
Version(1, 0)
@@ -20,7 +20,7 @@ Next, add a SQL file which updates database schema into [/src/main/resources/upd
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
We can also add any Scala code for upgrade GitBucket which modifies esources other than database. Override ```Version.update``` like below:
We can also add any Scala code for upgrade GitBucket which modifies resources other than database. Override ```Version.update``` like below:
```scala
val versions = Seq(

View File

@@ -1,48 +1,56 @@
About Action in Issue Comment
========
After the issue creation at GitBucket, users can add comments or close it.
The details are saved at ```ISSUE_COMMENT``` table.
The details are saved at `ISSUE_COMMENT` table.
To determine if it was any operation, you see the ```ACTION``` column.
To determine if it was any operation, you see the `ACTION` column.
And in the case of some actions, `CONTENT` column value contains additional information.
|ACTION|
|--------|
|comment|
|close_comment|
|reopen_comment|
|close|
|reopen|
|commit|
|merge|
|delete_branch|
|refer|
|ACTION |CONTENT |
|---------------|-----------------|
|comment |comment |
|close_comment |comment |
|reopen_comment |comment |
|close |"Close" |
|reopen |"Reopen" |
|commit |comment commitId |
|merge |comment |
|delete_branch |branchName |
|refer |issueId:title |
### comment
#####comment
This value is saved when users have made a normal comment.
#####close_comment, reopen_comment
### close_comment, reopen_comment
These values are saved when users have reopened or closed the issue with comments.
#####close, reopen
### close, reopen
These values are saved when users have reopened or closed the issue.
At the same time, store the fixed value(i.e. "Close" or "Reopen") to the ```CONTENT``` column.
At the same time, store the fixed value(i.e. "Close" or "Reopen") to the `CONTENT` column.
Therefore, this comment is not displayed, and not counted as a comment.
#####commit
This value is saved when users have pushed including the ```#issueId``` to the commit message.
At the same time, store it to the ```CONTENT``` column with its commit id.
### commit
This value is saved when users have pushed including the `#issueId` to the commit message.
At the same time, store it to the `CONTENT` column with its commit id.
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
#####merge
### merge
This value is saved when users have merged the pull request.
At the same time, store the message to the ```CONTENT``` column.
At the same time, store the message to the `CONTENT` column.
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
#####delete_branch
### delete_branch
This value is saved when users have deleted the branch. Users can delete branch after merging pull request which is requested from the same repository.
At the same time, store it to the ```CONTENT``` column with the deleted branch name.
At the same time, store it to the `CONTENT` column with the deleted branch name.
Therefore, this comment is not displayed, and not counted as a comment.
#####refer
This value is saved when other issue or issue comment contains reference to the issue like ```#issueId```.
At the same time, store id and title of the referrer issue as ```id:title```.
### refer
This value is saved when other issue or issue comment contains reference to the issue like `#issueId`.
At the same time, store id and title of the referrer issue as `id:title`.

View File

@@ -8,10 +8,10 @@ This directory has following structure:
* /HOME/gitbucket
* /repositories
* /USER_NAME
* / REPO_NAME.git (substance of repository. GitServlet sees this directory)
* / REPO_NAME
* /REPO_NAME.git (substance of repository. GitServlet sees this directory)
* /REPO_NAME
* /issues (files which are attached to issue)
* / REPO_NAME.wiki.git (wiki repository)
* /REPO_NAME.wiki.git (wiki repository)
* /data
* /USER_NAME
* /files

View File

@@ -1,63 +1,34 @@
How to run from the source tree
========
for Testers
Run for Development
--------
If you want to test GitBucket, input following command at the root directory of the source tree.
```
C:\gitbucket> sbt ~container:start
$ sbt ~jetty:start
```
Then access to `http://localhost:8080/` by your browser. The default administrator account is `root` and password is `root`.
for Developers
--------
If you want to modify source code and confirm it, you can run GitBucket in auto reloading mode as following:
Windows:
```
C:\gitbucket> sbt
...
> container:start
...
> ~ ;copy-resources;aux-compile
```
Linux:
```
~/gitbucket$ ./sbt.sh
...
> container:start
...
> ~ ;copy-resources;aux-compile
```
Source code modification is detected and reloaded automatically. You can modify logging configuration by editing `src/main/resources/logback-dev.xml`.
Build war file
--------
To build war file, run the following command:
Windows:
```
C:\gitbucket> sbt package
```
Linux:
```
~/gitbucket$ ./sbt.sh package
$ sbt package
```
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
To build executable war file, run
* Windows: Not available
* Linux: `./release/make-release-war.sh`
```
$ sbt executable
```
at the top of the source tree. It generates executable `gitbucket.war` into `target/scala-2.11`. We release this war file as release artifact.
at the top of the source tree. It generates executable `gitbucket.war` into `target/executable`. We release this war file as release artifact.

148
doc/jrebel.md Normal file
View File

@@ -0,0 +1,148 @@
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:
```
> 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.
----
## 1. Get a JRebel license
Sign up for a [usage plan](https://my.jrebel.com/). 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/).
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.
You don't need to integrate with your IDE, since we're using sbt to do the servlet deployment.
## 4. Tell jvm where JRebel is
Fortunately, the gitbucket project is already set up to use JRebel.
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
```
For example, if you unzipped your JRebel download in your home directory, you whould use:
```bash
export JREBEL=~/jrebel/jrebel.jar
```
Now reload your shell:
```
$ source ~/.bash_profile # on Mac
$ source ~/.bashrc # on Linux
```
## 5. See it in action!
Now you're ready to use JRebel with the gitbucket.
When you run sbt as normal, you will see a long message from JRebel, indicating it has loaded.
Here's an abbreviated version of what you will see:
```
$ ./sbt
[info] Loading project definition from /git/gitbucket/project
[info] Set current project to gitbucket (in build file:/git/gitbucket/)
>
```
You will start the servlet container slightly differently now that you're using sbt.
```
> jetty:start
:
[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:
:
> ~ copy-resources
[success] Total time: 0 s, completed Jan 3, 2016 9:13:54 PM
1. Waiting for source changes... (press enter to interrupt)
```
Finally, change your code.
For example, you can change the title on `src/main/twirl/gitbucket/core/main.scala.html` like this:
```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>
}
change code !!!!!!!!!!!!!!!!
</a>
:
```
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)
```
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$'.
```
## 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`.

View File

@@ -3,9 +3,10 @@ Developer's Guide
* [How to run from source tree](how_to_run.md)
* [Directory Structure](directory.md)
* [Mapping and Validation](validation.md)
* Authentication in Controller (not yet)
* [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)

View File

@@ -23,7 +23,7 @@ object MyBuild extends Build {
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
* The history of versions. A head of this sequence is the current GitBucket version.
*/
val versions = Seq(
new Version(3, 3), // <---- add this line!!
@@ -37,11 +37,10 @@ Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https
### Make release war file
Run `release/make-release-war.sh`. The release war file is generated into `target/scala-2.11/gitbucket.war`.
Run `sbt executable`. The release war file and fingerprint are generated into `target/executable/gitbucket.war`.
```bash
$ cd release
$ ./make-release-war.sh
$sbt executable
```
### Deploy assembly jar file

View File

@@ -1,11 +0,0 @@
#!/bin/bash
version=$1
output_dir=`dirname $0`
git rm -f ${output_dir}/jetty-*.jar
for name in 'io' 'servlet' 'xml' 'continuation' 'security' 'util' 'http' 'server' 'webapp'
do
jar_filename="jetty-${name}-${version}.jar"
wget "http://repo1.maven.org/maven2/org/eclipse/jetty/jetty-${name}/${version}/${jar_filename}" -O ${output_dir}/${jar_filename}
done
git add ${output_dir}/*.jar
git commit

View File

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

View File

@@ -1,81 +0,0 @@
import sbt._
import Keys._
import org.scalatra.sbt._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
import sbtassembly._
import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.8.0"
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
lazy val project = Project (
"gitbucket",
file(".")
)
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
.settings(
test in assembly := {},
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) =>
(xs map {_.toLowerCase}) match {
case ("manifest.mf" :: Nil) => MergeStrategy.discard
case _ => MergeStrategy.discard
}
case x => MergeStrategy.first
}
)
.settings(
sourcesInBase := false,
organization := Organization,
name := Name,
version := Version,
scalaVersion := ScalaVersion,
resolvers ++= Seq(
Classpaths.typesafeReleases,
"amateras-repo" at "http://amateras.sourceforge.jp/mvn/",
"amateras-snapshot-repo" at "http://amateras.sourceforge.jp/mvn-snapshot/"
),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.2.201412180340-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.2.201412180340-r",
"org.scalatra" %% "scalatra" % ScalatraVersion,
"org.scalatra" %% "scalatra-specs2" % ScalatraVersion % "test",
"org.scalatra" %% "scalatra-json" % ScalatraVersion,
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4",
"io.github.gitbucket" % "markedj" % "1.0.4-SNAPSHOT",
"org.apache.commons" % "commons-compress" % "1.9",
"org.apache.commons" % "commons-email" % "1.3.3",
"org.apache.httpcomponents" % "httpclient" % "4.3.6",
"org.apache.sshd" % "apache-sshd" % "0.11.0",
"org.apache.tika" % "tika-core" % "1.10",
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.180",
// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.16.v20140903" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.12" % "test",
"com.mchange" % "c3p0" % "0.9.5",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x" exclude("c3p0","c3p0")
),
play.twirl.sbt.Import.TwirlKeys.templateImports += "gitbucket.core._",
EclipseKeys.withSource := true,
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
javaOptions in Test += "-Dgitbucket.home=target/gitbucket_home_for_test",
testOptions in Test += Tests.Setup( () => new java.io.File("target/gitbucket_home_for_test").mkdir() ),
fork in Test := true,
packageOptions += Package.MainClass("JettyLauncher")
).enablePlugins(SbtTwirl)
}

View File

@@ -1,8 +1,6 @@
scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature")
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")
addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.8")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
addSbtPlugin("fi.gekkio.sbtplugins" % "sbt-jrebel-plugin" % "0.10.0")

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir="..">
<property environment="env"/>
<property name="target.dir" value="target"/>
<property name="embed.classes.dir" value="${target.dir}/embed-classes"/>
<property name="jetty.dir" value="embed-jetty"/>
<property name="scala.version" value="2.11"/>
<property name="gitbucket.version" value="${env.GITBUCKET_VERSION}"/>
<property name="jetty.version" value="8.1.16.v20140903"/>
<property name="servlet.version" value="3.0.0.v201112011016"/>
<condition property="sbt.exec" value="sbt.bat" else="sbt.sh">
<os family="windows" />
</condition>
<target name="clean">
<delete dir="${embed.classes.dir}"/>
<delete file="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="war" depends="clean">
<exec executable="${sbt.exec}" resolveexecutable="true" failonerror="true">
<arg line="clean compile test package" />
</exec>
</target>
<target name="embed" depends="war">
<mkdir dir="${embed.classes.dir}"/>
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/javax.servlet-${servlet.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-continuation-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-http-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-io-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-security-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-server-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-servlet-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-util-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-webapp-${jetty.version}.jar" />
<unzip dest="${embed.classes.dir}" src="${jetty.dir}/jetty-xml-${jetty.version}.jar" />
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${embed.classes.dir}"
update = "true"
includes="javax/**,org/**"/>
<zip destfile="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
basedir="${target.dir}/scala-${scala.version}/classes"
update = "true"
includes="JettyLauncher.class,HttpsSupportConnector.class"/>
</target>
<target name="rename" depends="embed">
<move file="${target.dir}/scala-${scala.version}/gitbucket_${scala.version}-${gitbucket.version}.war"
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="checksum" depends="rename">
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="MD5" format="MD5SUM" forceOverwrite="yes" fileext=".md5"/>
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="SHA" format="MD5SUM" forceOverwrite="yes" fileext=".sha1"/>
</target>
<target name="all" depends="checksum">
</target>
</project>

View File

@@ -5,6 +5,15 @@ cd ../
./sbt.sh clean assembly
cd release
if [[ "$GITBUCKET_VERSION" =~ -SNAPSHOT$ ]]; then
MVN_DEPLOY_PATH=mvn-snapshot
else
MVN_DEPLOY_PATH=mvn
fi
echo $MVN_DEPLOY_PATH
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
@@ -12,4 +21,4 @@ mvn deploy:deploy-file \
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/$MVN_DEPLOY_PATH/

View File

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

View File

@@ -1,15 +0,0 @@
#!/bin/sh
D="$(dirname "$0")"
D="$(cd "${D}"; pwd)"
DD="$(dirname "${D}")"
(
for f in "${D}/env.sh" "${D}/build.xml"; do
if [ ! -s "${f}" ]; then
echo >&2 "$0: Unable to access file '${f}'"
exit 1
fi
done
. "${D}/env.sh"
cd "${DD}"
ant -f "${D}/build.xml" all
)

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %*
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.9.jar" %*

2
sbt.sh
View File

@@ -1,2 +1,2 @@
#!/bin/sh
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.8.jar "$@"
java $JAVA_OPTS -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.9.jar "$@"

View File

@@ -1,5 +1,4 @@
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
@@ -30,16 +29,16 @@ public class JettyLauncher {
}
}
Server server = new Server();
Server server = new Server(port);
SelectChannelConnector connector = new SelectChannelConnector();
if(host != null) {
connector.setHost(host);
}
connector.setMaxIdleTime(1000 * 60 * 60);
connector.setSoLingerTime(-1);
connector.setPort(port);
server.addConnector(connector);
// SelectChannelConnector connector = new SelectChannelConnector();
// if(host != null) {
// connector.setHost(host);
// }
// connector.setMaxIdleTime(1000 * 60 * 60);
// connector.setSoLingerTime(-1);
// connector.setPort(port);
// server.addConnector(connector);
WebAppContext context = new WebAppContext();

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>gitbucket.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!--
<logger name="service.WebHookService" level="DEBUG" />
<logger name="servlet" level="DEBUG" />
-->
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
</configuration>

View File

@@ -6,12 +6,23 @@
</encoder>
</appender>
<!--
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>gitbucket.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
-->
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!--
<logger name="service.WebHookService" level="DEBUG" />
<logger name="servlet" level="DEBUG" />
<logger name="scala.slick.jdbc.JdbcBackend.statement" level="DEBUG" />
-->
</configuration>

View File

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

View File

@@ -0,0 +1,25 @@
DROP TABLE IF EXISTS PROTECTED_BRANCH;
CREATE TABLE PROTECTED_BRANCH(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
BRANCH VARCHAR(100) NOT NULL,
STATUS_CHECK_ADMIN BOOLEAN NOT NULL DEFAULT false
);
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH);
ALTER TABLE PROTECTED_BRANCH ADD CONSTRAINT IDX_PROTECTED_BRANCH_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
DROP TABLE IF EXISTS PROTECTED_BRANCH_REQUIRE_CONTEXT;
CREATE TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
BRANCH VARCHAR(100) NOT NULL,
CONTEXT VARCHAR(255) NOT NULL
);
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, BRANCH, CONTEXT);
ALTER TABLE PROTECTED_BRANCH_REQUIRE_CONTEXT ADD CONSTRAINT IDX_PROTECTED_BRANCH_REQUIRE_CONTEXT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, BRANCH) REFERENCES PROTECTED_BRANCH (USER_NAME, REPOSITORY_NAME, BRANCH)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,55 @@
DROP TABLE IF EXISTS WEB_HOOK_EVENT;
CREATE TABLE WEB_HOOK_EVENT(
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
URL VARCHAR(200) NOT NULL,
EVENT VARCHAR(30) NOT NULL
);
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_PK PRIMARY KEY (USER_NAME, REPOSITORY_NAME, URL, EVENT);
ALTER TABLE WEB_HOOK_EVENT ADD CONSTRAINT IDX_WEB_HOOK_EVENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME, URL) REFERENCES WEB_HOOK (USER_NAME, REPOSITORY_NAME, URL)
ON DELETE CASCADE ON UPDATE CASCADE;
CREATE TEMPORARY TABLE TMP_EVENTS (EVENT VARCHAR(30));
INSERT INTO TMP_EVENTS VALUES ('push'),('issue_comment'),('issues'),('pull_request');
INSERT INTO WEB_HOOK_EVENT (USER_NAME, REPOSITORY_NAME, URL, EVENT)
SELECT USER_NAME, REPOSITORY_NAME, URL, EVENT
FROM WEB_HOOK, TMP_EVENTS;
DROP TABLE TMP_EVENTS;
ALTER TABLE COMMIT_COMMENT ADD COLUMN ISSUE_ID INT;
CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS
SELECT
A.USER_NAME,
A.REPOSITORY_NAME,
A.ISSUE_ID,
NVL(B.COMMENT_COUNT, 0) + NVL(C.COMMENT_COUNT, 0) AS COMMENT_COUNT
FROM ISSUE A
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT
WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment')
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) B
ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID)
LEFT OUTER JOIN (
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT
GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID
) C
ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID);
UPDATE COMMIT_COMMENT C SET (ISSUE_ID) = (
SELECT MAX(P.ISSUE_ID)
FROM PULL_REQUEST P
WHERE
C.USER_NAME = P.USER_NAME AND
C.REPOSITORY_NAME = P.REPOSITORY_NAME AND
C.COMMIT_ID = P.COMMIT_ID_TO
);
ALTER TABLE COMMIT_COMMENT DROP COLUMN PULL_REQUEST;

View File

@@ -27,12 +27,10 @@ class ScalatraBootstrap extends LifeCycle {
}
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new PluginsController, "/*")
context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")

View File

@@ -0,0 +1,16 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/repos/#get-branch
* https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection
*/
case class ApiBranch(
name: String,
// commit: ApiBranchCommit,
protection: ApiBranchProtection)(repositoryName:RepositoryName) extends FieldSerializable {
def _links = Map(
"self" -> ApiPath(s"/api/v3/repos/${repositoryName.fullName}/branches/${name}"),
"html" -> ApiPath(s"/${repositoryName.fullName}/tree/${name}"))
}

View File

@@ -0,0 +1,47 @@
package gitbucket.core.api
import gitbucket.core.service.ProtectedBranchService
import org.json4s._
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
case class ApiBranchProtection(enabled: Boolean, required_status_checks: Option[ApiBranchProtection.Status]){
def status: ApiBranchProtection.Status = required_status_checks.getOrElse(ApiBranchProtection.statusNone)
}
object ApiBranchProtection{
/** form for enabling-and-disabling-branch-protection */
case class EnablingAndDisabling(protection: ApiBranchProtection)
def apply(info: ProtectedBranchService.ProtectedBranchInfo): ApiBranchProtection = ApiBranchProtection(
enabled = info.enabled,
required_status_checks = Some(Status(EnforcementLevel(info.enabled, info.includeAdministrators), info.contexts)))
val statusNone = Status(Off, Seq.empty)
case class Status(enforcement_level: EnforcementLevel, contexts: Seq[String])
sealed class EnforcementLevel(val name: String)
case object Off extends EnforcementLevel("off")
case object NonAdmins extends EnforcementLevel("non_admins")
case object Everyone extends EnforcementLevel("everyone")
object EnforcementLevel {
def apply(enabled: Boolean, includeAdministrators: Boolean): EnforcementLevel = if(enabled){
if(includeAdministrators){
Everyone
}else{
NonAdmins
}
}else{
Off
}
}
implicit val enforcementLevelSerializer = new CustomSerializer[EnforcementLevel](format => (
{
case JString("off") => Off
case JString("non_admins") => NonAdmins
case JString("everyone") => Everyone
},
{
case x: EnforcementLevel => JString(x.name)
}
))
}

View File

@@ -0,0 +1,21 @@
package gitbucket.core.api
import gitbucket.core.model.Label
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/issues/labels/
*/
case class ApiLabel(
name: String,
color: String)(repositoryName: RepositoryName){
var url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/labels/${name}")
}
object ApiLabel{
def apply(label:Label, repositoryName: RepositoryName): ApiLabel =
ApiLabel(
name = label.labelName,
color = label.color
)(repositoryName)
}

View File

@@ -0,0 +1,61 @@
package gitbucket.core.api
import gitbucket.core.util.RepositoryName
import gitbucket.core.model.CommitComment
import java.util.Date
/**
* https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
*/
case class ApiPullRequestReviewComment(
id: Int, // 29724692
// "diff_hunk": "@@ -1 +1 @@\n-# public-repo",
path: String, // "README.md",
// "position": 1,
// "original_position": 1,
commit_id: String, // "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
// "original_commit_id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
user: ApiUser,
body: String, // "Maybe you should use more emojji on this line.",
created_at: Date, // "2015-05-05T23:40:27Z",
updated_at: Date // "2015-05-05T23:40:27Z",
)(repositoryName:RepositoryName, issueId: Int) extends FieldSerializable {
// "url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692",
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/comments/${id}")
// "html_url": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692",
val html_url = ApiPath(s"/${repositoryName.fullName}/pull/${issueId}#discussion_r${id}")
// "pull_request_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1",
val pull_request_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/pulls/${issueId}")
/*
"_links": {
"self": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments/29724692"
},
"html": {
"href": "https://github.com/baxterthehacker/public-repo/pull/1#discussion_r29724692"
},
"pull_request": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1"
}
}
*/
val _links = Map(
"self" -> Map("href" -> url),
"html" -> Map("href" -> html_url),
"pull_request" -> Map("href" -> pull_request_url))
}
object ApiPullRequestReviewComment{
def apply(comment: CommitComment, commentedUser: ApiUser, repositoryName: RepositoryName, issueId: Int): ApiPullRequestReviewComment =
new ApiPullRequestReviewComment(
id = comment.commentId,
path = comment.fileName.getOrElse(""),
commit_id = comment.commitId,
user = commentedUser,
body = comment.content,
created_at = comment.registeredDate,
updated_at = comment.updatedDate
)(repositoryName, issueId)
}

View File

@@ -0,0 +1,11 @@
package gitbucket.core.api
import gitbucket.core.model.Account
case class ApiPusher(name: String, email: String)
object ApiPusher {
def apply(user: Account): ApiPusher = ApiPusher(
name = user.userName,
email = user.mailAddress)
}

View File

@@ -0,0 +1,18 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/issues/labels/#create-a-label
* api form
*/
case class CreateALabel(
name: String,
color: String
) {
def isValid: Boolean = {
name.length<=100 &&
!name.startsWith("_") &&
!name.startsWith("-") &&
color.length==6 &&
color.matches("[a-fA-F0-9+_.]+")
}
}

View File

@@ -5,40 +5,50 @@ import org.joda.time.DateTimeZone
import org.joda.time.format._
import org.json4s._
import org.json4s.jackson.Serialization
import java.util.Date
import scala.util.Try
object JsonFormat {
case class Context(baseUrl:String)
case class Context(baseUrl: String)
val parserISO = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
val jsonFormats = Serialization.formats(NoTypeHints) + new CustomSerializer[Date](format =>
(
{ case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate)
.getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case JString(s) => Try(parserISO.parseDateTime(s)).toOption.map(_.toDate).getOrElse(throw new MappingException("Can't convert " + s + " to Date")) },
{ case x: Date => JString(parserISO.print(new DateTime(x).withZone(DateTimeZone.UTC))) }
)
) + FieldSerializer[ApiUser]() + FieldSerializer[ApiPullRequest]() + FieldSerializer[ApiRepository]() +
FieldSerializer[ApiCommitListItem.Parent]() + FieldSerializer[ApiCommitListItem]() + FieldSerializer[ApiCommitListItem.Commit]() +
FieldSerializer[ApiCommitStatus]() + FieldSerializer[FieldSerializable]() + FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()
) + FieldSerializer[ApiUser]() +
FieldSerializer[ApiPullRequest]() +
FieldSerializer[ApiRepository]() +
FieldSerializer[ApiCommitListItem.Parent]() +
FieldSerializer[ApiCommitListItem]() +
FieldSerializer[ApiCommitListItem.Commit]() +
FieldSerializer[ApiCommitStatus]() +
FieldSerializer[FieldSerializable]() +
FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]() +
FieldSerializer[ApiIssue]() +
FieldSerializer[ApiComment]() +
FieldSerializer[ApiLabel]() +
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)
}
)
(
{
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)
}
)
)
/**
* convert object to json string
*/
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
}

View File

@@ -12,7 +12,7 @@ import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
@@ -90,8 +90,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class ForkRepositoryForm(owner: String, name: String)
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), repository, uniqueRepository))),
"owner" -> trim(label("Owner" , text(required, maxlength(100), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(100), repository, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
@@ -133,7 +133,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val members = getGroupMembers(account.userName)
gitbucket.core.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
getVisibleRepositories(context.loginAccount, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
}
@@ -212,6 +212,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
// removeUserRelatedData(userName)
removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true))
}
@@ -365,7 +366,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
*/
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
if(getRepository(form.owner, form.name).isEmpty){
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
}
@@ -384,9 +385,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name, context.baseUrl).isEmpty){
if(getRepository(owner, data.name).isEmpty){
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name, context.baseUrl).get
val repository = getRepository(owner, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
@@ -408,9 +409,9 @@ trait AccountControllerBase extends AccountManagementControllerBase {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){
if(getRepository(groupName, data.name).isEmpty){
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name, context.baseUrl).get
val repository = getRepository(groupName, data.name).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
ApiError(
@@ -446,7 +447,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
if(getRepository(accountName, repository.name).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${accountName}/${repository.name}")

View File

@@ -8,7 +8,7 @@ import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.json4s._
import org.scalatra._
@@ -28,7 +28,11 @@ abstract class ControllerBase extends ScalatraFilter
with ClientSideValidationFormSupport with JacksonJsonSupport with I18nSupport with FlashMapSupport with Validations
with SystemSettingsService {
implicit val jsonFormats = DefaultFormats
implicit val jsonFormats = gitbucket.core.api.JsonFormat.jsonFormats
before("/api/v3/*") {
contentType = formats("json")
}
// TODO Scala 2.11
// // Don't set content type via Accept header.
@@ -176,7 +180,6 @@ abstract class ControllerBase extends ScalatraFilter
* Context object for the current request.
*/
case class Context(settings: SystemSettingsService.SystemSettings, loginAccount: Option[Account], request: HttpServletRequest){
val path = settings.baseUrl.getOrElse(request.getContextPath)
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request)

View File

@@ -94,7 +94,7 @@ trait DashboardControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val userRepos = getUserRepositories(userName, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request)
html.issues(

View File

@@ -2,35 +2,46 @@ package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.helper.xml
import gitbucket.core.html
import gitbucket.core.model.Account
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService, RepositorySearchService, IssuesService}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator, ReferrerAuthenticator, StringUtil}
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
with RepositoryService with ActivityService with AccountService with RepositorySearchService with IssuesService
with UsersAuthenticator with ReferrerAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
self: RepositoryService with ActivityService with AccountService with RepositorySearchService
with UsersAuthenticator with ReferrerAuthenticator =>
case class SignInForm(userName: String, password: String)
val form = mapping(
val signinForm = mapping(
"userName" -> trim(label("Username", text(required))),
"password" -> trim(label("Password", text(required)))
)(SignInForm.apply)
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
get("/"){
val loginAccount = context.loginAccount
if(loginAccount.isEmpty) {
html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
gitbucket.core.html.index(getRecentActivities(),
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
} else {
val loginUserName = loginAccount.get.userName
@@ -39,9 +50,9 @@ trait IndexControllerBase extends ControllerBase {
visibleOwnerSet ++= loginUserGroups
html.index(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, context.baseUrl, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, context.baseUrl, withoutPhysicalInfo = true) }.getOrElse(Nil)
gitbucket.core.html.index(getRecentActivitiesByOwners(visibleOwnerSet),
getVisibleRepositories(loginAccount, withoutPhysicalInfo = true),
loginAccount.map{ account => getUserRepositories(account.userName, withoutPhysicalInfo = true) }.getOrElse(Nil)
)
}
}
@@ -51,10 +62,10 @@ trait IndexControllerBase extends ControllerBase {
if(redirect.isDefined && redirect.get.startsWith("/")){
flash += Keys.Flash.Redirect -> redirect.get
}
html.signin()
gitbucket.core.html.signin()
}
post("/signin", form){ form =>
post("/signin", signinForm){ form =>
authenticate(context.settings, form.userName, form.password) match {
case Some(account) => signin(account)
case None => redirect("/signin")
@@ -104,7 +115,7 @@ trait IndexControllerBase extends ControllerBase {
})
/**
* JSON APU for checking user existence.
* JSON API for checking user existence.
*/
post("/_user/existence")(usersOnly {
getAccountByUserName(params("userName")).isDefined
@@ -119,4 +130,33 @@ trait IndexControllerBase extends ControllerBase {
// this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
}
// TODO Move to RepositoryViwerController?
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
// TODO Move to RepositoryViwerController?
get("/:owner/:repository/search")(referrersOnly { repository =>
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => gitbucket.core.search.html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => gitbucket.core.search.html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
}
})
}

View File

@@ -11,7 +11,7 @@ import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.Ok
@@ -231,10 +231,20 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
Map(
"title" -> x.title,
"content" -> Markdown.toHtml(
markdown = x.content getOrElse "No description given.",
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.openedUserName)
)
)
)
}
} else Unauthorized
} getOrElse NotFound
@@ -249,9 +259,19 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
Map(
"content" -> view.Markdown.toHtml(
markdown = x.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
enableTaskList = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
)
)
)
}
} else Unauthorized
} getOrElse NotFound

View File

@@ -1,12 +1,13 @@
package gitbucket.core.controller
import gitbucket.core.api.{ApiError, CreateALabel, ApiLabel, JsonFormat}
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.{LockUtil, RepositoryName, ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
import org.scalatra.{NoContent, UnprocessableEntity, Created, Ok}
class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService
@@ -19,7 +20,7 @@ trait LabelsControllerBase extends ControllerBase {
case class LabelForm(labelName: String, color: String)
val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"labelName" -> trim(label("Label name", text(required, labelName, uniqueLabelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
@@ -31,6 +32,26 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* List all labels for this repository
* https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
*/
get("/api/v3/repos/:owner/:repository/labels")(referrersOnly { repository =>
JsonFormat(getLabels(repository.owner, repository.name).map { label =>
ApiLabel(label, RepositoryName(repository))
})
})
/**
* Get a single label
* https://developer.github.com/v3/issues/labels/#get-a-single-label
*/
get("/api/v3/repos/:owner/:repository/labels/:labelName")(referrersOnly { repository =>
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
JsonFormat(ApiLabel(label, RepositoryName(repository)))
} getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
html.edit(None, repository)
})
@@ -45,6 +66,31 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* Create a label
* https://developer.github.com/v3/issues/labels/#create-a-label
*/
post("/api/v3/repos/:owner/:repository/labels")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
val labelId = createLabel(repository.owner, repository.name, data.name, data.color)
getLabel(repository.owner, repository.name, labelId).map { label =>
Created(JsonFormat(ApiLabel(label, RepositoryName(repository))))
} getOrElse NotFound()
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")
))
}
}
}) getOrElse NotFound()
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository)
@@ -61,11 +107,50 @@ trait LabelsControllerBase extends ControllerBase {
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* Update a label
* https://developer.github.com/v3/issues/labels/#update-a-label
*/
patch("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
(for{
data <- extractFromJsonBody[CreateALabel] if data.isValid
} yield {
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
if (getLabel(repository.owner, repository.name, data.name).isEmpty) {
updateLabel(repository.owner, repository.name, label.labelId, data.name, data.color)
JsonFormat(ApiLabel(
getLabel(repository.owner, repository.name, label.labelId).get,
RepositoryName(repository)))
} else {
// TODO ApiError should support errors field to enhance compatibility of GitHub API
UnprocessableEntity(ApiError(
"Validation Failed",
Some("https://developer.github.com/v3/issues/labels/#create-a-label")))
}
} getOrElse NotFound()
}
}) getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok()
})
/**
* Delete a label
* https://developer.github.com/v3/issues/labels/#delete-a-label
*/
delete("/api/v3/repos/:owner/:repository/labels/:labelName")(collaboratorsOnly { repository =>
LockUtil.lock(RepositoryName(repository).fullName) {
getLabel(repository.owner, repository.name, params("labelName")).map { label =>
deleteLabel(repository.owner, repository.name, label.labelId)
NoContent()
} getOrElse NotFound()
}
})
/**
* Constraint for the identifier such as user name, repository name or page name.
*/
@@ -80,4 +165,12 @@ trait LabelsControllerBase extends ControllerBase {
}
}
private def uniqueLabelName: Constraint = new Constraint(){
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
val owner = params("owner")
val repository = params("repository")
getLabel(owner, repository, value).map(_ => "Name has already been taken.")
}
}
}

View File

@@ -4,7 +4,7 @@ import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService

View File

@@ -1,11 +0,0 @@
package gitbucket.core.controller
import gitbucket.core.admin.plugins.html
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util.AdminAuthenticator
class PluginsController extends ControllerBase with AdminAuthenticator {
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
})
}

View File

@@ -1,7 +1,7 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
import gitbucket.core.model.{Account, CommitStatus, CommitState, Repository, PullRequest, Issue, WebHook}
import gitbucket.core.pulls.html
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
@@ -16,7 +16,7 @@ import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.helpers
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent
import org.slf4j.LoggerFactory
@@ -27,13 +27,13 @@ import scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService
with CommitStatusService with MergeService with ProtectedBranchService
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService =>
with CommitStatusService with MergeService with ProtectedBranchService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -119,7 +119,8 @@ trait PullRequestsControllerBase extends ControllerBase {
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
repository,
flash.toMap.map(f => f._1 -> f._2.toString))
}
}
} getOrElse NotFound
@@ -136,7 +137,7 @@ trait PullRequestsControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
JsonFormat(ApiPullRequest(
issue,
@@ -166,24 +167,36 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { repository =>
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
val statuses = getCommitStatues(owner, name, pullreq.commitIdTo)
val hasConfrict = LockUtil.lock(s"${owner}/${name}"){
val hasConflict = LockUtil.lock(s"${owner}/${name}"){
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
val hasMergePermission = hasWritePermission(owner, name, context.loginAccount)
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.branch)
val mergeStatus = PullRequestService.MergeStatus(
hasConflict = hasConflict,
commitStatues = getCommitStatues(owner, name, pullreq.commitIdTo),
branchProtection = branchProtection,
branchIsOutOfDate = JGitUtil.getShaByRef(owner, name, pullreq.branch) != Some(pullreq.commitIdFrom),
needStatusCheck = context.loginAccount.map{ u =>
branchProtection.needStatusCheck(u.userName)
}.getOrElse(true),
hasUpdatePermission = hasWritePermission(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount) &&
context.loginAccount.map{ u =>
!getProtectedBranchInfo(pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.requestBranch).needStatusCheck(u.userName)
}.getOrElse(false),
hasMergePermission = hasMergePermission,
commitIdTo = pullreq.commitIdTo)
html.mergeguide(
hasConfrict,
hasProblem,
mergeStatus,
issue,
pullreq,
statuses,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName).get)
}
} getOrElse NotFound
})
@@ -203,6 +216,75 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/update_branch")(referrersOnly { baseRepository =>
(for {
issueId <- params("id").toIntOpt
loginAccount <- context.loginAccount
(issue, pullreq) <- getPullRequest(baseRepository.owner, baseRepository.name, issueId)
owner = pullreq.requestUserName
name = pullreq.requestRepositoryName
if hasWritePermission(owner, name, context.loginAccount)
} yield {
val branchProtection = getProtectedBranchInfo(owner, name, pullreq.requestBranch)
if(branchProtection.needStatusCheck(loginAccount.userName)){
flash += "error" -> s"branch ${pullreq.requestBranch} is protected need status check."
} else {
val repository = getRepository(owner, name).get
LockUtil.lock(s"${owner}/${name}"){
val alias = if(pullreq.repositoryName == pullreq.requestRepositoryName && pullreq.userName == pullreq.requestUserName){
pullreq.branch
}else{
s"${pullreq.userName}:${pullreq.branch}"
}
val existIds = using(Git.open(Directory.getRepositoryDir(owner, name))) { git => JGitUtil.getAllCommitIds(git) }.toSet
pullRemote(owner, name, pullreq.requestBranch, pullreq.userName, pullreq.repositoryName, pullreq.branch, loginAccount,
"Merge branch '${alias}' into ${pullreq.requestBranch}") match {
case None => // conflict
flash += "error" -> s"Can't automatic merging branch '${alias}' into ${pullreq.requestBranch}."
case Some(oldId) =>
// update pull request
updatePullRequests(owner, name, pullreq.requestBranch)
using(Git.open(Directory.getRepositoryDir(owner, name))) { git =>
// after update branch
val newCommitId = git.getRepository.resolve(s"refs/heads/${pullreq.requestBranch}")
val commits = git.log.addRange(oldId, newCommitId).call.iterator.asScala.map(c => new JGitUtil.CommitInfo(c)).toList
commits.foreach{ commit =>
if(!existIds.contains(commit.id)){
createIssueComment(owner, name, commit)
}
}
// record activity
recordPushActivity(owner, name, loginAccount.userName, pullreq.branch, commits)
// close issue by commit message
if(pullreq.requestBranch == repository.repository.defaultBranch){
commits.map{ commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
}
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, pullreq.requestBranch, baseUrl, loginAccount)
callWebHookOf(owner, name, WebHook.Push) {
for {
ownerAccount <- getAccountByUserName(owner)
} yield {
WebHookService.WebHookPushPayload(git, loginAccount, pullreq.requestBranch, repository, commits, ownerAccount, oldId = oldId, newId = newCommitId)
}
}
}
flash += "info" -> s"Merge branch '${alias}' into ${pullreq.requestBranch}"
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
}
}) getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
@@ -228,7 +310,7 @@ trait PullRequestsControllerBase extends ControllerBase {
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
val defaultBranch = getRepository(owner, name).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
@@ -261,7 +343,7 @@ trait PullRequestsControllerBase extends ControllerBase {
val headBranch:Option[String] = params.get("head")
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
getRepository(originUserName, originRepositoryName).map { originRepository =>
using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
@@ -302,12 +384,12 @@ trait PullRequestsControllerBase extends ControllerBase {
forkedRepository.repository.originRepositoryName
} else {
// Sibling repository
getUserRepositories(originOwner, context.baseUrl).find { x =>
getUserRepositories(originOwner).find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}.map(_.repository.repositoryName)
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
originRepository <- getRepository(originOwner, originRepositoryName)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -375,7 +457,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
originRepository <- getRepository(originOwner, originRepositoryName)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
@@ -528,4 +610,15 @@ trait PullRequestsControllerBase extends ControllerBase {
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
// TODO: same as gitbucket.core.servlet.CommitLogHook ...
private def createIssueComment(owner: String, repository: String, commit: CommitInfo) = {
StringUtil.extractIssueId(commit.fullMessage).foreach { issueId =>
if(getIssue(owner, repository, issueId).isDefined){
getAccountByMailAddress(commit.committerEmailAddress).foreach { account =>
createComment(owner, repository, account.userName, issueId.toInt, commit.fullMessage + " " + commit.id, "commit")
}
}
}
}
}

View File

@@ -2,14 +2,14 @@ package gitbucket.core.controller
import gitbucket.core.settings.html
import gitbucket.core.model.WebHook
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService, ProtectedBranchService, CommitStatusService}
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
@@ -18,23 +18,29 @@ import org.eclipse.jgit.lib.ObjectId
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
with RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
with OwnerAuthenticator with UsersAuthenticator
trait RepositorySettingsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with WebHookService
self: RepositoryService with AccountService with WebHookService with ProtectedBranchService with CommitStatusService
with OwnerAuthenticator with UsersAuthenticator =>
// for repository options
case class OptionsForm(repositoryName: String, description: Option[String], defaultBranch: String, isPrivate: Boolean)
case class OptionsForm(repositoryName: String, description: Option[String], isPrivate: Boolean)
val optionsForm = mapping(
"repositoryName" -> trim(label("Description" , text(required, maxlength(40), identifier, renameRepositoryName))),
"repositoryName" -> trim(label("Repository Name", text(required, maxlength(40), identifier, renameRepositoryName))),
"description" -> trim(label("Description" , optional(text()))),
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100)))),
"isPrivate" -> trim(label("Repository Type", boolean()))
)(OptionsForm.apply)
// for default branch
case class DefaultBranchForm(defaultBranch: String)
val defaultBranchForm = mapping(
"defaultBranch" -> trim(label("Default Branch" , text(required, maxlength(100))))
)(DefaultBranchForm.apply)
// for collaborator addition
case class CollaboratorForm(userName: String)
@@ -43,10 +49,11 @@ trait RepositorySettingsControllerBase extends ControllerBase {
)(CollaboratorForm.apply)
// for web hook url addition
case class WebHookForm(url: String)
case class WebHookForm(url: String, events: Set[WebHook.Event])
val webHookForm = mapping(
"url" -> trim(label("url", text(required, webHook)))
def webHookForm(update:Boolean) = mapping(
"url" -> trim(label("url", text(required, webHook(update)))),
"events" -> webhookEvents
)(WebHookForm.apply)
// for transfer ownership
@@ -74,12 +81,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
@@ -97,14 +102,61 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
})
/** branch settings */
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) =>
if(repository.branchList.find(_ == form.defaultBranch).isEmpty){
redirect(s"/${repository.owner}/${repository.name}/settings/options")
} else {
saveRepositoryDefaultBranch(repository.owner, repository.name, form.defaultBranch)
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + form.defaultBranch)
}
flash += "info" -> "Repository default branch has been updated."
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
}
})
/** Branch protection for branch */
get("/:owner/:repository/settings/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
val branch = params("branch")
if(repository.branchList.find(_ == branch).isEmpty){
redirect(s"/${repository.owner}/${repository.name}/settings/branches")
} else {
val protection = ApiBranchProtection(getProtectedBranchInfo(repository.owner, repository.name, branch))
val lastWeeks = getRecentStatuesContexts(repository.owner, repository.name, org.joda.time.LocalDateTime.now.minusWeeks(1).toDate).toSet
val knownContexts = (lastWeeks ++ protection.status.contexts).toSeq.sortBy(identity)
html.branchprotection(repository, branch, protection, knownContexts, flash.get("info"))
}
})
/** https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection */
patch("/api/v3/repos/:owner/:repo/branches/:branch")(ownerOnly { repository =>
import gitbucket.core.api._
(for{
branch <- params.get("branch") if repository.branchList.find(_ == branch).isDefined
protection <- extractFromJsonBody[ApiBranchProtection.EnablingAndDisabling].map(_.protection)
} yield {
if(protection.enabled){
enableBranchProtection(repository.owner, repository.name, branch, protection.status.enforcement_level == ApiBranchProtection.Everyone, protection.status.contexts)
} else {
disableBranchProtection(repository.owner, repository.name, branch)
}
JsonFormat(ApiBranch(branch, protection)(RepositoryName(repository)))
}) getOrElse NotFound
})
/**
* Display the Collaborators page.
*/
@@ -139,14 +191,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
html.hooks(getWebHooks(repository.owner, repository.name), repository, flash.get("info"))
})
/**
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/new")(ownerOnly { repository =>
val webhook = WebHook(repository.owner, repository.name, "")
html.edithooks(webhook, Set(WebHook.Push), repository, flash.get("info"), true)
})
/**
* Add the web hook URL.
*/
post("/:owner/:repository/settings/hooks/add", webHookForm)(ownerOnly { (form, repository) =>
addWebHookURL(repository.owner, repository.name, form.url)
post("/:owner/:repository/settings/hooks/new", webHookForm(false))(ownerOnly { (form, repository) =>
addWebHook(repository.owner, repository.name, form.url, form.events)
flash += "info" -> s"Webhook ${form.url} created"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
@@ -154,32 +215,87 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Delete the web hook URL.
*/
get("/:owner/:repository/settings/hooks/delete")(ownerOnly { repository =>
deleteWebHookURL(repository.owner, repository.name, params("url"))
deleteWebHook(repository.owner, repository.name, params("url"))
flash += "info" -> s"Webhook ${params("url")} deleted"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
/**
* Send the test request to registered web hook URLs.
*/
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
ajaxPost("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
def _headers(h: Array[org.apache.http.Header]): Array[Array[String]] = h.map { h => Array(h.getName, h.getValue) }
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(4)
.call.iterator.asScala.map(new CommitInfo(_)).toList
import scala.concurrent.duration._
import scala.concurrent._
import scala.util.control.NonFatal
import org.apache.http.util.EntityUtils
import scala.concurrent.ExecutionContext.Implicits.global
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, (if(commits.isEmpty){Nil}else{commits.tail}), ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()))
val url = params("url")
val dummyWebHookInfo = WebHook(repository.owner, repository.name, url)
val dummyPayload = {
val ownerAccount = getAccountByUserName(repository.owner).get
val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(4)
.call.iterator.asScala.map(new CommitInfo(_)).toList
val pushedCommit = commits.drop(1)
WebHookPushPayload(
git = git,
sender = ownerAccount,
refName = "refs/heads/" + repository.repository.defaultBranch,
repositoryInfo = repository,
commits = pushedCommit,
repositoryOwner = ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId())
)
}
flash += "url" -> form.url
flash += "info" -> "Test payload deployed!"
val (webHook, json, reqFuture, resFuture) = callWebHook(WebHook.Push, List(dummyWebHookInfo), dummyPayload).head
val toErrorMap: PartialFunction[Throwable, Map[String,String]] = {
case e: java.net.UnknownHostException => Map("error"-> ("Unknown host " + e.getMessage))
case e: java.lang.IllegalArgumentException => Map("error"-> ("invalid url"))
case e: org.apache.http.client.ClientProtocolException => Map("error"-> ("invalid url"))
case NonFatal(e) => Map("error"-> (e.getClass + " "+ e.getMessage))
}
contentType = formats("json")
org.json4s.jackson.Serialization.write(Map(
"url" -> url,
"request" -> Await.result(reqFuture.map(req => Map(
"headers" -> _headers(req.getAllHeaders),
"payload" -> json
)).recover(toErrorMap), 20 seconds),
"responce" -> Await.result(resFuture.map(res => Map(
"status" -> res.getStatusLine(),
"body" -> EntityUtils.toString(res.getEntity()),
"headers" -> _headers(res.getAllHeaders())
)).recover(toErrorMap), 20 seconds)
))
}
})
/**
* Display the web hook edit page.
*/
get("/:owner/:repository/settings/hooks/edit")(ownerOnly { repository =>
getWebHook(repository.owner, repository.name, params("url")).map{ case (webhook, events) =>
html.edithooks(webhook, events, repository, flash.get("info"), false)
} getOrElse NotFound
})
/**
* Update web hook settings.
*/
post("/:owner/:repository/settings/hooks/edit", webHookForm(true))(ownerOnly { (form, repository) =>
updateWebHook(repository.owner, repository.name, form.url, form.events)
flash += "info" -> s"webhook ${form.url} updated"
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
})
@@ -229,9 +345,30 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/**
* Provides duplication check for web hook url.
*/
private def webHook: Constraint = new Constraint(){
private def webHook(needExists: Boolean): Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
getWebHookURLs(params("owner"), params("repository")).map(_.url).find(_ == value).map(_ => "URL had been registered already.")
if(getWebHook(params("owner"), params("repository"), value).isDefined != needExists){
Some(if(needExists){
"URL had not been registered yet."
} else {
"URL had been registered already."
})
} else {
None
}
}
private def webhookEvents = new ValueType[Set[WebHook.Event]]{
def convert(name: String, params: Map[String, String], messages: Messages): Set[WebHook.Event] = {
WebHook.Event.values.flatMap { t =>
params.get(name + "." + t.name).map(_ => t)
}.toSet
}
def validate(name: String, params: Map[String, String], messages: Messages): Seq[(String, String)] = if(convert(name,params,messages).isEmpty){
Seq(name -> messages("error.required").format(name))
} else {
Nil
}
}
/**

View File

@@ -1,5 +1,7 @@
package gitbucket.core.controller
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import gitbucket.core.api._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html
@@ -11,13 +13,12 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.model.{Account, CommitState}
import gitbucket.core.service.CommitStatusService
import gitbucket.core.model.{Account, CommitState, WebHook}
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
@@ -32,7 +33,7 @@ import org.scalatra._
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService
/**
* The repository viewer.
@@ -40,7 +41,7 @@ class RepositoryViewerController extends RepositoryViewerControllerBase
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService with CommitsService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService =>
with WebHookPullRequestService with WebHookPullRequestReviewCommentService with ProtectedBranchService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -102,11 +103,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*/
post("/:owner/:repository/_preview")(referrersOnly { repository =>
contentType = "text/html"
helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
helpers.markdown(
markdown = params("content"),
repository = repository,
enableWikiLink = params("enableWikiLink").toBoolean,
enableRefsLink = params("enableRefsLink").toBoolean,
enableLineBreaks = params("enableLineBreaks").toBoolean,
enableTaskList = params("enableTaskList").toBoolean,
enableAnchor = false,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount)
)
})
/**
@@ -177,7 +183,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
val listStatusesRoute = get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
@@ -188,6 +194,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* legacy route
*/
get("/api/v3/repos/:owner/:repo/statuses/:ref"){
listStatusesRoute.action()
}
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*
@@ -206,12 +221,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")),
protectedBranch)
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
val protectedBranch = getProtectedBranchInfo(repository.owner, repository.name, branch).needStatusCheck(context.loginAccount.get.userName)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
@@ -219,7 +237,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
JGitUtil.getContentInfo(git, path, objectId),
protectedBranch)
} getOrElse NotFound
}
})
@@ -282,6 +301,21 @@ trait RepositoryViewerControllerBase extends ControllerBase {
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
get("/:owner/:repository/raw/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).flatMap { objectId =>
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
contentType = FileUtil.getMimeType(path)
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.outputStream)
()
}
} getOrElse NotFound
}
})
/**
* Displays the file content of the specified branch or commit.
*/
@@ -292,12 +326,11 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
// Download (This route is left for backword compatibility)
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
//RawData("application/octet-stream", bytes)
contentType = "application/octet-stream"
contentType = FileUtil.getMimeType(path)
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.getOutputStream)
loader.copyTo(response.outputStream)
()
} getOrElse NotFound
} else {
@@ -370,7 +403,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
@@ -395,13 +428,15 @@ trait RepositoryViewerControllerBase extends ControllerBase {
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId)
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)
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)
}
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
helper.html.commitcomment(comment, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
@@ -413,8 +448,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
Map(
"content" -> view.Markdown.toHtml(
markdown = x.content,
repository = repository,
enableWikiLink = false,
enableRefsLink = true,
enableAnchor = true,
enableLineBreaks = true,
hasWritePermission = isEditable(x.userName, x.repositoryName, x.commentedUserName)
)
))
}
} else Unauthorized
@@ -446,6 +489,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
val protectedBranches = getProtectedBranchList(repository.owner, repository.name).toSet
val branches = JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
@@ -453,7 +497,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
origin = repository.repository.originUserName.isEmpty
)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
.map(br => (br, getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId), protectedBranches.contains(br.name)))
.reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
@@ -516,8 +560,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
context.baseUrl),
repository.repository.originRepositoryName.getOrElse(repository.name)),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
@@ -590,7 +633,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
!file.isDirectory && readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
@@ -644,7 +687,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
inserter.close()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
@@ -667,7 +710,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
callWebHookOf(repository.owner, repository.name, "push") {
callWebHookOf(repository.owner, repository.name, WebHook.Push) {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
@@ -715,11 +758,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
.setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream)
.call()
Unit
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
override protected def renderUncaughtException(e: Throwable)(implicit request: HttpServletRequest, response: HttpServletResponse): Unit = {
e.printStackTrace()
}
}

View File

@@ -1,51 +0,0 @@
package gitbucket.core.controller
import gitbucket.core.search.html
import gitbucket.core.service._
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
import ControlUtil._
import Implicits._
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
with RepositoryService with AccountService with ActivityService with RepositorySearchService with IssuesService with ReferrerAuthenticator
trait SearchControllerBase extends ControllerBase { self: RepositoryService
with ActivityService with RepositorySearchService with ReferrerAuthenticator =>
val searchForm = mapping(
"query" -> trim(text(required)),
"owner" -> trim(text(required)),
"repository" -> trim(text(required))
)(SearchForm.apply)
case class SearchForm(query: String, owner: String, repository: String)
post("/search", searchForm){ form =>
redirect(s"/${form.owner}/${form.repository}/search?q=${StringUtil.urlEncode(form.query)}")
}
get("/:owner/:repository/search")(referrersOnly { repository =>
defining(params("q").trim, params.getOrElse("type", "code")){ case (query, target) =>
val page = try {
val i = params.getOrElse("page", "1").toInt
if(i <= 0) 1 else i
} catch {
case e: NumberFormatException => 1
}
target.toLowerCase match {
case "issue" => html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)
}
}
})
}

View File

@@ -4,8 +4,9 @@ import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util.AdminAuthenticator
import gitbucket.core.ssh.SshServer
import gitbucket.core.plugin.PluginRegistry
import SystemSettingsService._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
@@ -23,6 +24,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"ssh" -> trim(label("SSH access", boolean())),
"sshHost" -> trim(label("SSH host", optional(text()))),
"sshPort" -> trim(label("SSH port", optional(number()))),
"useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
@@ -50,9 +52,14 @@ trait SystemSettingsControllerBase extends ControllerBase {
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
Vector(
if(settings.ssh && settings.baseUrl.isEmpty){
Some("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else None,
if(settings.ssh && settings.sshHost.isEmpty){
Some("sshHost" -> "SSH host is required if SSH access is enabled.")
} else None
).flatten
}
private val pluginForm = mapping(
@@ -68,20 +75,21 @@ trait SystemSettingsControllerBase extends ControllerBase {
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
if (form.sshAddress != context.settings.sshAddress) {
SshServer.stop()
for {
sshAddress <- form.sshAddress
baseUrl <- form.baseUrl
}
SshServer.start(sshAddress, baseUrl)
}
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})
get("/admin/plugins")(adminOnly {
html.plugins(PluginRegistry().getPlugins())
})
}

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.scalatra.i18n.Messages
import org.apache.commons.io.FileUtils
@@ -100,12 +100,12 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
if(form.isRemoved){
// Remove repositories
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
deleteRepository(userName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
}
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
}

View File

@@ -7,7 +7,7 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import io.github.gitbucket.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.scalatra.i18n.Messages
@@ -38,7 +38,9 @@ trait WikiControllerBase extends ControllerBase {
get("/:owner/:repository/wiki")(referrersOnly { repository =>
getWikiPage(repository.owner, repository.name, "Home").map { page =>
html.page("Home", page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer"))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/Home/_edit")
})
@@ -47,7 +49,9 @@ trait WikiControllerBase extends ControllerBase {
getWikiPage(repository.owner, repository.name, pageName).map { page =>
html.page(pageName, page, getWikiPageList(repository.owner, repository.name),
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
repository, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getWikiPage(repository.owner, repository.name, "_Sidebar"),
getWikiPage(repository.owner, repository.name, "_Footer"))
} getOrElse redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(pageName)}/_edit")
})
@@ -124,7 +128,11 @@ trait WikiControllerBase extends ControllerBase {
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
}
})
@@ -140,7 +148,11 @@ trait WikiControllerBase extends ControllerBase {
updateLastActivityDate(repository.owner, repository.name)
recordCreateWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName)
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
if(notReservedPageName(form.pageName)) {
redirect(s"/${repository.owner}/${repository.name}/wiki/${StringUtil.urlEncode(form.pageName)}")
} else {
redirect(s"/${repository.owner}/${repository.name}/wiki")
}
}
})
@@ -186,13 +198,15 @@ trait WikiControllerBase extends ControllerBase {
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.exists("\\/:*?\"<>|".contains(_))){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
} else if(notReservedPageName(value) && (value.startsWith("_") || value.startsWith("-"))){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
private def notReservedPageName(value: String) = ! (Array[String]("_Sidebar","_Footer") contains value)
private def conflictForNew: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
targetWikiPage.map { _ =>

View File

@@ -26,12 +26,16 @@ protected[model] trait TemplateComponent { self: Profile =>
trait LabelTemplate extends BasicTemplate { self: Table[_] =>
val labelId = column[Int]("LABEL_ID")
val labelName = column[String]("LABEL_NAME")
def byLabel(owner: String, repository: String, labelId: Int) =
byRepository(owner, repository) && (this.labelId === labelId.bind)
def byLabel(userName: Column[String], repositoryName: Column[String], labelId: Column[Int]) =
byRepository(userName, repositoryName) && (this.labelId === labelId)
def byLabel(owner: String, repository: String, labelName: String) =
byRepository(owner, repository) && (this.labelName === labelName.bind)
}
trait MilestoneTemplate extends BasicTemplate { self: Table[_] =>
@@ -54,4 +58,9 @@ protected[model] trait TemplateComponent { self: Profile =>
byRepository(userName, repositoryName) && (this.commitId === commitId)
}
trait BranchTemplate extends BasicTemplate{ self: Table[_] =>
val branch = column[String]("BRANCH")
def byBranch(owner: String, repository: String, branchName: String) = byRepository(owner, repository) && (branch === branchName.bind)
def byBranch(owner: Column[String], repository: Column[String], branchName: Column[String]) = byRepository(owner, repository) && (this.branch === branchName)
}
}

View File

@@ -55,8 +55,8 @@ trait CommitCommentComponent extends TemplateComponent { self: Profile =>
val newLine = column[Option[Int]]("NEW_LINE_NUMBER")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
val pullRequest = column[Boolean]("PULL_REQUEST")
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, pullRequest) <> (CommitComment.tupled, CommitComment.unapply)
val issueId = column[Option[Int]]("ISSUE_ID")
def * = (userName, repositoryName, commitId, commentId, commentedUserName, content, fileName, oldLine, newLine, registeredDate, updatedDate, issueId) <> (CommitComment.tupled, CommitComment.unapply)
def byPrimaryKey(commentId: Int) = this.commentId === commentId.bind
}
@@ -74,5 +74,5 @@ case class CommitComment(
newLine: Option[Int],
registeredDate: java.util.Date,
updatedDate: java.util.Date,
pullRequest: Boolean
issueId: Option[Int]
) extends Comment

View File

@@ -19,7 +19,7 @@ trait CommitStatusComponent extends TemplateComponent { self: Profile =>
val creator = column[String]("CREATOR")
val registeredDate = column[java.util.Date]("REGISTERED_DATE")
val updatedDate = column[java.util.Date]("UPDATED_DATE")
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> (CommitStatus.tupled, CommitStatus.unapply)
def * = (commitStatusId, userName, repositoryName, commitId, context, state, targetUrl, description, creator, registeredDate, updatedDate) <> ((CommitStatus.apply _).tupled, CommitStatus.unapply)
def byPrimaryKey(id: Int) = commitStatusId === id.bind
}
}
@@ -38,7 +38,20 @@ case class CommitStatus(
registeredDate: java.util.Date,
updatedDate: java.util.Date
)
object CommitStatus {
def pending(owner: String, repository: String, context: String) = CommitStatus(
commitStatusId = 0,
userName = owner,
repositoryName = repository,
commitId = "",
context = context,
state = CommitState.PENDING,
targetUrl = None,
description = Some("Waiting for status to be reported"),
creator = "",
registeredDate = new java.util.Date(),
updatedDate = new java.util.Date())
}
sealed abstract class CommitState(val name: String)

View File

@@ -7,7 +7,7 @@ trait LabelComponent extends TemplateComponent { self: Profile =>
class Labels(tag: Tag) extends Table[Label](tag, "LABEL") with LabelTemplate {
override val labelId = column[Int]("LABEL_ID", O AutoInc)
val labelName = column[String]("LABEL_NAME")
override val labelName = column[String]("LABEL_NAME")
val color = column[String]("COLOR")
def * = (userName, repositoryName, labelId, labelName, color) <> (Label.tupled, Label.unapply)

View File

@@ -48,6 +48,8 @@ trait CoreProfile extends ProfileProvider with Profile
with RepositoryComponent
with SshKeyComponent
with WebHookComponent
with WebHookEventComponent
with PluginComponent
with ProtectedBranchComponent
object Profile extends CoreProfile

View File

@@ -0,0 +1,37 @@
package gitbucket.core.model
import scala.slick.lifted.MappedTo
import scala.slick.jdbc._
trait ProtectedBranchComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
lazy val ProtectedBranches = TableQuery[ProtectedBranches]
class ProtectedBranches(tag: Tag) extends Table[ProtectedBranch](tag, "PROTECTED_BRANCH") with BranchTemplate {
val statusCheckAdmin = column[Boolean]("STATUS_CHECK_ADMIN")
def * = (userName, repositoryName, branch, statusCheckAdmin) <> (ProtectedBranch.tupled, ProtectedBranch.unapply)
def byPrimaryKey(userName: String, repositoryName: String, branch: String) = byBranch(userName, repositoryName, branch)
def byPrimaryKey(userName: Column[String], repositoryName: Column[String], branch: Column[String]) = byBranch(userName, repositoryName, branch)
}
lazy val ProtectedBranchContexts = TableQuery[ProtectedBranchContexts]
class ProtectedBranchContexts(tag: Tag) extends Table[ProtectedBranchContext](tag, "PROTECTED_BRANCH_REQUIRE_CONTEXT") with BranchTemplate {
val context = column[String]("CONTEXT")
def * = (userName, repositoryName, branch, context) <> (ProtectedBranchContext.tupled, ProtectedBranchContext.unapply)
}
}
case class ProtectedBranch(
userName: String,
repositoryName: String,
branch: String,
statusCheckAdmin: Boolean)
case class ProtectedBranchContext(
userName: String,
repositoryName: String,
branch: String,
context: String)

View File

@@ -7,7 +7,7 @@ trait WebHookComponent extends TemplateComponent { self: Profile =>
class WebHooks(tag: Tag) extends Table[WebHook](tag, "WEB_HOOK") with BasicTemplate {
val url = column[String]("URL")
def * = (userName, repositoryName, url) <> (WebHook.tupled, WebHook.unapply)
def * = (userName, repositoryName, url) <> ((WebHook.apply _).tupled, WebHook.unapply)
def byPrimaryKey(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
}
@@ -18,3 +18,32 @@ case class WebHook(
repositoryName: String,
url: String
)
object WebHook {
sealed class Event(var name: String)
case object CommitComment extends Event("commit_comment")
case object Create extends Event("create")
case object Delete extends Event("delete")
case object Deployment extends Event("deployment")
case object DeploymentStatus extends Event("deployment_status")
case object Fork extends Event("fork")
case object Gollum extends Event("gollum")
case object IssueComment extends Event("issue_comment")
case object Issues extends Event("issues")
case object Member extends Event("member")
case object PageBuild extends Event("page_build")
case object Public extends Event("public")
case object PullRequest extends Event("pull_request")
case object PullRequestReviewComment extends Event("pull_request_review_comment")
case object Push extends Event("push")
case object Release extends Event("release")
case object Status extends Event("status")
case object TeamAdd extends Event("team_add")
case object Watch extends Event("watch")
object Event{
val values = List(CommitComment,Create,Delete,Deployment,DeploymentStatus,Fork,Gollum,IssueComment,Issues,Member,PageBuild,Public,PullRequest,PullRequestReviewComment,Push,Release,Status,TeamAdd,Watch)
private val map:Map[String,Event] = values.map(e => e.name -> e).toMap
def valueOf(name: String): Event = map(name)
def valueOpt(name: String): Option[Event] = map.get(name)
}
}

View File

@@ -0,0 +1,30 @@
package gitbucket.core.model
trait WebHookEventComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import gitbucket.core.model.Profile.WebHooks
lazy val WebHookEvents = TableQuery[WebHookEvents]
implicit val typedType = MappedColumnType.base[WebHook.Event, String](_.name, WebHook.Event.valueOf(_))
class WebHookEvents(tag: Tag) extends Table[WebHookEvent](tag, "WEB_HOOK_EVENT") with BasicTemplate {
val url = column[String]("URL")
val event = column[WebHook.Event]("EVENT")
def * = (userName, repositoryName, url, event) <> ((WebHookEvent.apply _).tupled, WebHookEvent.unapply)
def byWebHook(owner: String, repository: String, url: String) = byRepository(owner, repository) && (this.url === url.bind)
def byWebHook(owner: Column[String], repository: Column[String], url: Column[String]) =
byRepository(userName, repositoryName) && (this.url === url)
def byWebHook(webhook: WebHooks) =
byRepository(webhook.userName, webhook.repositoryName) && (this.url === webhook.url)
def byPrimaryKey(owner: String, repository: String, url: String, event: WebHook.Event) = byWebHook(owner, repository, url) && (this.event === event.bind)
}
}
case class WebHookEvent(
userName: String,
repositoryName: String,
url: String,
event: WebHook.Event
)

View File

@@ -67,6 +67,16 @@ trait Plugin {
*/
def repositoryRoutings(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[GitRepositoryRouting] = Nil
/**
* Override to add receive hooks.
*/
val receiveHooks: Seq[ReceiveHook] = Nil
/**
* Override to add receive hooks.
*/
def receiveHooks(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[ReceiveHook] = Nil
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
@@ -87,6 +97,9 @@ trait Plugin {
(repositoryRoutings ++ repositoryRoutings(registry, context, settings)).foreach { routing =>
registry.addRepositoryRouting(routing)
}
(receiveHooks ++ receiveHooks(registry, context, settings)).foreach { receiveHook =>
registry.addReceiveHook(receiveHook)
}
}
/**

View File

@@ -6,6 +6,7 @@ import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.service.ProtectedBranchService.ProtectedBranchReceiveHook
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
@@ -29,6 +30,8 @@ class PluginRegistry {
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
private val repositoryRoutings = new ListBuffer[GitRepositoryRouting]
private val receiveHooks = new ListBuffer[ReceiveHook]
receiveHooks += new ProtectedBranchReceiveHook()
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
@@ -98,6 +101,12 @@ class PluginRegistry {
}
}
def addReceiveHook(commitHook: ReceiveHook): Unit = {
receiveHooks += commitHook
}
def getReceiveHooks: Seq[ReceiveHook] = receiveHooks.toSeq
private case class GlobalAction(
method: String,
path: String,
@@ -169,7 +178,7 @@ object PluginRegistry {
))
} catch {
case e: Exception => {
case e: Throwable => {
logger.error(s"Error during plugin initialization", e)
}
}

View File

@@ -0,0 +1,15 @@
package gitbucket.core.plugin
import gitbucket.core.model.Profile._
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
import profile.simple._
trait ReceiveHook {
def preReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Option[String] = None
def postReceive(owner: String, repository: String, receivePack: ReceivePack, command: ReceiveCommand, pusher: String)
(implicit session: Session): Unit = ()
}

View File

@@ -20,7 +20,14 @@ trait Renderer {
object MarkdownRenderer extends Renderer {
override def render(request: RenderRequest): Html = {
import request._
Html(Markdown.toHtml(fileContent, repository, enableWikiLink, enableRefsLink, enableAnchor)(context))
Html(Markdown.toHtml(
markdown = fileContent,
repository = repository,
enableWikiLink = enableWikiLink,
enableRefsLink = enableRefsLink,
enableAnchor = enableAnchor,
enableLineBreaks = false
)(context))
}
}
@@ -35,11 +42,13 @@ object DefaultRenderer extends Renderer {
}
}
case class RenderRequest(filePath: List[String],
fileContent: String,
branch: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
context: Context)
case class RenderRequest(
filePath: List[String],
fileContent: String,
branch: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
enableAnchor: Boolean,
context: Context
)

View File

@@ -7,46 +7,51 @@ import gitbucket.core.model.{CommitState, CommitStatus, Account}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.joda.time.LocalDateTime
import gitbucket.core.model.Profile.dateColumnType
trait CommitStatusService {
/** insert or update */
def createCommitStatus(userName: String, repositoryName: String, sha:String, context:String, state:CommitState, targetUrl:Option[String], description:Option[String], now:java.util.Date, creator:Account)(implicit s: Session): Int =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind )
def createCommitStatus(userName: String, repositoryName: String, sha: String, context: String, state: CommitState,
targetUrl: Option[String], description: Option[String], now: java.util.Date, creator: Account)(implicit s: Session): Int =
CommitStatuses
.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind )
.map(_.commitStatusId).firstOption match {
case Some(id:Int) => {
CommitStatuses.filter(_.byPrimaryKey(id)).map{
t => (t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
}.update( (state, targetUrl, now, creator.userName, description) )
id
}
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
userName = userName,
repositoryName = repositoryName,
commitId = sha,
context = context,
state = state,
targetUrl = targetUrl,
description = description,
creator = creator.userName,
registeredDate = now,
updatedDate = now)
case Some(id: Int) => {
CommitStatuses.filter(_.byPrimaryKey(id)).map { t =>
(t.state , t.targetUrl , t.updatedDate , t.creator, t.description)
}.update((state, targetUrl, now, creator.userName, description))
id
}
case None => (CommitStatuses returning CommitStatuses.map(_.commitStatusId)) += CommitStatus(
userName = userName,
repositoryName = repositoryName,
commitId = sha,
context = context,
state = state,
targetUrl = targetUrl,
description = description,
creator = creator.userName,
registeredDate = now,
updatedDate = now)
}
def getCommitStatus(userName: String, repositoryName: String, id: Int)(implicit s: Session) :Option[CommitStatus] =
CommitStatuses.filter(t => t.byPrimaryKey(id) && t.byRepository(userName, repositoryName)).firstOption
def getCommitStatus(userName: String, repositoryName: String, sha: String, context: String)(implicit s: Session) :Option[CommitStatus] =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context===context.bind ).firstOption
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) && t.context === context.bind).firstOption
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
byCommitStatues(userName, repositoryName, sha).list
def getRecentStatuesContexts(userName: String, repositoryName: String, time: java.util.Date)(implicit s: Session) :List[String] =
CommitStatuses.filter(t => t.byRepository(userName, repositoryName)).filter(t => t.updatedDate > time.bind).groupBy(_.context).map(_._1).list
def getCommitStatuesWithCreator(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[(CommitStatus, Account)] =
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts)
.filter{ case (t,a) => t.creator === a.userName }.list
byCommitStatues(userName, repositoryName, sha).innerJoin(Accounts).filter { case (t, a) => t.creator === a.userName }.list
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) ).sortBy(_.updatedDate desc)
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha)).sortBy(_.updatedDate desc)
}

View File

@@ -13,9 +13,9 @@ import StringUtil._
trait CommitsService {
def getCommitComments(owner: String, repository: String, commitId: String, pullRequest: Boolean)(implicit s: Session) =
def getCommitComments(owner: String, repository: String, commitId: String, includePullRequest: Boolean)(implicit s: Session) =
CommitComments filter {
t => t.byCommit(owner, repository, commitId) && (t.pullRequest === pullRequest || pullRequest)
t => t.byCommit(owner, repository, commitId) && (t.issueId.isEmpty || includePullRequest)
} list
def getCommitComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
@@ -27,7 +27,8 @@ trait CommitsService {
None
def createCommitComment(owner: String, repository: String, commitId: String, loginUser: String,
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int], pullRequest: Boolean)(implicit s: Session): Int =
content: String, fileName: Option[String], oldLine: Option[Int], newLine: Option[Int],
issueId: Option[Int])(implicit s: Session): Int =
CommitComments.autoInc insert CommitComment(
userName = owner,
repositoryName = repository,
@@ -39,7 +40,7 @@ trait CommitsService {
newLine = newLine,
registeredDate = currentDate,
updatedDate = currentDate,
pullRequest = pullRequest)
issueId = issueId)
def updateCommitComment(commentId: Int, content: String)(implicit s: Session) =
CommitComments

View File

@@ -399,7 +399,7 @@ trait IssuesService {
object IssuesService {
import javax.servlet.http.HttpServletRequest
val IssueLimit = 30
val IssueLimit = 25
case class IssueSearchCondition(
labels: Set[String] = Set.empty,

View File

@@ -12,6 +12,9 @@ trait LabelsService {
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def getLabel(owner: String, repository: String, labelName: String)(implicit s: Session): Option[Label] =
Labels.filter(_.byLabel(owner, repository, labelName)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels returning Labels.map(_.labelId) += Label(
userName = owner,

View File

@@ -10,10 +10,9 @@ import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.transport.RefSpec
import org.eclipse.jgit.errors.NoMergeBaseException
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent, Repository}
import org.eclipse.jgit.revwalk.RevWalk
trait MergeService {
import MergeService._
/**
@@ -52,26 +51,30 @@ trait MergeService {
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
def tryMergeRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String): Option[(ObjectId, ObjectId, ObjectId)] = {
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${remoteBranch}"
val tmpRefName = s"refs/remote-temp/${remoteUserName}/${remoteRepositoryName}/${remoteBranch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).toURI.toString)
.setRemote(getRepositoryDir(remoteUserName, remoteRepositoryName).toURI.toString)
.setRefSpecs(refSpec)
.call
// merge conflict check
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${requestBranch}")
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${localBranch}")
val mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
if(merger.merge(mergeBaseTip, mergeTip)){
Some((merger.getResultTreeId, mergeBaseTip, mergeTip))
} else {
None
}
} catch {
case e: NoMergeBaseException => true
case e: NoMergeBaseException => None
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
@@ -80,8 +83,54 @@ trait MergeService {
}
}
}
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean =
tryMergeRemote(userName, repositoryName, branch, requestUserName, requestRepositoryName, requestBranch).isEmpty
def pullRemote(localUserName: String, localRepositoryName: String, localBranch: String,
remoteUserName: String, remoteRepositoryName: String, remoteBranch: String,
loginAccount: Account, message: String): Option[ObjectId] = {
tryMergeRemote(localUserName, localRepositoryName, localBranch, remoteUserName, remoteRepositoryName, remoteBranch).map{ case (newTreeId, oldBaseId, oldHeadId) =>
using(Git.open(getRepositoryDir(localUserName, localRepositoryName))) { git =>
val committer = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
val newCommit = Util.createMergeCommit(git.getRepository, newTreeId, committer, message, Seq(oldBaseId, oldHeadId))
Util.updateRefs(git.getRepository, s"refs/heads/${localBranch}", newCommit, false, committer, Some("merge"))
}
oldBaseId
}
}
}
object MergeService{
object Util{
// return treeId
def createMergeCommit(repository: Repository, treeId: ObjectId, committer: PersonIdent, message: String, parents: Seq[ObjectId]): ObjectId = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
mergeCommit.setParentIds(parents:_*)
mergeCommit.setAuthor(committer)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.close()
mergeCommitId
}
def updateRefs(repository: Repository, ref: String, newObjectId: ObjectId, force: Boolean, committer: PersonIdent, refLogMessage: Option[String] = None):Unit = {
// update refs
val refUpdate = repository.updateRef(ref)
refUpdate.setNewObjectId(newObjectId)
refUpdate.setForceUpdate(force)
refUpdate.setRefLogIdent(committer)
refLogMessage.map(refUpdate.setRefLogMessage(_, true))
refUpdate.update()
}
}
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
val repository = git.getRepository
val mergedBranchName = s"refs/pull/${issueId}/merge"
@@ -90,17 +139,17 @@ object MergeService{
lazy val mergeTip = repository.resolve(s"refs/pull/${issueId}/head")
def checkConflictCache(): Option[Boolean] = {
Option(repository.resolve(mergedBranchName)).flatMap{ merged =>
if(parseCommit( merged ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
if(parseCommit(merged).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// merged branch exists
Some(false)
}else{
} else {
None
}
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
if(parseCommit( conflicted ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
if(parseCommit(conflicted).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists
Some(true)
}else{
} else {
None
}
})
@@ -120,17 +169,12 @@ object MergeService{
def updateBranch(treeId:ObjectId, message:String, branchName:String){
// creates merge commit
val mergeCommitId = createMergeCommit(treeId, committer, message)
// update refs
val refUpdate = repository.updateRef(branchName)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(true)
refUpdate.setRefLogIdent(committer)
refUpdate.update()
Util.updateRefs(repository, branchName, mergeCommitId, true, committer)
}
if(!conflicted){
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
}else{
} else {
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
}
@@ -145,28 +189,12 @@ object MergeService{
// creates merge commit
val mergeCommitId = createMergeCommit(mergeResultCommit.getTree().getId(), committer, message)
// update refs
val refUpdate = repository.updateRef(s"refs/heads/${branch}")
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(committer)
refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
Util.updateRefs(repository, s"refs/heads/${branch}", mergeCommitId, false, committer, Some("merged"))
}
// return treeId
private def createMergeCommit(treeId:ObjectId, committer:PersonIdent, message:String) = {
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(treeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
mergeCommit.setAuthor(committer)
mergeCommit.setCommitter(committer)
mergeCommit.setMessage(message)
// insertObject and got mergeCommit Object Id
val inserter = repository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
mergeCommitId
}
private def createMergeCommit(treeId: ObjectId, committer: PersonIdent, message: String) =
Util.createMergeCommit(repository, treeId, committer, message, Seq[ObjectId](mergeBaseTip, mergeTip))
private def parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
}
}

View File

@@ -0,0 +1,121 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.model.Profile._
import gitbucket.core.plugin.ReceiveHook
import profile.simple._
import org.eclipse.jgit.transport.{ReceivePack, ReceiveCommand}
trait ProtectedBranchService {
import ProtectedBranchService._
private def getProtectedBranchInfoOpt(owner: String, repository: String, branch: String)(implicit session: Session): Option[ProtectedBranchInfo] =
ProtectedBranches
.leftJoin(ProtectedBranchContexts)
.on{ case (pb, c) => pb.byBranch(c.userName, c.repositoryName, c.branch) }
.map{ case (pb, c) => pb -> c.context.? }
.filter(_._1.byPrimaryKey(owner, repository, branch))
.list
.groupBy(_._1)
.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))
def getProtectedBranchList(owner: String, repository: String)(implicit session: Session): List[String] =
ProtectedBranches.filter(_.byRepository(owner, repository)).map(_.branch).list
def enableBranchProtection(owner: String, repository: String, branch:String, includeAdministrators: Boolean, contexts: Seq[String])
(implicit session: Session): Unit = {
disableBranchProtection(owner, repository, branch)
ProtectedBranches.insert(new ProtectedBranch(owner, repository, branch, includeAdministrators && contexts.nonEmpty))
contexts.map{ context =>
ProtectedBranchContexts.insert(new ProtectedBranchContext(owner, repository, branch, context))
}
}
def disableBranchProtection(owner: String, repository: String, branch:String)(implicit session: Session): Unit =
ProtectedBranches.filter(_.byPrimaryKey(owner, repository, branch)).delete
}
object ProtectedBranchService {
class ProtectedBranchReceiveHook extends ReceiveHook with ProtectedBranchService {
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)
} else {
None
}
}
}
case class ProtectedBranchInfo(
owner: String,
repository: String,
enabled: Boolean,
/**
* Require status checks to pass before merging
* Choose which status checks must pass before branches can be merged into test.
* When enabled, commits must first be pushed to another branch,
* then merged or pushed directly to test after status checks have passed.
*/
contexts: Seq[String],
/**
* Include administrators
* Enforce required status checks for repository administrators.
*/
includeAdministrators: Boolean) extends AccountService with CommitStatusService {
def isAdministrator(pusher: String)(implicit session: Session): Boolean =
pusher == owner || getGroupMembers(owner).filter(gm => gm.userName == pusher && gm.isManager).nonEmpty
/**
* Can't be force pushed
* Can't be deleted
* Can't have changes merged into them until required status checks pass
*/
def getStopReason(isAllowNonFastForwards: Boolean, command: ReceiveCommand, pusher: String)(implicit session: Session): Option[String] = {
if(enabled){
command.getType() match {
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if isAllowNonFastForwards =>
Some("Cannot force-push to a protected branch")
case ReceiveCommand.Type.UPDATE|ReceiveCommand.Type.UPDATE_NONFASTFORWARD if needStatusCheck(pusher) =>
unSuccessedContexts(command.getNewId.name) match {
case s if s.size == 1 => Some(s"""Required status check "${s.toSeq(0)}" is expected""")
case s if s.size >= 1 => Some(s"${s.size} of ${contexts.size} required status checks are expected")
case _ => None
}
case ReceiveCommand.Type.DELETE =>
Some("Cannot delete a protected branch")
case _ => None
}
}else{
None
}
}
def unSuccessedContexts(sha1: String)(implicit session: Session): Set[String] = if(contexts.isEmpty){
Set.empty
} else {
contexts.toSet -- getCommitStatues(owner, repository, sha1).filter(_.state == CommitState.SUCCESS).map(_.context).toSet
}
def needStatusCheck(pusher: String)(implicit session: Session): Boolean = pusher match {
case _ if !enabled => false
case _ if contexts.isEmpty => false
case _ if includeAdministrators => true
case p if isAdministrator(p) => false
case _ => true
}
}
object ProtectedBranchInfo{
def disabled(owner: String, repository: String): ProtectedBranchInfo = ProtectedBranchInfo(owner, repository, false, Nil, false)
}
}

View File

@@ -1,6 +1,6 @@
package gitbucket.core.service
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook, CommitStatus, CommitState}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
@@ -145,4 +145,29 @@ object PullRequestService {
case class PullRequestCount(userName: String, count: Int)
case class MergeStatus(
hasConflict: Boolean,
commitStatues:List[CommitStatus],
branchProtection: ProtectedBranchService.ProtectedBranchInfo,
branchIsOutOfDate: Boolean,
hasUpdatePermission: Boolean,
needStatusCheck: Boolean,
hasMergePermission: Boolean,
commitIdTo: String){
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 canUpdate = branchIsOutOfDate && !hasConflict
val canMerge = hasMergePermission && !hasConflict && !hasRequiredStatusProblem
lazy val commitStateSummary:(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
}
lazy val statusesAndRequired:List[(CommitStatus, Boolean)] = statuses.map{ s => s -> branchProtection.contexts.exists(_==s.context) }
lazy val isAllSuccess = commitStateSummary._1==CommitState.SUCCESS
}
}

View File

@@ -79,8 +79,8 @@ trait RepositorySearchService { self: IssuesService =>
}
}
}
treeWalk.release
revWalk.release
treeWalk.close()
revWalk.close()
list.toList
}

View File

@@ -1,5 +1,6 @@
package gitbucket.core.service
import gitbucket.core.controller.Context
import gitbucket.core.model.{Collaborator, Repository, Account}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
@@ -46,17 +47,20 @@ trait RepositoryService { self: AccountService =>
(Repositories filter { t => t.byRepository(oldUserName, oldRepositoryName) } firstOption).map { repository =>
Repositories insert repository.copy(userName = newUserName, repositoryName = newRepositoryName)
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitStatuses = CommitStatuses.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHooks = WebHooks .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val webHookEvents = WebHookEvents .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val milestones = Milestones .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueId = IssueId .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val commitStatuses = CommitStatuses .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val collaborators = Collaborators .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val protectedBranches = ProtectedBranches .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val protectedBranchContexts = ProtectedBranchContexts.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t =>
(t.originUserName === oldUserName.bind) && (t.originRepositoryName === oldRepositoryName.bind)
@@ -75,9 +79,10 @@ trait RepositoryService { self: AccountService =>
deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
Issues.insertAll(issues.map { x => x.copy(
@@ -88,11 +93,13 @@ trait RepositoryService { self: AccountService =>
}
)} :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments.insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses.insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
PullRequests .insertAll(pullRequests .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitComments .insertAll(commitComments.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
CommitStatuses .insertAll(commitStatuses.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
ProtectedBranches .insertAll(protectedBranches.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
ProtectedBranchContexts.insertAll(protectedBranchContexts.map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
// Update source repository of pull requests
PullRequests.filter { t =>
@@ -146,6 +153,7 @@ trait RepositoryService { self: AccountService =>
IssueId .filter(_.byRepository(userName, repositoryName)).delete
Milestones .filter(_.byRepository(userName, repositoryName)).delete
WebHooks .filter(_.byRepository(userName, repositoryName)).delete
WebHookEvents .filter(_.byRepository(userName, repositoryName)).delete
Repositories .filter(_.byRepository(userName, repositoryName)).delete
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME
@@ -187,10 +195,9 @@ trait RepositoryService { self: AccountService =>
*
* @param userName the user name of the repository owner
* @param repositoryName the repository name
* @param baseUrl the base url of this application
* @return the repository information
*/
def getRepository(userName: String, repositoryName: String, baseUrl: String)(implicit s: Session): Option[RepositoryInfo] = {
def getRepository(userName: String, repositoryName: String)(implicit s: Session): Option[RepositoryInfo] = {
(Repositories filter { t => t.byRepository(userName, repositoryName) } firstOption) map { repository =>
// for getting issue count and pull request count
val issues = Issues.filter { t =>
@@ -198,7 +205,7 @@ trait RepositoryService { self: AccountService =>
}.map(_.pullRequest).list
new RepositoryInfo(
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl),
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName),
repository,
issues.count(_ == false),
issues.count(_ == true),
@@ -227,7 +234,7 @@ trait RepositoryService { self: AccountService =>
}.list
}
def getUserRepositories(userName: String, baseUrl: String, withoutPhysicalInfo: Boolean = false)
def getUserRepositories(userName: String, withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
Repositories.filter { t1 =>
(t1.userName === userName.bind) ||
@@ -235,9 +242,9 @@ trait RepositoryService { self: AccountService =>
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
},
repository,
getForkedCount(
@@ -253,13 +260,12 @@ trait RepositoryService { self: AccountService =>
* If repositoryUserName is given then filters results by repository owner.
*
* @param loginAccount the logged in account
* @param baseUrl the base url of this application
* @param repositoryUserName the repository owner (if None then returns all repositories which are visible for logged in user)
* @param withoutPhysicalInfo if true then the result does not include physical repository information such as commit count,
* branches and tags
* @return the repository information which is sorted in descending order of lastActivityDate.
*/
def getVisibleRepositories(loginAccount: Option[Account], baseUrl: String, repositoryUserName: Option[String] = None,
def getVisibleRepositories(loginAccount: Option[Account], repositoryUserName: Option[String] = None,
withoutPhysicalInfo: Boolean = false)
(implicit s: Session): List[RepositoryInfo] = {
(loginAccount match {
@@ -277,9 +283,9 @@ trait RepositoryService { self: AccountService =>
}.sortBy(_.lastActivityDate desc).list.map{ repository =>
new RepositoryInfo(
if(withoutPhysicalInfo){
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
new JGitUtil.RepositoryInfo(repository.userName, repository.repositoryName)
} else {
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName, baseUrl)
JGitUtil.getRepositoryInfo(repository.userName, repository.repositoryName)
},
repository,
getForkedCount(
@@ -306,11 +312,17 @@ trait RepositoryService { self: AccountService =>
/**
* Save repository options.
*/
def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], defaultBranch: String, isPrivate: Boolean)(implicit s: Session): Unit =
def saveRepositoryOptions(userName: String, repositoryName: String,
description: Option[String], isPrivate: Boolean)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => (r.description.?, r.defaultBranch, r.isPrivate, r.updatedDate) }
.update (description, defaultBranch, isPrivate, currentDate)
.map { r => (r.description.?, r.isPrivate, r.updatedDate) }
.update (description, isPrivate, currentDate)
def saveRepositoryDefaultBranch(userName: String, repositoryName: String,
defaultBranch: String)(implicit s: Session): Unit =
Repositories.filter(_.byRepository(userName, repositoryName))
.map { r => r.defaultBranch }
.update (defaultBranch)
/**
* Add collaborator to the repository.
@@ -376,32 +388,39 @@ trait RepositoryService { self: AccountService =>
object RepositoryService {
case class RepositoryInfo(owner: String, name: String, httpUrl: String, repository: Repository,
case class RepositoryInfo(owner: String, name: String, repository: Repository,
issueCount: Int, pullCount: Int, commitCount: Int, forkedCount: Int,
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]){
lazy val host = """^https?://(.+?)(:\d+)?/""".r.findFirstMatchIn(httpUrl).get.group(1)
def sshUrl(port: Int, userName: String) = s"ssh://${userName}@${host}:${port}/${owner}/${name}.git"
def sshOpenRepoUrl(platform: String, port: Int, userName: String) = openRepoUrl(platform, sshUrl(port, userName))
def httpOpenRepoUrl(platform: String) = openRepoUrl(platform, httpUrl)
def openRepoUrl(platform: String, openUrl: String) = s"github-${platform}://openRepo/${openUrl}"
branchList: Seq[String], tags: Seq[JGitUtil.TagInfo], managers: Seq[String]) {
/**
* Creates instance with issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, issueCount: Int, pullCount: Int, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, issueCount, pullCount, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
this(
repo.owner, repo.name, model,
issueCount, pullCount, repo.commitCount, forkedCount,
repo.branchList, repo.tags, managers)
/**
* Creates instance without issue count and pull request count.
*/
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(repo.owner, repo.name, repo.url, model, 0, 0, repo.commitCount, forkedCount, repo.branchList, repo.tags, managers)
def this(repo: JGitUtil.RepositoryInfo, model: Repository, forkedCount: Int, managers: Seq[String]) =
this(
repo.owner, repo.name, model,
0, 0, repo.commitCount, forkedCount,
repo.branchList, repo.tags, managers)
def httpUrl(implicit context: Context): String = RepositoryService.httpUrl(owner, name)
def sshUrl(implicit context: Context): Option[String] = RepositoryService.sshUrl(owner, name)
}
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
def httpUrl(owner: String, name: String)(implicit context: Context): String = s"${context.baseUrl}/git/${owner}/${name}.git"
def sshUrl(owner: String, name: String)(implicit context: Context): Option[String] =
if(context.settings.ssh){
context.loginAccount.flatMap { loginAccount =>
context.settings.sshAddress.map { x => s"ssh://${loginAccount.userName}@${x.host}:${x.port}/${owner}/${name}.git" }
}
} else None
def openRepoUrl(openUrl: String)(implicit context: Context): String = s"github-${context.platform}://openRepo/${openUrl}"
}

View File

@@ -1,6 +1,7 @@
package gitbucket.core.service
import gitbucket.core.util.{Directory, ControlUtil}
import gitbucket.core.util.Implicits._
import Directory._
import ControlUtil._
import SystemSettingsService._
@@ -21,6 +22,7 @@ trait SystemSettingsService {
props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(Ssh, settings.ssh.toString)
settings.sshHost.foreach(x => props.setProperty(SshHost, x.trim))
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
props.setProperty(UseSMTP, settings.useSMTP.toString)
if(settings.useSMTP) {
@@ -75,6 +77,7 @@ trait SystemSettingsService {
getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false),
getOptionValue[String](props, SshHost, None).map(_.trim),
getOptionValue(props, SshPort, Some(DefaultSshPort)),
getValue(props, UseSMTP, getValue(props, Notification, false)), // handle migration scenario from only notification to useSMTP
if(getValue(props, UseSMTP, getValue(props, Notification, false))){
@@ -126,16 +129,19 @@ object SystemSettingsService {
notification: Boolean,
activityLogLimit: Option[Int],
ssh: Boolean,
sshHost: Option[String],
sshPort: Option[Int],
useSMTP: Boolean,
smtp: Option[Smtp],
ldapAuthentication: Boolean,
ldap: Option[Ldap]){
def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse {
defining(request.getRequestURL.toString){ url =>
url.substring(0, url.length - (request.getRequestURI.length - request.getContextPath.length))
def baseUrl(request: HttpServletRequest): String = baseUrl.fold(request.baseUrl)(_.stripSuffix("/"))
def sshAddress:Option[SshAddress] =
for {
host <- sshHost if ssh
}
}.stripSuffix("/")
yield SshAddress(host, sshPort.getOrElse(DefaultSshPort))
}
case class Ldap(
@@ -161,6 +167,10 @@ object SystemSettingsService {
fromAddress: Option[String],
fromName: Option[String])
case class SshAddress(
host:String,
port:Int)
val DefaultSshPort = 29418
val DefaultSmtpPort = 25
val DefaultLdapPort = 389
@@ -174,6 +184,7 @@ object SystemSettingsService {
private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit"
private val Ssh = "ssh"
private val SshHost = "ssh.host"
private val SshPort = "ssh.port"
private val UseSMTP = "useSMTP"
private val SmtpHost = "smtp.host"
@@ -216,7 +227,4 @@ object SystemSettingsService {
else value
}
// // TODO temporary flag
// val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
}

View File

@@ -1,7 +1,7 @@
package gitbucket.core.service
import gitbucket.core.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment}
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment, WebHookEvent, CommitComment}
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.util.JGitUtil.CommitInfo
@@ -14,6 +14,9 @@ import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.ObjectId
import org.slf4j.LoggerFactory
import scala.concurrent._
import org.apache.http.HttpRequest
import org.apache.http.HttpResponse
trait WebHookService {
@@ -21,46 +24,93 @@ trait WebHookService {
private val logger = LoggerFactory.getLogger(classOf[WebHookService])
def getWebHookURLs(owner: String, repository: String)(implicit s: Session): List[WebHook] =
WebHooks.filter(_.byRepository(owner, repository)).sortBy(_.url).list
/** get All WebHook informations of repository */
def getWebHooks(owner: String, repository: String)(implicit s: Session): List[(WebHook, Set[WebHook.Event])] =
WebHooks.filter(_.byRepository(owner, repository))
.innerJoin(WebHookEvents).on { (w, t) => t.byWebHook(w) }
.map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).toList.sortBy(_._1.url)
def addWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
/** get All WebHook informations of repository event */
def getWebHooksByEvent(owner: String, repository: String, event: WebHook.Event)(implicit s: Session): List[WebHook] =
WebHookEvents.filter(t => t.byRepository(owner, repository) && t.event === event.bind)
.list.map(t => WebHook(t.userName, t.repositoryName, t.url))
/** get All WebHook information from repository to url */
def getWebHook(owner: String, repository: String, url: String)(implicit s: Session): Option[(WebHook, Set[WebHook.Event])] =
WebHooks
.filter(_.byPrimaryKey(owner, repository, url))
.innerJoin(WebHookEvents).on { (w, t) => t.byWebHook(w) }
.map{ case (w,t) => w -> t.event }
.list.groupBy(_._1).mapValues(_.map(_._2).toSet).headOption
def addWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
WebHooks insert WebHook(owner, repository, url)
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHookOf(owner: String, repository: String, eventName: String)(makePayload: => Option[WebHookPayload])(implicit s: Session, c: JsonFormat.Context): Unit = {
val webHookURLs = getWebHookURLs(owner, repository)
if(webHookURLs.nonEmpty){
makePayload.map(callWebHook(eventName, webHookURLs, _))
events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event)
}
}
def callWebHook(eventName: String, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): Unit = {
import org.apache.http.client.methods.HttpPost
def updateWebHook(owner: String, repository: String, url :String, events: Set[WebHook.Event])(implicit s: Session): Unit = {
WebHookEvents.filter(_.byWebHook(owner, repository, url)).delete
events.toSet.map{ event: WebHook.Event =>
WebHookEvents insert WebHookEvent(owner, repository, url, event)
}
}
def deleteWebHook(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHookOf(owner: String, repository: String, event: WebHook.Event)(makePayload: => Option[WebHookPayload])
(implicit s: Session, c: JsonFormat.Context): Unit = {
val webHooks = getWebHooksByEvent(owner, repository, event)
if(webHooks.nonEmpty){
makePayload.map(callWebHook(event, webHooks, _))
}
}
def callWebHook(event: WebHook.Event, webHookURLs: List[WebHook], payload: WebHookPayload)
(implicit c: JsonFormat.Context): List[(WebHook, String, Future[HttpRequest], Future[HttpResponse])] = {
import org.apache.http.impl.client.HttpClientBuilder
import scala.concurrent._
import ExecutionContext.Implicits.global
import org.apache.http.protocol.HttpContext
import org.apache.http.client.methods.HttpPost
if(webHookURLs.nonEmpty){
val json = JsonFormat(payload)
val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl =>
webHookURLs.map { webHookUrl =>
val reqPromise = Promise[HttpRequest]
val f = Future {
logger.debug(s"start web hook invocation for ${webHookUrl}")
val httpPost = new HttpPost(webHookUrl.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", eventName)
val itcp = new org.apache.http.HttpRequestInterceptor{
def process(res: HttpRequest, ctx: HttpContext): Unit = {
reqPromise.success(res)
}
}
try{
val httpClient = HttpClientBuilder.create.addInterceptorLast(itcp).build
logger.debug(s"start web hook invocation for ${webHookUrl.url}")
val httpPost = new HttpPost(webHookUrl.url)
httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded")
httpPost.addHeader("X-Github-Event", event.name)
httpPost.addHeader("X-Github-Delivery", java.util.UUID.randomUUID().toString)
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
val params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json))
httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8"))
httpClient.execute(httpPost)
httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}")
val res = httpClient.execute(httpPost)
httpPost.releaseConnection()
logger.debug(s"end web hook invocation for ${webHookUrl}")
res
}catch{
case e:Throwable => {
if(!reqPromise.isCompleted){
reqPromise.failure(e)
}
throw e
}
}
}
f.onSuccess {
case s => logger.debug(s"Success: web hook request to ${webHookUrl.url}")
@@ -68,9 +118,12 @@ trait WebHookService {
f.onFailure {
case t => logger.error(s"Failed: web hook request to ${webHookUrl.url}", t)
}
(webHookUrl, json, reqPromise.future, f)
}
} else {
Nil
}
logger.debug("end callWebHook")
// logger.debug("end callWebHook")
}
}
@@ -80,8 +133,9 @@ trait WebHookPullRequestService extends WebHookService {
import WebHookService._
// https://developer.github.com/v3/activity/events/types/#issuesevent
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issues"){
def callIssuesWebHook(action: String, repository: RepositoryService.RepositoryInfo, issue: Issue, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues){
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
for{
repoOwner <- users.get(repository.owner)
@@ -97,16 +151,17 @@ trait WebHookPullRequestService extends WebHookService {
}
}
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
def callPullRequestWebHook(action: String, repository: RepositoryService.RepositoryInfo, issueId: Int, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, "pull_request"){
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))
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestPayload(
action = action,
@@ -135,15 +190,17 @@ trait WebHookPullRequestService extends WebHookService {
ru <- Accounts if ru.userName === pr.requestUserName
iu <- Accounts if iu.userName === is.openedUserName
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
wht <- WebHookEvents if wht.event === WebHook.PullRequest.asInstanceOf[WebHook.Event].bind && wht.byWebHook(wh)
} yield {
((is, iu, pr, bu, ru), wh)
}).list.groupBy(_._1).mapValues(_.map(_._2))
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
def callPullRequestWebHookByRequestBranch(action: String, requestRepository: RepositoryService.RepositoryInfo, requestBranch: String, baseUrl: String, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._
for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl)
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
@@ -155,7 +212,37 @@ trait WebHookPullRequestService extends WebHookService {
baseRepository = baseRepo,
baseOwner = baseOwner,
sender = sender)
callWebHook("pull_request", webHooks, payload)
callWebHook(WebHook.PullRequest, webHooks, payload)
}
}
}
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)
(implicit s: Session, context:JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment){
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)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
WebHookPullRequestReviewCommentPayload(
action = action,
comment = comment,
issue = issue,
issueUser = issueUser,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
baseRepository = repository,
baseOwner = baseOwner,
sender = sender)
}
}
}
}
@@ -164,8 +251,9 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
self: AccountService with RepositoryService with PullRequestService with IssuesService =>
import WebHookService._
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, "issue_comment"){
def callIssueCommentWebHook(repository: RepositoryService.RepositoryInfo, issue: Issue, issueCommentId: Int, sender: Account)
(implicit s: Session, context:JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment){
for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
@@ -191,7 +279,8 @@ object WebHookService {
// https://developer.github.com/v3/activity/events/types/#pushevent
case class WebHookPushPayload(
pusher: ApiUser,
pusher: ApiPusher,
sender: ApiUser,
ref: String,
before: String,
after: String,
@@ -208,11 +297,12 @@ object WebHookService {
}
object WebHookPushPayload {
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
def apply(git: Git, sender: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account,
newId: ObjectId, oldId: ObjectId): WebHookPushPayload =
WebHookPushPayload(
pusher = ApiUser(pusher),
pusher = ApiPusher(sender),
sender = ApiUser(sender),
ref = refName,
before = ObjectId.toString(oldId),
after = ObjectId.toString(newId),
@@ -289,4 +379,38 @@ object WebHookService {
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser), issue.isPullRequest),
sender = ApiUser(sender))
}
// https://developer.github.com/v3/activity/events/types/#pullrequestreviewcommentevent
case class WebHookPullRequestReviewCommentPayload(
action: String,
comment: ApiPullRequestReviewComment,
pull_request: ApiPullRequest,
repository: ApiRepository,
sender: ApiUser
) extends WebHookPayload
object WebHookPullRequestReviewCommentPayload{
def apply(
action: String,
comment: CommitComment,
issue: Issue,
issueUser: Account,
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
baseRepository: RepositoryInfo,
baseOwner: Account,
sender: Account
) : WebHookPullRequestReviewCommentPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
WebHookPullRequestReviewCommentPayload(
action = action,
comment = ApiPullRequestReviewComment(comment, senderPayload, RepositoryName(baseRepository), issue.issueId),
pull_request = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser)),
repository = baseRepoPayload,
sender = senderPayload)
}
}
}

View File

@@ -1,7 +1,9 @@
package gitbucket.core.service
import java.util.Date
import gitbucket.core.controller.Context
import gitbucket.core.model.Account
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import org.eclipse.jgit.api.Git
@@ -13,7 +15,6 @@ import java.io.ByteArrayInputStream
import org.eclipse.jgit.patch._
import org.eclipse.jgit.api.errors.PatchFormatException
import scala.collection.JavaConverters._
import RepositoryService.RepositoryInfo
object WikiService {
@@ -38,10 +39,13 @@ object WikiService {
*/
case class WikiPageHistoryInfo(name: String, committer: String, message: String, date: Date)
def httpUrl(repository: RepositoryInfo) = repository.httpUrl.replaceFirst("\\.git\\Z", ".wiki.git")
def sshUrl(repository: RepositoryInfo, settings: SystemSettingsService.SystemSettings, userName: String) =
repository.sshUrl(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), userName).replaceFirst("\\.git\\Z", ".wiki.git")
def wikiHttpUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): String
= RepositoryService.httpUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
def wikiSshUrl(repositoryInfo: RepositoryInfo)(implicit context: Context): Option[String]
= RepositoryService.sshUrl(repositoryInfo.owner, repositoryInfo.name + ".wiki")
}
trait WikiService {
@@ -93,7 +97,7 @@ trait WikiService {
def getWikiPageList(owner: String, repository: String): List[String] = {
using(Git.open(Directory.getWikiRepositoryDir(owner, repository))){ git =>
JGitUtil.getFileList(git, "master", ".")
.filter(_.name.endsWith(".md"))
.filter(_.name.endsWith(".md")).filterNot(_.name.startsWith("_"))
.map(_.name.stripSuffix(".md"))
.sortBy(x => x)
}

View File

@@ -18,9 +18,13 @@ import gitbucket.core.util.Directory
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
* The history of versions. A head of this sequence is the current GitBucket version.
*/
val versions = Seq(
new Version(3, 12),
new Version(3, 11),
new Version(3, 10),
new Version(3, 9),
new Version(3, 8),
new Version(3, 7) with SystemSettingsService {
override def update(conn: Connection, cl: ClassLoader): Unit = {
@@ -153,7 +157,7 @@ object AutoUpdate {
)
/**
* The head version of BitBucket.
* The head version of GitBucket.
*/
val headVersion = versions.head

View File

@@ -74,7 +74,7 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
request.paths match {
case Array(_, repositoryOwner, repositoryName, _*) =>
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", ""), "") match {
getRepository(repositoryOwner, repositoryName.replaceFirst("\\.wiki\\.git$|\\.git$", "")) match {
case Some(repository) => {
if(!isUpdating && !repository.repository.isPrivate && settings.allowAnonymousAccess){
chain.doFilter(request, response)

View File

@@ -3,7 +3,7 @@ package gitbucket.core.servlet
import java.io.File
import gitbucket.core.api
import gitbucket.core.model.Session
import gitbucket.core.model.{Session, WebHook}
import gitbucket.core.plugin.{GitRepositoryRouting, PluginRegistry}
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
@@ -111,13 +111,21 @@ import scala.collection.JavaConverters._
class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl: String)(implicit session: Session)
extends PostReceiveHook with PreReceiveHook
with RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService {
with WebHookPullRequestService with ProtectedBranchService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
def onPreReceive(receivePack: ReceivePack, commands: java.util.Collection[ReceiveCommand]): Unit = {
try {
commands.asScala.foreach { command =>
// call pre-commit hook
PluginRegistry().getReceiveHooks
.flatMap(_.preReceive(owner, repository, receivePack, command, pusher))
.headOption.foreach { error =>
command.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, error)
}
}
using(Git.open(Directory.getRepositoryDir(owner, repository))) { git =>
existIds = JGitUtil.getAllCommitIds(git)
}
@@ -152,7 +160,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
val repositoryInfo = getRepository(owner, repository, baseUrl).get
val repositoryInfo = getRepository(owner, repository).get
// Extract new commit and apply issue comment
val defaultBranch = repositoryInfo.repository.defaultBranch
@@ -200,13 +208,16 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
}
// call web hook
callWebHookOf(owner, repository, "push"){
callWebHookOf(owner, repository, WebHook.Push){
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount,
newId = command.getNewId(), oldId = command.getOldId())
}
}
// call post-commit hook
PluginRegistry().getReceiveHooks.foreach(_.postReceive(owner, repository, receivePack, command, pusher))
}
}
// update repository last modified time.

View File

@@ -23,10 +23,10 @@ object GitCommand {
abstract class GitCommand() extends Command {
private val logger = LoggerFactory.getLogger(classOf[GitCommand])
protected var err: OutputStream = null
protected var in: InputStream = null
protected var out: OutputStream = null
protected var callback: ExitCallback = null
@volatile protected var err: OutputStream = null
@volatile protected var in: InputStream = null
@volatile protected var out: OutputStream = null
@volatile protected var callback: ExitCallback = null
protected def runTask(user: String)(implicit session: Session): Unit
@@ -87,11 +87,11 @@ abstract class DefaultGitCommand(val owner: String, val repoName: String) extend
}
class DefaultGitUploadPack(owner: String, repoName: String, baseUrl: String) extends DefaultGitCommand(owner, repoName)
class DefaultGitUploadPack(owner: String, repoName: String) extends DefaultGitCommand(owner, repoName)
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
if(!repositoryInfo.repository.isPrivate || isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
@@ -107,7 +107,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
with RepositoryService with AccountService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", ""), baseUrl).foreach { repositoryInfo =>
getRepository(owner, repoName.replaceFirst("\\.wiki\\Z", "")).foreach { repositoryInfo =>
if(isWritableUser(user, repositoryInfo)){
using(Git.open(getRepositoryDir(owner, repoName))) { git =>
val repository = git.getRepository
@@ -124,7 +124,7 @@ class DefaultGitReceivePack(owner: String, repoName: String, baseUrl: String) ex
}
}
class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
class PluginGitUploadPack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -139,7 +139,7 @@ class PluginGitUploadPack(repoName: String, baseUrl: String, routing: GitReposit
}
}
class PluginGitReceivePack(repoName: String, baseUrl: String, routing: GitRepositoryRouting) extends GitCommand
class PluginGitReceivePack(repoName: String, routing: GitRepositoryRouting) extends GitCommand
with SystemSettingsService {
override protected def runTask(user: String)(implicit session: Session): Unit = {
@@ -163,9 +163,9 @@ class GitCommandFactory(baseUrl: String) extends CommandFactory {
logger.debug(s"command: $command")
command match {
case SimpleCommandRegex ("upload" , repoName) if(pluginRepository(repoName)) => new PluginGitUploadPack (repoName, baseUrl, routing(repoName))
case SimpleCommandRegex ("receive", repoName) if(pluginRepository(repoName)) => new PluginGitReceivePack(repoName, baseUrl, routing(repoName))
case DefaultCommandRegex("upload" , owner, repoName) => new DefaultGitUploadPack (owner, repoName, baseUrl)
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 _ => new UnknownCommand(command)
}

View File

@@ -1,12 +1,13 @@
package gitbucket.core.ssh
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.service.SystemSettingsService.SshAddress
import org.apache.sshd.common.Factory
import org.apache.sshd.server.{Environment, ExitCallback, Command}
import java.io.{OutputStream, InputStream}
import org.eclipse.jgit.lib.Constants
class NoShell extends Factory[Command] with SystemSettingsService {
class NoShell(sshAddress:SshAddress) extends Factory[Command] {
override def create(): Command = new Command() {
private var in: InputStream = null
private var out: OutputStream = null
@@ -15,7 +16,6 @@ class NoShell extends Factory[Command] with SystemSettingsService {
override def start(env: Environment): Unit = {
val user = env.getEnv.get("USER")
val port = loadSystemSettings().sshPort.getOrElse(SystemSettingsService.DefaultSshPort)
val message =
"""
| Welcome to
@@ -31,8 +31,8 @@ class NoShell extends Factory[Command] with SystemSettingsService {
|
| Please use:
|
| git clone ssh://%s@GITBUCKET_HOST:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, port).replace("\n", "\r\n") + "\r\n"
| git clone ssh://%s@%s:%d/OWNER/REPOSITORY_NAME.git
""".stripMargin.format(user, sshAddress.host, sshAddress.port).replace("\n", "\r\n") + "\r\n"
err.write(Constants.encode(message))
err.flush()
in.close()

View File

@@ -1,10 +1,11 @@
package gitbucket.core.ssh
import java.security.PublicKey
import gitbucket.core.service.SshKeyService
import gitbucket.core.servlet.Database
import org.apache.sshd.server.PublickeyAuthenticator
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
import org.apache.sshd.server.session.ServerSession
import java.security.PublicKey
class PublicKeyAuthenticator extends PublickeyAuthenticator with SshKeyService {

View File

@@ -1,28 +1,34 @@
package gitbucket.core.ssh
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import javax.servlet.{ServletContextEvent, ServletContextListener}
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util.Directory
import gitbucket.core.service.SystemSettingsService.SshAddress
import gitbucket.core.util.{Directory}
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
object SshServer {
private val logger = LoggerFactory.getLogger(SshServer.getClass)
private val server = org.apache.sshd.SshServer.setUpDefaultServer()
private val server = org.apache.sshd.server.SshServer.setUpDefaultServer()
private val active = new AtomicBoolean(false)
private def configure(port: Int, baseUrl: String) = {
server.setPort(port)
server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(s"${Directory.GitBucketHome}/gitbucket.ser", "RSA"))
private def configure(sshAddress: SshAddress, baseUrl: String) = {
server.setPort(sshAddress.port)
val provider = new SimpleGeneratorHostKeyProvider(new File(s"${Directory.GitBucketHome}/gitbucket.ser"))
provider.setAlgorithm("RSA")
provider.setOverwriteAllowed(false)
server.setKeyPairProvider(provider)
server.setPublickeyAuthenticator(new PublicKeyAuthenticator)
server.setCommandFactory(new GitCommandFactory(baseUrl))
server.setShellFactory(new NoShell)
server.setShellFactory(new NoShell(sshAddress))
}
def start(port: Int, baseUrl: String) = {
def start(sshAddress: SshAddress, baseUrl: String) = {
if(active.compareAndSet(false, true)){
configure(port, baseUrl)
configure(sshAddress, baseUrl)
server.start()
logger.info(s"Start SSH Server Listen on ${server.getPort}")
}
@@ -50,20 +56,18 @@ class SshServerListener extends ServletContextListener with SystemSettingsServic
override def contextInitialized(sce: ServletContextEvent): Unit = {
val settings = loadSystemSettings()
if(settings.ssh){
settings.baseUrl match {
case None =>
logger.error("Could not start SshServer because the baseUrl is not configured.")
case Some(baseUrl) =>
SshServer.start(settings.sshPort.getOrElse(SystemSettingsService.DefaultSshPort), baseUrl)
}
if (settings.sshAddress.isDefined && settings.baseUrl.isEmpty) {
logger.error("Could not start SshServer because the baseUrl is not configured.")
}
for {
sshAddress <- settings.sshAddress
baseUrl <- settings.baseUrl
}
SshServer.start(sshAddress, baseUrl)
}
override def contextDestroyed(sce: ServletContextEvent): Unit = {
if(loadSystemSettings().ssh){
SshServer.stop()
}
SshServer.stop()
}
}

View File

@@ -1,10 +1,13 @@
package gitbucket.core.ssh
import java.security.PublicKey
import org.slf4j.LoggerFactory
import org.apache.commons.codec.binary.Base64
import org.apache.sshd.common.config.keys.KeyUtils
import org.apache.sshd.common.util.buffer.ByteArrayBuffer
import org.eclipse.jgit.lib.Constants
import org.apache.sshd.common.util.{KeyUtils, Buffer}
import org.slf4j.LoggerFactory
object SshUtil {
@@ -20,7 +23,7 @@ object SshUtil {
try {
val encodedKey = parts(1)
val decode = Base64.decodeBase64(Constants.encodeASCII(encodedKey))
Some(new Buffer(decode).getRawPublicKey)
Some(new ByteArrayBuffer(decode).getRawPublicKey)
} catch {
case e: Throwable =>
logger.debug(e.getMessage, e)

View File

@@ -36,7 +36,7 @@ trait OwnerAuthenticator { self: ControllerBase with RepositoryService with Acco
private def authenticate(action: (RepositoryInfo) => Any) = {
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(repository.owner == x.userName) => action(repository)
@@ -95,7 +95,7 @@ trait CollaboratorsAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = {
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(paths(0) == x.userName) => action(repository)
@@ -118,7 +118,7 @@ trait ReferrerAuthenticator { self: ControllerBase with RepositoryService =>
private def authenticate(action: (RepositoryInfo) => Any) = {
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
getRepository(paths(0), paths(1)).map { repository =>
if(!repository.repository.isPrivate){
action(repository)
} else {
@@ -145,7 +145,7 @@ trait ReadableUsersAuthenticator { self: ControllerBase with RepositoryService =
private def authenticate(action: (RepositoryInfo) => Any) = {
{
defining(request.paths){ paths =>
getRepository(paths(0), paths(1), baseUrl).map { repository =>
getRepository(paths(0), paths(1)).map { repository =>
context.loginAccount match {
case Some(x) if(x.isAdmin) => action(repository)
case Some(x) if(!repository.repository.isPrivate) => action(repository)

View File

@@ -1,8 +1,6 @@
package gitbucket.core.util
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.treewalk.TreeWalk
import scala.util.control.Exception._
import scala.language.reflectiveCalls
@@ -31,12 +29,6 @@ object ControlUtil {
git2.getRepository.close()
}
def using[T](revWalk: RevWalk)(f: RevWalk => T): T =
try f(revWalk) finally revWalk.release()
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
def ignore[T](f: => Unit): Unit = try {
f
} catch {

View File

@@ -75,6 +75,11 @@ object Implicits {
def gitRepositoryPath: String = request.getRequestURI.replaceFirst("^/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(session: HttpSession){

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