Compare commits

...

226 Commits
3.0 ... 3.4

Author SHA1 Message Date
Naoki Takezoe
c4a682773c Fix warnings 2015-06-27 10:18:22 +09:00
Naoki Takezoe
07e1873cb3 Update release.md 2015-06-27 10:14:30 +09:00
Naoki Takezoe
14d0c4dc32 GitBucket 3.4 release 2015-06-27 10:12:25 +09:00
Naoki Takezoe
19f79b6e54 Merge pull request #789 from milligramme/patch-1
modify css: hide href content on printing
2015-06-26 17:47:31 +09:00
Naoki Takezoe
e0b9fc9481 (refs #788)Fix NotFound in comparing with sibling repository 2015-06-26 17:06:33 +09:00
milligramme
16c28ad938 modify css: hide href content on printing
on printing, href content appeared. 
it is caused by bootstrap.css

faae237ac5/src/main/webapp/assets/vendors/bootstrap/css/bootstrap.css (L183-L185)
2015-06-26 15:31:15 +09:00
Naoki Takezoe
0e5af9ffa9 Update README.md 2015-06-23 14:29:32 +09:00
Naoki Takezoe
a0f6f03d4e Add IntelliJ IDEA logo 2015-06-23 14:18:05 +09:00
Naoki Takezoe
f4cdfc5f32 (refs #769)go-import meta tag is generated for only public repos 2015-06-10 23:18:35 +09:00
Naoki Takezoe
b45988705b (refs #769)Add go-import meta tag 2015-06-10 23:14:12 +09:00
Naoki Takezoe
dd50239759 (refs #780)Not add an issue refer comment if it already exists 2015-06-09 10:32:58 +09:00
Naoki Takezoe
7297a07466 (refs #751)Fix issue reference in verbatim node 2015-06-09 02:24:45 +09:00
Naoki Takezoe
e5fa43f91c Merge pull request #778 from team-lab/fix/diff-ignore-space
fix diff ignore space
2015-06-08 13:22:25 +09:00
nazoking
88080c8d02 fix diff ignore space 2015-06-08 01:33:22 +09:00
Naoki Takezoe
d38dfee540 Remove empty lines 2015-06-07 22:38:03 +09:00
Naoki Takezoe
4da6a7b6f9 Merge pull request #768 from Mura-Mi/avoid-npe-masterless
Avoid NPE when master branch does not exist
2015-06-06 03:34:34 +09:00
Naoki Takezoe
957dfeed6d (refs #775)Move isRenderable() to helpers from PluginRepository 2015-06-05 01:34:23 +09:00
Naoki Takezoe
aa5a07b98e (refs #775)Add new extension point to add markup render 2015-06-05 01:30:00 +09:00
Naoki Takezoe
fd1ee07297 Provides declarative style plug-in definition. 2015-06-03 02:18:07 +09:00
Naoki Takezoe
07026125a9 Merge remote-tracking branch 'origin/master' 2015-06-02 01:06:21 +09:00
Naoki Takezoe
badb747d24 (refs #773)Fix url processing in the blame view 2015-06-02 01:06:02 +09:00
Naoki Takezoe
e27c20d600 Merge pull request #771 from bfritz/fix-release-typo
Fix typo in release.md
2015-06-01 08:27:26 +09:00
Naoki Takezoe
eb46c94c7c Merge pull request #772 from xuwei-k/fix-travis-fail
fix .travis.yml
2015-06-01 07:26:16 +09:00
xuwei-k
2e58f82a61 fix .travis.yml
```
java.lang.RuntimeException: Setting value cannot be null: {file:/home/travis/build/takezoe/gitbucket/}gitbucket/*:version
	at scala.sys.package$.error(package.scala:27)
	at sbt.EvaluateSettings$INode.setValue(INode.scala:143)
	at sbt.EvaluateSettings$MixedNode.evaluate0(INode.scala:175)
```
2015-06-01 04:04:45 +09:00
Brad Fritz
df28013b18 Fix typo in release.md 2015-05-31 11:53:08 -04:00
Naoki Takezoe
8f4e285974 Update README.md 2015-05-31 18:37:12 +09:00
Naoki Takezoe
08c4eeeefe (refs #770)Update release.md 2015-05-31 17:33:36 +09:00
Naoki Takezoe
1fdc2c629c (refs #770)Add set assembly invocation to deploy-assembly-jar.sh 2015-05-31 17:33:21 +09:00
Naoki Takezoe
a6767a1c3d (refs #770)Gather release scripts 2015-05-31 16:58:51 +09:00
Naoki Takezoe
c539d6b643 Update build.xml 2015-05-31 12:21:07 +09:00
Naoki Takezoe
80c3ffdfd7 Update release.md 2015-05-31 12:18:19 +09:00
Naoki Takezoe
f80f2106fe Fix deploy script 2015-05-31 11:58:02 +09:00
Naoki Takezoe
cbfae66882 Fix version 2015-05-31 11:52:15 +09:00
Naoki Takezoe
8a5014fcbe Update release.md 2015-05-31 11:49:35 +09:00
Naoki Takezoe
54d1bff213 Bump version to 3.3 2015-05-31 11:44:01 +09:00
Naoki Takezoe
0075664b9a Merge pull request #764 from team-lab/feature/file-finder-button-on-blob-view
add file finder button on blob view.
2015-05-31 11:26:28 +09:00
Mura-Mi
a2c26f0f2c Avoid NPE when master branch does not exist 2015-05-29 01:28:11 +09:00
nazoking
45f41a13e4 add file finder button on blob view. 2015-05-27 13:59:36 +09:00
Naoki Takezoe
faae237ac5 Update README.md 2015-05-26 01:56:57 +09:00
Naoki Takezoe
e01758e74c Update README.md 2015-05-25 23:29:20 +09:00
Naoki Takezoe
db7dd31c79 Merge branch 'sapk-fix-mobile' 2015-05-25 22:57:39 +09:00
Naoki Takezoe
7d7ac5e2be Merge branch 'fix-mobile' of https://github.com/sapk/gitbucket into sapk-fix-mobile
# Conflicts:
#	src/main/webapp/assets/common/css/gitbucket.css
2015-05-25 22:57:24 +09:00
Naoki Takezoe
577016a33f (refs #762)Add meta-tag to disable IE backward compatibility mode 2015-05-25 22:12:12 +09:00
Naoki Takezoe
fdf2102923 (refs #763)Don't remove disabled user's data and repositories. 2015-05-25 22:03:40 +09:00
Naoki Takezoe
8b47e57be0 Merge branch 'team-lab-featuer/image-on-diff' 2015-05-25 02:06:36 +09:00
Naoki Takezoe
8dad6b64b0 Merge branch 'featuer/image-on-diff' of https://github.com/team-lab/gitbucket into team-lab-featuer/image-on-diff
# Conflicts:
#	src/main/webapp/assets/common/js/gitbucket.js
2015-05-25 02:06:14 +09:00
Naoki Takezoe
05d36abdab Merge branch 'team-lab-feature/blame' 2015-05-25 02:00:49 +09:00
Naoki Takezoe
04e31c5b4f Merge branch 'feature/blame' of https://github.com/team-lab/gitbucket into team-lab-feature/blame
# Conflicts:
#	src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
#	src/main/webapp/assets/common/css/gitbucket.css
2015-05-25 02:00:27 +09:00
Naoki Takezoe
6470428a85 Merge pull request #696 from team-lab/feature/file-finder
add file finder
2015-05-25 01:49:45 +09:00
Naoki Takezoe
a7efb3989a Merge pull request #760 from sugamasao/add-pre-tag-horizontal-scroll
Add pre-tag horizontal scroll on markdown
2015-05-21 20:28:24 +09:00
sugamasao
32072d0bbf Add pre-tag horizontal scroll on markdown 2015-05-20 18:22:53 +09:00
Antoine GIRARD
78a3e4454d Fix mobile view 2015-05-18 21:37:07 +02:00
Naoki Takezoe
e576178e1e Merge pull request #758 from sugamasao/fix-height-of-diff-line
`_` is hidden by diff line
2015-05-17 00:26:19 +09:00
Naoki Takezoe
0f8bc2b03d Merge pull request #755 from tomomura/fix/api-user-login-value
fix login value for Webhook and API.
2015-05-17 00:25:32 +09:00
sugamasao
43565458d4 Fix fix height of diff line
`_` is hidden by diff line
2015-05-15 15:37:36 +09:00
tomomura
71cc3be6d5 fix login value. 2015-05-13 11:46:02 +09:00
Naoki Takezoe
9a8eef7b19 (refs #738)Set "application/octet-stream" as Content-Type to avoid automatic charset detection for text files in Scalatra 2015-05-10 17:36:26 +09:00
Naoki Takezoe
a08c4368b7 Revert dependency 2015-05-10 16:32:21 +09:00
Naoki Takezoe
3b456b2aab Move deploy-assembly-jar script 2015-05-10 15:36:27 +09:00
nazoking
31559418ba add image diff 2015-05-09 01:50:51 +09:00
Naoki Takezoe
692a6e43bc Update release.md 2015-05-07 11:30:41 +09:00
Naoki Takezoe
af4cce654c Update release.md 2015-05-07 11:29:41 +09:00
Naoki Takezoe
c63b02fd4a Move ERD and SVG file to /doc from /etc 2015-05-07 11:22:27 +09:00
Naoki Takezoe
e6974b6e51 Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-05-07 11:14:25 +09:00
Naoki Takezoe
da3b7dbeff Improve assembly-jar deploying 2015-05-07 11:14:17 +09:00
Naoki Takezoe
9737bd7012 (refs #739)Update document 2015-05-07 11:08:11 +09:00
Naoki Takezoe
55fd8e5e2d (refs #739)Update document 2015-05-07 11:06:32 +09:00
Naoki Takezoe
9c4f181d93 (refs #739)Document for release operation 2015-05-07 10:55:59 +09:00
Naoki Takezoe
374342cfc1 Merge pull request #718 from team-lab/feature/api-add-issue-urls
Feature/api add issue urls
2015-05-06 21:19:59 +09:00
Naoki Takezoe
6fbdd237d1 Merge pull request #731 from ndarilek/master
Implement additional accessibility fixes
2015-05-06 10:46:13 +09:00
Naoki Takezoe
4e78f01a09 Dispose ObjectLoader certainty 2015-05-06 10:30:53 +09:00
Naoki Takezoe
8853264808 Merge pull request #744 from jochembroekhoff/master
jQuery updated to v1.11.1
2015-05-06 09:09:07 +09:00
Jochem Broekhoff
6db9b8038f jQuery updated to v1.11.1 2015-05-05 09:59:23 +02:00
Naoki Takezoe
c73e89ccd4 Update version to 3.2.0 2015-05-03 01:45:43 +09:00
Naoki Takezoe
f58a506780 Define Version 3.2 2015-05-03 01:26:35 +09:00
Naoki Takezoe
411d19e74e Update README.md 2015-05-03 01:25:55 +09:00
Naoki Takezoe
bd51ffd9d2 (refs #660)Register Scalatra error callback to rollback transaction 2015-05-03 01:14:22 +09:00
Naoki Takezoe
83980fdccd (refs #534)Allow to download large file 2015-05-02 23:09:18 +09:00
Naoki Takezoe
baab243bc8 Merge pull request #716 from expf/fallback_message
Fix style of message for browsers that do not support drag'n'drop file u...
2015-04-30 19:36:09 +09:00
Naoki Takezoe
4dd8e1dc63 (refs #707)Exclude disabled users from completion list 2015-04-30 16:19:04 +09:00
shimamoto
0b11b8b084 Merge pull request #732 from takezoe/delete_old_activity
(refs #441) Add limit of activity log configuration
2015-04-29 23:16:10 +09:00
Naoki Takezoe
704775dc60 (refs #441)Fix delete condition 2015-04-29 20:19:20 +09:00
Naoki Takezoe
c7a7be1de0 (refs #441)Fix testcase 2015-04-29 15:26:14 +09:00
Naoki Takezoe
e21a970977 (refs #441)Add limit of activity log configuration 2015-04-29 12:57:57 +09:00
Naoki Takezoe
a526dcf2dd Separate AutoUpdate from InitializeListener 2015-04-29 09:37:29 +09:00
Nolan Darilek
a7b48d63e4 Add autofocus to forms at strategic places. 2015-04-28 13:16:43 -05:00
Nolan Darilek
3677906e95 Label additional icons. 2015-04-28 13:11:50 -05:00
Naoki Takezoe
e86710fbbd Merge pull request #728 from kaakaa/testhook-npe
fix test hook error
2015-04-28 02:21:18 +09:00
kaakaa
73e850493a fix test hook error 2015-04-25 16:10:52 -04:00
Naoki Takezoe
7d735f6f8a Merge pull request #725 from sebastiancadena/patch-1
Fix typo
2015-04-24 19:41:11 +09:00
Juan Sebastian Cadena
043e99a9eb Fix typo
Repositories
2015-04-23 19:24:47 -05:00
Naoki Takezoe
65e079b1d3 Merge pull request #722 from ndarilek/master
Label unlabelled icons for accessibility.
2015-04-24 01:58:15 +09:00
Naoki Takezoe
32e6f584d8 Merge pull request #723 from team-lab/fix/#721-npe-getFileList-multi-origin-subfolder
(refs #721)NullPointerException on getFileList
2015-04-24 01:57:01 +09:00
nazoking
ebc5219ce6 (refs #721)NullPointerException on getFileList if branch has multi-origin and get sub-folder file list 2015-04-22 13:45:31 +09:00
Nolan Darilek
ad8e620bdf Label unlabelled icons for accessibility. 2015-04-21 17:38:19 -05:00
Naoki Takezoe
8590c693b9 (refs #715)Escape $ in replacement string 2015-04-22 01:20:11 +09:00
nazoking
5568acc5f3 add html_url on api issue-comment 2015-04-20 14:32:16 +09:00
nazoking
c467594199 add html_url and comments_url on api issue 2015-04-20 13:19:17 +09:00
Kohei Ichioka
8b29bf7d93 Fix style of message for browsers that do not support drag'n'drop file uploads 2015-04-20 10:24:41 +09:00
Naoki Takezoe
43b7f83082 (refs #713)Fix anchor to headline in Wiki 2015-04-20 00:24:49 +09:00
Naoki Takezoe
9ef5c981ef (refs #666)Remove html5shiv.js reference 2015-04-19 16:49:11 +09:00
Naoki Takezoe
f027ac34d4 Merge pull request #697 from banjun/fix-basicauth-for-public-repo
let non-anonymous user read access to public repos with basic auth
2015-04-16 02:13:33 +09:00
Naoki Takezoe
2ea31d6869 Revert "Fix basic auth read when anon access disabled"
This reverts commit 5f3d6242fd.
2015-04-16 02:12:40 +09:00
Naoki Takezoe
a1af7d0f9c Merge pull request #702 from marklacroix/no-anon-basic-auth-read
Fix basic auth read when anon access disabled
2015-04-16 02:05:50 +09:00
Naoki Takezoe
797bf37bfa Merge pull request #705 from team-lab/fix/word-diff-probrem
fix word-diff javascript error
2015-04-16 01:52:54 +09:00
Shintaro Murakami
7f78815c11 (refs #712) Fix problems of showing inline notes. 2015-04-13 19:31:15 +09:00
Naoki Takezoe
d8a3f308ed Merge pull request #709 from jparound30/fix_db_conn_handling
Fix db connection error in dev mode
2015-04-11 22:07:42 +09:00
jparound30
74e18a982d Fix db connection error in dev mode
Changes to close the no longer needed DB connections.
2015-04-11 13:32:29 +09:00
nazoking
e86ad423c5 fix word-diff javascript error (error occured if dictionary contains the word 'hasOwnProperty') 2015-04-09 22:18:46 +09:00
Naoki Takezoe
2f00060c57 Merge pull request #699 from rlazoti/fix-notifier
Fix issue notifications
2015-04-09 20:07:11 +09:00
Naoki Takezoe
3b7e6aa68e Merge pull request #701 from team-lab/fix/api-issue-user
fix some api user is invalid, and add content-type on webhook
2015-04-09 02:27:16 +09:00
Mark LaCroix
5f3d6242fd Fix basic auth read when anon access disabled
Basic authentication fails when not updating a public repository
with the "Disable Anonymous Access" option enabled
2015-04-08 11:46:44 -04:00
nazoking
6793a86bae fix some api user is invalid
issue.userName -> issue.openedUserName
  issueComment.userName -> issueComment.commentedUserName
2015-04-08 20:16:47 +09:00
Rodrigo Lazoti
d99ce20529 fix issue notifications 2015-04-07 10:56:52 -03:00
banjun
93e7b604cd let non-anonymous user read access to public repos with basic auth 2015-04-06 22:15:17 +09:00
nazoking
59c18056fc add file finder 2015-04-06 21:22:03 +09:00
Naoki Takezoe
5c81ce9b68 Add SystemSettings to Plugin arguments 2015-04-05 22:46:48 +09:00
Naoki Takezoe
c9cf62701f Merge pull request #690 from noc06140728/fix-basic-auth
Allow a password that contains colons
2015-04-05 17:04:43 +09:00
Naoki Takezoe
f65babca4b Merge branch 'team-lab-feature/#422-compare-link-near-branch-selector' 2015-04-05 16:53:09 +09:00
Naoki Takezoe
23c146fc5d Merge branch 'feature/#422-compare-link-near-branch-selector' of https://github.com/team-lab/gitbucket into team-lab-feature/#422-compare-link-near-branch-selector
Conflicts:
	src/main/twirl/gitbucket/core/repo/files.scala.html
2015-04-05 16:52:47 +09:00
Naoki Takezoe
e14f336142 Merge pull request #694 from team-lab/feature/#536-Commits-view-on-directory
Browse history for directory
2015-04-05 16:46:20 +09:00
nazoking
54647be5bd (#536) Browse commits for directory 2015-04-04 11:13:07 +09:00
Naoki Takezoe
9517d65646 Update README.md 2015-04-04 02:55:15 +09:00
Naoki Takezoe
0156e401fa Update README.md for 3.1.1 release 2015-04-04 02:39:25 +09:00
Naoki Takezoe
d6e2bc464d Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-04-03 22:39:29 +09:00
Naoki Takezoe
5124ff593d Update version number to 3.1.1 2015-04-03 22:39:17 +09:00
Naoki Takezoe
727c90afdc Merge pull request #681 from team-lab/feature/improve-file-list-performance
Feature/improve file list performance
2015-04-03 22:32:51 +09:00
Naoki Takezoe
9ebd5e3265 Merge pull request #691 from team-lab/fix/#684-api-contextpath-support
fix/api context-path support
2015-04-03 22:31:09 +09:00
nazoking
d120142127 (ref #684)fix api context-path support 2015-04-02 16:53:30 +09:00
Naoki Takezoe
f9e4cddcaf Update version number to 3.1.1 2015-04-02 11:10:29 +09:00
Naoki Takezoe
8f64f174d9 Rolled back H2 to 1.4.180 from 1.4.186 2015-04-02 11:08:44 +09:00
Masahiro Namba
d6817796b3 Allow a password that contains colons
Allow a password that contains colons in the basic autentication.
2015-04-01 02:11:05 +09:00
nazoking
ab19d473c4 (refs #442)show compare/pull request link near branch link. 2015-03-30 21:16:42 +09:00
Naoki Takezoe
48f0116358 Fix compilation error 2015-03-29 03:37:48 +09:00
Naoki Takezoe
d8e6e97845 Pass ServletContext to Plugin 2015-03-29 03:26:06 +09:00
Naoki Takezoe
9a8920788c Update gutbucket version to 3.1.0 2015-03-29 02:27:28 +09:00
nazoking
27864a3a3c Performance Improve for getFileList 2015-03-29 01:21:10 +09:00
nazoking
b39e863591 add test for getFileList 2015-03-29 01:12:33 +09:00
nazoking
d8d18ed25c Merge branch 'master' into feature/blame
Conflicts:
	src/main/scala/gitbucket/core/util/JGitUtil.scala
2015-03-28 21:19:40 +09:00
Naoki Takezoe
7661e8cadd Update assembly jar version to 3.1.0 2015-03-28 17:24:09 +09:00
Naoki Takezoe
7d3c7a0c61 Update README.md for 3.1 release 2015-03-28 17:05:11 +09:00
Naoki Takezoe
7375ff9f97 (refs #677)Fix Fork button for non group users 2015-03-28 17:01:41 +09:00
Naoki Takezoe
5df6ec8985 Merge pull request #680 from team-lab/fix/#679-jquery-hashchange-plugin-for-jquery1.9
fix #679 jquery-hashchange-plugin for jquery 1.9
2015-03-28 03:27:09 +09:00
nazoking
83fd2648f5 follow rename 2015-03-27 21:19:02 +09:00
nazoking
8e81758941 fix link 2015-03-27 21:06:12 +09:00
nazoking
41a6a29771 fix for ie 7,8,9 2015-03-27 21:03:41 +09:00
nazoking
9a8479ee58 fix #679 jquery-hashchange-plugin for jquery 1.9 2015-03-27 16:11:39 +09:00
Naoki Takezoe
73766f11eb (refs #664)Normalize line separator and empty line 2015-03-25 20:56:17 +09:00
Naoki Takezoe
a22878e2c5 (refs #671)Use servletPath instead of requestURI 2015-03-25 19:47:13 +09:00
Naoki Takezoe
1a2eb9d1e7 Merge pull request #676 from team-lab/#672-fix-blank-diff-output
#672 fix blank diff output
2015-03-25 13:00:20 +09:00
nazoking
277ace3c8e fix for ie 5 2015-03-25 10:21:40 +09:00
nazoking
40f376dbd9 fix for ie 7 2015-03-25 10:20:59 +09:00
nazoking
444af0935e fix regression: blank diff output #672 2015-03-25 09:36:01 +09:00
Tomofumi Tanaka
fb15fa0e43 Fix jetty.version in build.xml 2015-03-24 23:55:45 +09:00
Tomofumi Tanaka
bcd3e14870 Remove exec permission 2015-03-24 23:55:05 +09:00
Tomofumi Tanaka
c18702dcea Merge remote-tracking branch 'origin/api-support' 2015-03-24 23:40:01 +09:00
Naoki Takezoe
1341ef9c52 Merge pull request #670 from seratch/fixes-for-minor-updates
More fixes for #669
2015-03-24 15:21:39 +09:00
Kazuhiro Sera
f605a8d085 Run ./embed-jetty/update.sh 8.1.16.v20140903 2015-03-24 12:54:45 +09:00
Kazuhiro Sera
e0f7a7a3c6 Add a script to update embed-jetty jars 2015-03-24 12:54:28 +09:00
Kazuhiro Sera
1d72bed442 Bump Scala version to 2.11.6 on Travis builds 2015-03-24 12:50:28 +09:00
Naoki Takezoe
284b8e7c16 Merge pull request #669 from seratch/scalatra-2.3.1
Bump Scalatra to 2.3.1, sbt to 0.13.8
2015-03-24 11:27:43 +09:00
Kazuhiro Sera
ff9fb24094 Bump Scalatra to 2.3.1, sbt to 0.13.8 and upgrade minor version of other dependencies 2015-03-24 08:43:56 +09:00
Naoki Takezoe
fde4448dd0 Fix layout 2015-03-24 06:55:34 +09:00
Shintaro Murakami
d16ce90a3d (refs #649) Fix condition of filtering issues from milestone view. 2015-03-24 00:54:49 +09:00
Naoki Takezoe
3ed5525956 (refs #665)Fix markdown format 2015-03-23 21:42:49 +09:00
Naoki Takezoe
855d1e12aa Rename index.md to readme.md 2015-03-23 21:33:44 +09:00
Naoki Takezoe
e03797a58f Update notification.md 2015-03-23 21:33:01 +09:00
Naoki Takezoe
f0d38cf8ec Update auto_update.md 2015-03-23 21:32:26 +09:00
Naoki Takezoe
2723580e17 Update comment_action.md 2015-03-23 21:31:42 +09:00
Naoki Takezoe
1977aa481d Update mapping_and_validation.md 2015-03-23 21:30:57 +09:00
Naoki Takezoe
4b36a8f831 Update directory.md 2015-03-23 21:30:29 +09:00
Naoki Takezoe
96b56e38ba Update how_to_run.md 2015-03-23 21:29:56 +09:00
Naoki Takezoe
849d117ad3 Update index.md 2015-03-23 21:29:22 +09:00
Naoki Takezoe
8d57fca779 (refs #665)Move Mapping and Validation 2015-03-23 21:28:03 +09:00
Naoki Takezoe
0dc867306b Update index.md 2015-03-23 21:24:46 +09:00
Naoki Takezoe
eefb4c01ec (refs #665)Move developer's docs from Wiki 2015-03-23 21:21:24 +09:00
Naoki Takezoe
ccce499f7f Merge pull request #655 from hho/patch-1
Fix font in line comments
2015-03-22 08:00:56 -07:00
Naoki Takezoe
9f11eaa4d3 Merge pull request #643 from team-lab/feature/diff-ignore-space
Improvement diff
2015-03-22 05:59:16 -07:00
Naoki Takezoe
7b85c0e55f Merge pull request #656 from jparound30/feature_rename_detection
(refs #641) Enable rename detection
2015-03-22 02:23:31 -07:00
Tomofumi Tanaka
7e92f1abd5 Fix test code package name 2015-03-16 23:38:00 +09:00
Tomofumi Tanaka
825f2518e9 Rename migration sql 2015-03-16 23:23:59 +09:00
Tomofumi Tanaka
def1e877db Move api package to new rule 2015-03-16 23:22:56 +09:00
Tomofumi Tanaka
6acbd5b2cf Merge remote-tracking branch 'origin/master' into api-support
Conflicts:
	src/main/scala/ScalatraBootstrap.scala
	src/main/scala/gitbucket/core/controller/AccountController.scala
	src/main/scala/gitbucket/core/controller/ControllerBase.scala
	src/main/scala/gitbucket/core/controller/IssuesController.scala
	src/main/scala/gitbucket/core/controller/PullRequestsController.scala
	src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala
	src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala
	src/main/scala/gitbucket/core/model/Profile.scala
	src/main/scala/gitbucket/core/service/PullRequestService.scala
	src/main/scala/gitbucket/core/service/WebHookService.scala
	src/main/scala/gitbucket/core/servlet/InitializeListener.scala
	src/main/scala/gitbucket/core/view/helpers.scala
	src/main/twirl/gitbucket/core/pulls/conversation.scala.html
	src/main/twirl/gitbucket/core/pulls/mergeguide.scala.html
	src/main/twirl/issues/listparts.scala.html
2015-03-16 22:49:47 +09:00
tanacasino
73b7aef4a9 Merge pull request #635 from team-lab/api-support
Api support for JENKINS GitHub pull request builder plugin
2015-03-16 00:34:47 +09:00
nazoking
3d73e3922b (api-support)fix typo 2015-03-16 00:14:52 +09:00
nazoking
224e44151f (api-support)change crlf 2015-03-16 00:04:30 +09:00
Naoki Takezoe
d9c1293985 (refs #653)Sort tags by date 2015-03-15 11:38:45 +09:00
jparound30
849a40d4b5 (refs #641) Enable rename detection 2015-03-15 09:37:56 +09:00
Henning Hoefer
177387e9b0 Fix font in line comments
Line comments on a diff were rendered in the browsers default font
2015-03-13 15:04:06 +01:00
Naoki Takezoe
cacce54714 (refs #644)Fix authentication for Git access via HTTP 2015-03-13 01:58:04 +09:00
Naoki Takezoe
12082322ee (refs #646)Add autocomplete="true" to password field 2015-03-10 11:34:02 +09:00
Naoki Takezoe
7e0a5b7fec (refs #645)Fix disableByNotYourSelf validation 2015-03-10 11:31:17 +09:00
nazoking
d2cf4afc81 add diff stat bar 2015-03-10 00:54:57 +09:00
nazoking
3e0a50926f resolve gravatar on blame 2015-03-09 23:42:04 +09:00
Naoki Takezoe
cce62de075 (refs #642)Fix markdown rendering style 2015-03-09 19:18:38 +09:00
nazoking
d9450df7e9 more github like style 2015-03-08 21:35:46 +09:00
nazoking
41fc81fab6 add filename for syntax highlight hint 2015-03-08 21:35:39 +09:00
nazoking
aa35498bdd Implements ignore-whitespace and hilight-syntax on diff.
Improvement experience speed
 * lazy diff rendering ( it is effective if tha diff has a lot of files ).
 * some feature implemented by javascript, to implement by css.
 * some javascript event handlers on each elements move to on parent elements.
2015-03-08 19:49:20 +09:00
Naoki Takezoe
14becd0bd6 Merge pull request #640 from rlazoti/fix-plugin
fix javascript getter for plug-in system
2015-03-05 02:00:27 +09:00
Rodrigo Lazoti
7390e21934 fix javascript getter for plug-in system 2015-03-03 18:07:25 -03:00
nazoking
e408eb43bb fix parent. only path exists 2015-03-04 03:37:46 +09:00
nazoking
dc0aa0851e implove source-line-num performance.
and Stop scroll when click line number.
2015-03-04 03:14:22 +09:00
nazoking
51d7c43489 add blame 2015-03-04 01:52:07 +09:00
nazoking
1e6d26221d show commit status on pull-request-list view 2015-03-02 03:12:26 +09:00
nazoking
44a8e98c7b add rete_limit api(disabled message only) 2015-03-02 03:12:25 +09:00
nazoking
415519716e show error as json 2015-03-02 03:12:24 +09:00
nazoking
597f86dc7b add webhook pull_request synchronize action 2015-03-02 03:12:23 +09:00
nazoking
579ed19949 move api classes to api package 2015-03-02 03:12:22 +09:00
nazoking
9221bfa045 create merge status cache as branch refs/pull/{issueId}/{merge,conflict} 2015-03-02 03:12:21 +09:00
nazoking
3057a31a6c add test for Diretory (set GitBucketHome when test scope) 2015-03-02 03:12:20 +09:00
nazoking
d47ccf587c Authentication move to filter 2015-03-02 03:12:19 +09:00
nazoking
3e78d423ac (refs #630) Fix bug on changing issues status. 2015-03-02 03:12:18 +09:00
nazoking
0299cee5ec add CommitStatus api and views. 2015-03-02 03:12:17 +09:00
nazoking
97ceffe689 add CommitStatus table and model and service. 2015-03-02 03:12:11 +09:00
nazoking
83bcbef6ce move json extract logic to ControllerBase 2015-02-27 13:40:08 +09:00
nazoking
47cb4d1c49 add some WebAPI/v3.
* [Users](https://developer.github.com/v3/users/) get-a-single-user
 * [Issues/Comments](https://developer.github.com/v3/issues/comments/) list-comments-on-an-issue, create-a-comment
 * [Pull Requests](https://developer.github.com/v3/pulls/) list-pull-requests, get-a-single-pull-request, list-commits-on-a-pull-request
 * [Repositories](https://developer.github.com/v3/repos/) get
2015-02-27 13:40:07 +09:00
nazoking
32799cead7 more github like 2015-02-27 13:37:55 +09:00
nazoking
dbedc2166b fix typo 2015-02-27 13:37:54 +09:00
nazoking
e86b404ca2 date time format to iso-8601 ( fit The GitHub spec ) 2015-02-27 13:37:53 +09:00
nazoking
321a3a72f0 call web hook on issue or pull_request both opened or closed. and issue_comment. 2015-02-27 13:37:52 +09:00
nazoking
b512e7c390 make WebHookPullRequestPayload 2015-02-27 13:37:51 +09:00
nazoking
ae7ead6272 Add "X-Github-Event" header, and change interface. 2015-02-27 13:37:50 +09:00
nazoking
db55719a6f fix typo 2015-02-27 13:37:49 +09:00
nazoking
4e1094e75b add test 2015-02-27 13:37:48 +09:00
nazoking
3fd97662f5 Add Authorization logic to Controller 2015-02-27 13:37:47 +09:00
nazoking
d6946b93c3 Add access token table and manage ui. 2015-02-27 13:37:46 +09:00
151 changed files with 12743 additions and 6988 deletions

View File

@@ -1,3 +1,5 @@
language: scala
scala:
- 2.11.2
sudo: false
script:
- . env.sh
- sbt test

View File

@@ -54,20 +54,22 @@ For Installation on Windows Server with IIS see [this wiki page](https://github.
### 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.
```
$ 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
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, copy the [gitbucket.plist](https://raw.github.com/takezoe/gitbucket/master/contrib/macosx/gitbucket.plist) file to `~/Library/LaunchAgents/`
@@ -79,6 +81,32 @@ Run the following commands in `Terminal` to
Release Notes
--------
### 3.4 - 27 Jun 2015
- Declarative style plug-in definition
- New extension point to add markup render
- go-import support
### 3.3 - 31 May 2015
- Rich graphical diff for images
- File finder is available in the repository viewer
- Blame is displayed at the source viewer
- Remain user data and repositories even if user is disabled
- Mobile view improvement
### 3.2 - 3 May 2015
- Directory history button
- Compare / pull request button
- Limit of activity log
### 3.1.1 - 4 Apr 2015
- Rolled back H2 version to avoid version compatibility issue
- Plug-ins became possible to access ServletContext
### 3.1 - 28 Mar 2015
- Web APIs for Jenkins github pull-request builder
- Improved diff view
- Bump Scalatra to 2.3.1, sbt to 0.13.8
### 3.0 - 3 Mar 2015
- New plug-in system is available
- Connection pooling by c3p0
@@ -265,3 +293,7 @@ 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/)

22
doc/activity.md Normal file
View File

@@ -0,0 +1,22 @@
Activity Timeline
========
GitBucket records several types of user activity to ```ACTIVITY``` table. Activity types are shown below:
type | message | additional information
------------------|------------------------------------------------------|------------------------
create_repository |$user created $owner/$repo |-
open_issue |$user opened issue $owner/$repo#$issueId |-
close_issue |$user closed issue $owner/$repo#$issueId |-
close_issue |$user closed pull request $owner/$repo#$issueId |-
reopen_issue |$user reopened issue $owner/$repo#$issueId |-
comment_issue |$user commented on issue $owner/$repo#$issueId |-
comment_issue |$user commented on pull request $owner/$repo#$issueId |-
create_wiki |$user created the $owner/$repo wiki |$page
edit_wiki |$user edited the $owner/$repo wiki |$page<br>$page:$commitId(since 1.5)
push |$user pushed to $owner/$repo#$branch to $owner/$repo |$commitId:$shortMessage\n*
create_tag |$user created tag $tag at $owner/$repo |-
create_branch |$user created branch $branch at $owner/$repo |-
delete_branch |$user deleted branch $branch at $owner/$repo |-
fork |$user forked $owner/$repo to $owner/$repo |-
open_pullreq |$user opened pull request $owner/$repo#issueId |-
merge_pullreq |$user merge pull request $owner/$repo#issueId |-

37
doc/auto_update.md Normal file
View File

@@ -0,0 +1,37 @@
Automatic Schema Updating
========
GitBucket uses H2 database to manage project and account data. GitBucket updates database schema automatically in the first run after the upgrading.
To release a new version of GitBucket, add the version definition to the [servlet.AutoUpdate](https://github.com/takezoe/gitbucket/blob/master/src/main/scala/servlet/AutoUpdateListener.scala) at first.
```scala
object AutoUpdate {
...
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
Version(1, 0)
)
...
```
Next, add a SQL file which updates database schema into [/src/main/resources/update/](https://github.com/takezoe/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
We can also add any Scala code for upgrade GitBucket which modifies esources other than database. Override ```Version.update``` like below:
```scala
val versions = Seq(
new Version(1, 3){
override def update(conn: Connection): Unit = {
super.update(conn)
// Add any code here!
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0)
)
```

48
doc/comment_action.md Normal file
View File

@@ -0,0 +1,48 @@
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.
To determine if it was any operation, you see the ```ACTION``` column.
|ACTION|
|--------|
|comment|
|close_comment|
|reopen_comment|
|close|
|reopen|
|commit|
|merge|
|delete_branch|
|refer|
#####comment
This value is saved when users have made a normal comment.
#####close_comment, reopen_comment
These values are saved when users have reopened or closed the issue with comments.
#####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.
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.
This comment is displayed. But it can not be edited by all users, and also not counted as a comment.
#####merge
This value is saved when users have merged the pull request.
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
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.
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```.

44
doc/directory.md Normal file
View File

@@ -0,0 +1,44 @@
Directory Structure
========
GitBucket persists all data into __HOME/.gitbucket__ in default (In 1.9 or before, HOME/gitbucket is default).
This directory has following structure:
```
* /HOME/gitbucket
* /repositories
* /USER_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)
* /data
* /USER_NAME
* /files
* avatar.xxx (image file of user avatar)
* /plugins
* /PLUGIN_NAME
* plugin.js
* /tmp
* /_upload
* /SESSION_ID (removed at session timeout)
* current time millis + random 10 alphanumeric chars (temporary file for file uploading)
* /USER_NAME
* /init-REPO_NAME (used in repository creation and removed after it) ... unused since 1.8
* /REPO_NAME.wiki (working directory for wiki repository) ... unused since 1.8
* /REPO_NAME
* /download (temporary directories are created under this directory)
```
There are some ways to specify the data directory instead of the default location.
1. Environment variable __GITBUCKET_HOME__
2. System property __gitbucket.home__ (e.g. ```-Dgitbucket.home=PATH_TO_DATADIR```)
3. Command line option for embedded Jetty (e.g. ```java -jar gitbucket.war --data=PATH_TO_DATADIR```)
4. Context parameter __gitbucket.home__ in web.xml like below:
```xml
<context-param>
<param-name>gitbucket.home</param-name>
<param-value>PATH_TO_DATADIR</param-value>
</context-param>
```

38
doc/how_to_run.md Normal file
View File

@@ -0,0 +1,38 @@
How to run from the source tree
========
for Testers
--------
If you want to test GitBucket, input following command at the root directory of the source tree.
```
C:\gitbucket> sbt ~container: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:
```
C:\gitbucket> sbt
...
> container:start
...
> ~ ;copy-resources;aux-compile
```
Build war file
--------
To build war file, run the following command:
```
C:\gitbucket> sbt package
```
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
To build executable war file, run Ant 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. Please note the current build.xml works on Windows only. Replace `sbt.bat` with `sbt.sh` in build.xml if you want to run it on Linux.

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

23
doc/notification.md Normal file
View File

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

11
doc/readme.md Normal file
View File

@@ -0,0 +1,11 @@
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)
* [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)

50
doc/release.md Normal file
View File

@@ -0,0 +1,50 @@
Release Operation
========
Update version number
--------
Note to update version number in files below:
### src/main/scala/gitbucket/core/servlet/AutoUpdate.scala
```scala
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 3), // <---- add this line!!
new Version(3, 2),
```
### env.sh
```bash
#!/bin/sh
export GITBUCKET_VERSION=3.3.0 # <---- update here!!
```
Generate release files
--------
Note: Release operation requires [Ant](http://ant.apache.org/) and [Maven](https://maven.apache.org/).
### Make release war file
Run `release/make-release-war.sh`. The release war file is generated into `target/scala-2.11/gitbucket.war`.
```bash
$ cd release
$ ./make-release-war.sh
```
### Deploy assembly jar file
For plug-in development, we have to publish the assembly jar file to the public Maven repository by `release/deploy-assembly-jar.sh`.
```bash
$ cd release/
$ ./deploy-assembly-jar.sh
```

71
doc/validation.md Normal file
View File

@@ -0,0 +1,71 @@
Mapping and Validation
========
GitBucket uses [scalatra-forms](https://github.com/takezoe/scalatra-forms) to validate request parameters and map them to the scala object. This is inspired by Play2 form mapping / validation.
At first, define the mapping as following:
```scala
import jp.sf.amateras.scalatra.forms._
case class RegisterForm(name: String, description: String)
val form = mapping(
"name" -> text(required, maxlength(40)),
"description" -> text()
)(RegisterForm.apply)
```
The servlet have to mixed in ```jp.sf.amateras.scalatra.forms.ClientSideValidationFormSupport``` to validate request parameters and take mapped object. It validates request parameters before action. If any errors are detected, it throws an exception.
```scala
class RegisterServlet extends ScalatraServlet with ClientSideValidationFormSupport {
post("/register", form) { form: RegisterForm =>
...
}
}
```
In the view template, you can add client-side validation by adding ```validate="true"``` to your form. Error messages are set to ```span#error-<fieldname>```.
```html
<form method="POST" action="/register" validate="true">
Name: <input type="name" type="text">
<span class="error" id="error-name"></span>
<br/>
Description: <input type="description" type="text">
<span class="error" id="error-description"></span>
<br/>
<input type="submit" value="Register"/>
</form>
```
Client-side validation calls ```<form-action>/validate``` to validate form contents. It returns a validation result as JSON. In this case, form action is ```/register```, so ```/register/validate``` is called before submitting a form. ```ClientSideValidationFormSupport``` adds this JSON API automatically.
For Ajax request, you have to use '''ajaxGet''' or '''ajaxPost''' to define action. It almost same as '''get''' or '''post'''. You can implement actions which handle Ajax request as same as normal actions.
Small difference is they return validation errors as JSON.
```scala
ajaxPost("/register", form){ form =>
...
}
```
You can call these actions using jQuery as below:
```javascript
$('#register').click(function(e){
$.ajax($(this).attr('action'), {
type: 'POST',
data: {
name: $('#name').val(),
mail: $('#mail').val()
}
})
.done(function(data){
$('#result').text('Registered!');
})
.fail(function(data, status){
displayErrors($.parseJSON(data.responseText));
});
});
```

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

11
embed-jetty/update.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/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

2
env.sh Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
export GITBUCKET_VERSION=3.4.0

View File

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

14
gitbucket-assembly.iml Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

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

View File

@@ -10,9 +10,9 @@ import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.0.0"
val ScalaVersion = "2.11.2"
val ScalatraVersion = "2.3.0"
val Version = System.getenv("GITBUCKET_VERSION")
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
lazy val project = Project (
"gitbucket",
@@ -42,34 +42,39 @@ object MyBuild extends Build {
),
scalacOptions := Seq("-deprecation", "-language:postfixOps"),
libraryDependencies ++= Seq(
"org.eclipse.jgit" % "org.eclipse.jgit.http.server" % "3.4.1.201406201815-r",
"org.eclipse.jgit" % "org.eclipse.jgit.archive" % "3.4.1.201406201815-r",
"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.10",
"org.json4s" %% "json4s-jackson" % "3.2.11",
"jp.sf.amateras" %% "scalatra-forms" % "0.1.0",
"commons-io" % "commons-io" % "2.4",
"org.pegdown" % "pegdown" % "1.4.1",
"org.apache.commons" % "commons-compress" % "1.5",
"org.apache.commons" % "commons-email" % "1.3.1",
"org.apache.httpcomponents" % "httpclient" % "4.3",
"org.pegdown" % "pegdown" % "1.4.1", // 1.4.2 has incompatible APi changes
"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",
"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.8.v20121106" % "container;provided",
"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.11" % "test",
"junit" % "junit" % "4.12" % "test",
"com.mchange" % "c3p0" % "0.9.5",
"com.typesafe" % "config" % "1.2.1",
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
"com.typesafe.play" %% "twirl-compiler" % "1.0.4",
"com.typesafe.akka" %% "akka-actor" % "2.3.10",
"com.enragedginger" %% "akka-quartz-scheduler" % "1.3.0-akka-2.3.x"
),
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,11 +1,8 @@
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.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.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")

View File

@@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<project name="gitbucket" default="all" basedir=".">
<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="3.0.0"/>
<property name="jetty.version" value="8.1.8.v20121106"/>
<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">

View File

@@ -1,9 +1,15 @@
#!/bin/sh
source ../env.sh
cd ../
./sbt.sh clean assembly
cd release
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
-Dversion=3.0.0\
-Dversion=$GITBUCKET_VERSION\
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-3.0.0.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/

3
release/make-release-war.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
source ../env.sh
ant -f build.xml all

Binary file not shown.

BIN
sbt-launch-0.13.8.jar Normal file

Binary file not shown.

View File

@@ -1,2 +1,2 @@
set SCRIPT_DIR=%~dp0
java -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.5.jar" %*
java -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" %*

3
sbt.sh
View File

@@ -1,2 +1,3 @@
#!/bin/sh
java -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.5.jar "$@"
source ./env.sh
java -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 "$@"

View File

@@ -0,0 +1,42 @@
DROP TABLE IF EXISTS ACCESS_TOKEN;
CREATE TABLE ACCESS_TOKEN (
ACCESS_TOKEN_ID INT NOT NULL AUTO_INCREMENT,
TOKEN_HASH VARCHAR(40) NOT NULL,
USER_NAME VARCHAR(100) NOT NULL,
NOTE TEXT NOT NULL
);
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_PK PRIMARY KEY (ACCESS_TOKEN_ID);
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_FK0 FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE ACCESS_TOKEN ADD CONSTRAINT IDX_ACCESS_TOKEN_TOKEN_HASH UNIQUE(TOKEN_HASH);
DROP TABLE IF EXISTS COMMIT_STATUS;
CREATE TABLE COMMIT_STATUS(
COMMIT_STATUS_ID INT AUTO_INCREMENT,
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
COMMIT_ID VARCHAR(40) NOT NULL,
CONTEXT VARCHAR(255) NOT NULL, -- context is too long (maximum is 255 characters)
STATE VARCHAR(10) NOT NULL, -- pending, success, error, or failure
TARGET_URL VARCHAR(200),
DESCRIPTION TEXT,
CREATOR VARCHAR(100) NOT NULL,
REGISTERED_DATE TIMESTAMP NOT NULL, -- CREATED_AT
UPDATED_DATE TIMESTAMP NOT NULL -- UPDATED_AT
);
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_PK PRIMARY KEY (COMMIT_STATUS_ID);
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_1
UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, CONTEXT);
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK1
FOREIGN KEY (USER_NAME, REPOSITORY_NAME)
REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK2
FOREIGN KEY (USER_NAME) REFERENCES ACCOUNT (USER_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE COMMIT_STATUS ADD CONSTRAINT IDX_COMMIT_STATUS_FK3
FOREIGN KEY (CREATOR) REFERENCES ACCOUNT (USER_NAME)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,13 +1,14 @@
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{TransactionFilter, BasicAuthenticationFilter}
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter}
import gitbucket.core.util.Directory
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
import java.util.EnumSet
import javax.servlet._
import org.scalatra._
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
@@ -16,7 +17,8 @@ class ScalatraBootstrap extends LifeCycle {
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter)
context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*")
context.addFilter("accessTokenAuthenticationFilter", new AccessTokenAuthenticationFilter)
context.getFilterRegistration("accessTokenAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/api/v3/*")
// Register controllers
context.mount(new AnonymousAccessController, "/*")
@@ -45,4 +47,8 @@ class ScalatraBootstrap extends LifeCycle {
dir.mkdirs()
}
}
}
override def destroy(context: ServletContext): Unit = {
Database.closeDataSource()
}
}

View File

@@ -0,0 +1,25 @@
package gitbucket.core.api
import gitbucket.core.model.{Account, CommitState, CommitStatus}
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*/
case class ApiCombinedCommitStatus(
state: String,
sha: String,
total_count: Int,
statuses: Iterable[ApiCommitStatus],
repository: ApiRepository){
// val commit_url = ApiPath(s"/api/v3/repos/${repository.full_name}/${sha}")
val url = ApiPath(s"/api/v3/repos/${repository.full_name}/commits/${sha}/status")
}
object ApiCombinedCommitStatus {
def apply(sha:String, statuses: Iterable[(CommitStatus, Account)], repository:ApiRepository): ApiCombinedCommitStatus = ApiCombinedCommitStatus(
state = CommitState.combine(statuses.map(_._1.state).toSet).name,
sha = sha,
total_count= statuses.size,
statuses = statuses.map{ case (s, a)=> ApiCommitStatus(s, ApiUser(a)) },
repository = repository)
}

View File

@@ -0,0 +1,29 @@
package gitbucket.core.api
import gitbucket.core.model.IssueComment
import gitbucket.core.util.RepositoryName
import java.util.Date
/**
* https://developer.github.com/v3/issues/comments/
*/
case class ApiComment(
id: Int,
user: ApiUser,
body: String,
created_at: Date,
updated_at: Date)(repositoryName: RepositoryName, issueId: Int){
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${issueId}#comment-${id}")
}
object ApiComment{
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser): ApiComment =
ApiComment(
id = comment.commentId,
user = user,
body = comment.content,
created_at = comment.registeredDate,
updated_at = comment.updatedDate)(repositoryName, issueId)
}

View File

@@ -0,0 +1,48 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.api.Git
import java.util.Date
/**
* https://developer.github.com/v3/repos/commits/
*/
case class ApiCommit(
id: String,
message: String,
timestamp: Date,
added: List[String],
removed: List[String],
modified: List[String],
author: ApiPersonIdent,
committer: ApiPersonIdent)(repositoryName:RepositoryName){
val url = ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
val html_url = ApiPath(s"/${repositoryName.fullName}/commit/${id}")
}
object ApiCommit{
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = {
val diffs = JGitUtil.getDiffs(git, commit.id, false)
ApiCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime,
added = diffs._1.collect {
case x if x.changeType == DiffEntry.ChangeType.ADD => x.newPath
},
removed = diffs._1.collect {
case x if x.changeType == DiffEntry.ChangeType.DELETE => x.oldPath
},
modified = diffs._1.collect {
case x if x.changeType != DiffEntry.ChangeType.ADD && x.changeType != DiffEntry.ChangeType.DELETE => x.newPath
},
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(repositoryName)
}
}

View File

@@ -0,0 +1,42 @@
package gitbucket.core.api
import gitbucket.core.api.ApiCommitListItem._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
/**
* https://developer.github.com/v3/repos/commits/
*/
case class ApiCommitListItem(
sha: String,
commit: Commit,
author: Option[ApiUser],
committer: Option[ApiUser],
parents: Seq[Parent])(repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
}
object ApiCommitListItem {
def apply(commit: CommitInfo, repositoryName: RepositoryName): ApiCommitListItem = ApiCommitListItem(
sha = commit.id,
commit = Commit(
message = commit.fullMessage,
author = ApiPersonIdent.author(commit),
committer = ApiPersonIdent.committer(commit)
)(commit.id, repositoryName),
author = None,
committer = None,
parents = commit.parents.map(Parent(_)(repositoryName)))(repositoryName)
case class Parent(sha: String)(repositoryName: RepositoryName){
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}")
}
case class Commit(
message: String,
author: ApiPersonIdent,
committer: ApiPersonIdent)(sha:String, repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/git/commits/${sha}")
}
}

View File

@@ -0,0 +1,38 @@
package gitbucket.core.api
import gitbucket.core.model.CommitStatus
import gitbucket.core.util.RepositoryName
import java.util.Date
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*/
case class ApiCommitStatus(
created_at: Date,
updated_at: Date,
state: String,
target_url: Option[String],
description: Option[String],
id: Int,
context: String,
creator: ApiUser
)(sha: String, repositoryName: RepositoryName) {
val url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/commits/${sha}/statuses")
}
object ApiCommitStatus {
def apply(status: CommitStatus, creator:ApiUser): ApiCommitStatus = ApiCommitStatus(
created_at = status.registeredDate,
updated_at = status.updatedDate,
state = status.state.name,
target_url = status.targetUrl,
description= status.description,
id = status.commitStatusId,
context = status.context,
creator = creator
)(status.commitId, RepositoryName(status))
}

View File

@@ -0,0 +1,5 @@
package gitbucket.core.api
case class ApiError(
message: String,
documentation_url: Option[String] = None)

View File

@@ -0,0 +1,35 @@
package gitbucket.core.api
import gitbucket.core.model.Issue
import gitbucket.core.util.RepositoryName
import java.util.Date
/**
* https://developer.github.com/v3/issues/
*/
case class ApiIssue(
number: Int,
title: String,
user: ApiUser,
// labels,
state: String,
created_at: Date,
updated_at: Date,
body: String)(repositoryName: RepositoryName){
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/issues/${number}")
}
object ApiIssue{
def apply(issue: Issue, repositoryName: RepositoryName, user: ApiUser): ApiIssue =
ApiIssue(
number = issue.issueId,
title = issue.title,
user = user,
state = if(issue.closed){ "closed" }else{ "open" },
body = issue.content.getOrElse(""),
created_at = issue.registeredDate,
updated_at = issue.updatedDate)(repositoryName)
}

View File

@@ -0,0 +1,6 @@
package gitbucket.core.api
/**
* path for api url. if set path '/repos/aa/bb' then, expand 'http://server:post/repos/aa/bb' when converted to json.
*/
case class ApiPath(path: String)

View File

@@ -0,0 +1,25 @@
package gitbucket.core.api
import gitbucket.core.util.JGitUtil.CommitInfo
import java.util.Date
case class ApiPersonIdent(
name: String,
email: String,
date: Date)
object ApiPersonIdent {
def author(commit: CommitInfo): ApiPersonIdent =
ApiPersonIdent(
name = commit.authorName,
email = commit.authorEmailAddress,
date = commit.authorTime)
def committer(commit: CommitInfo): ApiPersonIdent =
ApiPersonIdent(
name = commit.committerName,
email = commit.committerEmailAddress,
date = commit.commitTime)
}

View File

@@ -0,0 +1,59 @@
package gitbucket.core.api
import gitbucket.core.model.{Issue, PullRequest}
import java.util.Date
/**
* https://developer.github.com/v3/pulls/
*/
case class ApiPullRequest(
number: Int,
updated_at: Date,
created_at: Date,
head: ApiPullRequest.Commit,
base: ApiPullRequest.Commit,
mergeable: Option[Boolean],
title: String,
body: String,
user: ApiUser) {
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
val url = ApiPath(s"${base.repo.url.path}/pulls/${number}")
//val issue_url = ApiPath(s"${base.repo.url.path}/issues/${number}")
val commits_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/commits")
val review_comments_url = ApiPath(s"${base.repo.url.path}/pulls/${number}/comments")
val review_comment_url = ApiPath(s"${base.repo.url.path}/pulls/comments/{number}")
val comments_url = ApiPath(s"${base.repo.url.path}/issues/${number}/comments")
val statuses_url = ApiPath(s"${base.repo.url.path}/statuses/${head.sha}")
}
object ApiPullRequest{
def apply(issue: Issue, pullRequest: PullRequest, headRepo: ApiRepository, baseRepo: ApiRepository, user: ApiUser): ApiPullRequest = ApiPullRequest(
number = issue.issueId,
updated_at = issue.updatedDate,
created_at = issue.registeredDate,
head = Commit(
sha = pullRequest.commitIdTo,
ref = pullRequest.requestBranch,
repo = headRepo)(issue.userName),
base = Commit(
sha = pullRequest.commitIdFrom,
ref = pullRequest.branch,
repo = baseRepo)(issue.userName),
mergeable = None, // TODO: need check mergeable.
title = issue.title,
body = issue.content.getOrElse(""),
user = user
)
case class Commit(
sha: String,
ref: String,
repo: ApiRepository)(baseOwner:String){
val label = if( baseOwner == repo.owner.login ){ ref }else{ s"${repo.owner.login}:${ref}" }
val user = repo.owner
}
}

View File

@@ -0,0 +1,48 @@
package gitbucket.core.api
import gitbucket.core.model.{Account, Repository}
import gitbucket.core.service.RepositoryService.RepositoryInfo
// https://developer.github.com/v3/repos/
case class ApiRepository(
name: String,
full_name: String,
description: String,
watchers: Int,
forks: Int,
`private`: Boolean,
default_branch: String,
owner: ApiUser) {
val forks_count = forks
val watchers_coun = watchers
val url = ApiPath(s"/api/v3/repos/${full_name}")
val http_url = ApiPath(s"/git/${full_name}.git")
val clone_url = ApiPath(s"/git/${full_name}.git")
val html_url = ApiPath(s"/${full_name}")
}
object ApiRepository{
def apply(
repository: Repository,
owner: ApiUser,
forkedCount: Int =0,
watchers: Int = 0): ApiRepository =
ApiRepository(
name = repository.repositoryName,
full_name = s"${repository.userName}/${repository.repositoryName}",
description = repository.description.getOrElse(""),
watchers = 0,
forks = forkedCount,
`private` = repository.isPrivate,
default_branch = repository.defaultBranch,
owner = owner
)
def apply(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount)
def apply(repositoryInfo: RepositoryInfo, owner: Account): ApiRepository =
this(repositoryInfo.repository, ApiUser(owner))
}

View File

@@ -0,0 +1,36 @@
package gitbucket.core.api
import gitbucket.core.model.Account
import java.util.Date
case class ApiUser(
login: String,
email: String,
`type`: String,
site_admin: Boolean,
created_at: Date) {
val url = ApiPath(s"/api/v3/users/${login}")
val html_url = ApiPath(s"/${login}")
// val followers_url = ApiPath(s"/api/v3/users/${login}/followers")
// val following_url = ApiPath(s"/api/v3/users/${login}/following{/other_user}")
// val gists_url = ApiPath(s"/api/v3/users/${login}/gists{/gist_id}")
// val starred_url = ApiPath(s"/api/v3/users/${login}/starred{/owner}{/repo}")
// val subscriptions_url = ApiPath(s"/api/v3/users/${login}/subscriptions")
// val organizations_url = ApiPath(s"/api/v3/users/${login}/orgs")
// val repos_url = ApiPath(s"/api/v3/users/${login}/repos")
// val events_url = ApiPath(s"/api/v3/users/${login}/events{/privacy}")
// val received_events_url = ApiPath(s"/api/v3/users/${login}/received_events")
}
object ApiUser{
def apply(user: Account): ApiUser = ApiUser(
login = user.userName,
email = user.mailAddress,
`type` = if(user.isGroupAccount){ "Organization" }else{ "User" },
site_admin = user.isAdmin,
created_at = user.registeredDate
)
}

View File

@@ -0,0 +1,7 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/issues/comments/#create-a-comment
* api form
*/
case class CreateAComment(body: String)

View File

@@ -0,0 +1,26 @@
package gitbucket.core.api
import gitbucket.core.model.CommitState
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
* api form
*/
case class CreateAStatus(
/* state is Required. The state of the status. Can be one of pending, success, error, or failure. */
state: String,
/* context is a string label to differentiate this status from the status of other systems. Default: "default" */
context: Option[String],
/* The target URL to associate with this status. This URL will be linked from the GitHub UI to allow users to easily see the source of the Status. */
target_url: Option[String],
/* description is a short description of the status.*/
description: Option[String]
) {
def isValid: Boolean = {
CommitState.valueOf(state).isDefined &&
// only http
target_url.filterNot(f => "\\Ahttps?://".r.findPrefixOf(f).isDefined && f.length<255).isEmpty &&
context.filterNot(f => f.length<255).isEmpty &&
description.filterNot(f => f.length<1000).isEmpty
}
}

View File

@@ -0,0 +1,44 @@
package gitbucket.core.api
import org.joda.time.DateTime
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)
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 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[ApiCommit]() + FieldSerializer[ApiCombinedCommitStatus]() +
FieldSerializer[ApiPullRequest.Commit]() + FieldSerializer[ApiIssue]() + FieldSerializer[ApiComment]()
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)
}
)
)
/**
* convert object to json string
*/
def apply(obj: AnyRef)(implicit c: Context): String = Serialization.write(obj)(jsonFormats + apiPathSerializer(c))
}

View File

@@ -1,30 +1,35 @@
package gitbucket.core.controller
import gitbucket.core.account.html
import gitbucket.core.api._
import gitbucket.core.helper
import gitbucket.core.model.GroupMember
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.StringUtil._
import gitbucket.core.ssh.SshUtil
import gitbucket.core.service._
import gitbucket.core.ssh.SshUtil
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib.{FileMode, Constants}
import org.scalatra.i18n.Messages
class AccountController extends AccountControllerBase
with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
with AccessTokenService with WebHookService
trait AccountControllerBase extends AccountManagementControllerBase {
self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator =>
with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator
with AccessTokenService with WebHookService =>
case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String,
url: Option[String], fileId: Option[String])
@@ -34,6 +39,8 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case class SshKeyForm(title: String, publicKey: String)
case class PersonalTokenForm(note: String)
val newForm = mapping(
"userName" -> trim(label("User name" , text(required, maxlength(100), identifier, uniqueUserName))),
"password" -> trim(label("Password" , text(required, maxlength(20)))),
@@ -57,6 +64,10 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"publicKey" -> trim(label("Key" , text(required, validPublicKey)))
)(SshKeyForm.apply)
val personalTokenForm = mapping(
"note" -> trim(label("Token", text(required, maxlength(100))))
)(PersonalTokenForm.apply)
case class NewGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String)
case class EditGroupForm(groupName: String, url: Option[String], fileId: Option[String], members: String, clearImage: Boolean)
@@ -145,6 +156,25 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
}
/**
* https://developer.github.com/v3/users/#get-a-single-user
*/
get("/api/v3/users/:userName") {
getAccountByUserName(params("userName")).map { account =>
JsonFormat(ApiUser(account))
} getOrElse NotFound
}
/**
* https://developer.github.com/v3/users/#get-the-authenticated-user
*/
get("/api/v3/user") {
context.loginAccount.map { account =>
JsonFormat(ApiUser(account))
} getOrElse Unauthorized
}
get("/:userName/_edit")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
@@ -172,15 +202,15 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val userName = params("userName")
getAccountByUserName(userName, true).foreach { account =>
// Remove repositories
getRepositoryNamesOfUser(userName).foreach { repositoryName =>
deleteRepository(userName, repositoryName)
FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
}
// Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
removeUserRelatedData(userName)
// // Remove repositories
// getRepositoryNamesOfUser(userName).foreach { repositoryName =>
// deleteRepository(userName, repositoryName)
// FileUtils.deleteDirectory(getRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getWikiRepositoryDir(userName, repositoryName))
// FileUtils.deleteDirectory(getTemporaryDir(userName, repositoryName))
// }
// // Remove from GROUP_MEMBER, COLLABORATOR and REPOSITORY
// removeUserRelatedData(userName)
updateAccount(account.copy(isRemoved = true))
}
@@ -209,6 +239,40 @@ trait AccountControllerBase extends AccountManagementControllerBase {
redirect(s"/${userName}/_ssh")
})
get("/:userName/_application")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
var tokens = getAccessTokens(x.userName)
val generatedToken = flash.get("generatedToken") match {
case Some((tokenId:Int, token:String)) => {
val gt = tokens.find(_.accessTokenId == tokenId)
gt.map{ t =>
tokens = tokens.filterNot(_ == t)
(t, token)
}
}
case _ => None
}
html.application(x, tokens, generatedToken)
} getOrElse NotFound
})
post("/:userName/_personalToken", personalTokenForm)(oneselfOnly { form =>
val userName = params("userName")
getAccountByUserName(userName).map { x =>
val (tokenId, token) = generateAccessToken(userName, form.note)
flash += "generatedToken" -> (tokenId, token)
}
redirect(s"/${userName}/_application")
})
get("/:userName/_personalToken/delete/:id")(oneselfOnly {
val userName = params("userName")
val tokenId = params("id").toInt
deleteAccessToken(userName, tokenId)
redirect(s"/${userName}/_application")
})
get("/register"){
if(context.settings.allowAccountRegistration){
if(context.loginAccount.isDefined){

View File

@@ -1,19 +1,25 @@
package gitbucket.core.controller
import gitbucket.core.api.ApiError
import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.model.Account
import org.scalatra._
import org.scalatra.json._
import org.json4s._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.json4s._
import org.scalatra._
import org.scalatra.i18n._
import org.scalatra.json._
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import javax.servlet.{FilterChain, ServletResponse, ServletRequest}
import org.scalatra.i18n._
import scala.util.Try
/**
* Provides generic features for controller implementations.
@@ -51,6 +57,9 @@ abstract class ControllerBase extends ScalatraFilter
// Git repository
chain.doFilter(request, response)
} else {
if(path.startsWith("/api/v3/")){
httpRequest.setAttribute(Keys.Request.APIv3, true)
}
// Scalatra actions
super.doFilter(request, response, chain)
}
@@ -74,7 +83,7 @@ abstract class ControllerBase extends ScalatraFilter
}
}
private def LoginAccount: Option[Account] = session.getAs[Account](Keys.Session.LoginAccount)
private def LoginAccount: Option[Account] = request.getAs[Account](Keys.Session.LoginAccount).orElse(session.getAs[Account](Keys.Session.LoginAccount))
def ajaxGet(path : String)(action : => Any) : Route =
super.get(path){
@@ -103,6 +112,9 @@ abstract class ControllerBase extends ScalatraFilter
protected def NotFound() =
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.NotFound()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.NotFound(ApiError("Not Found"))
} else {
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
}
@@ -110,6 +122,9 @@ abstract class ControllerBase extends ScalatraFilter
protected def Unauthorized()(implicit context: Context) =
if(request.hasAttribute(Keys.Request.Ajax)){
org.scalatra.Unauthorized()
} else if(request.hasAttribute(Keys.Request.APIv3)){
contentType = formats("json")
org.scalatra.Unauthorized(ApiError("Requires authentication"))
} else {
if(context.loginAccount.isDefined){
org.scalatra.Unauthorized(redirect("/"))
@@ -146,6 +161,15 @@ abstract class ControllerBase extends ScalatraFilter
response.addHeader("X-Content-Type-Options", "nosniff")
rawData
}
// jenkins send message as 'application/x-www-form-urlencoded' but scalatra already parsed as multi-part-request.
def extractFromJsonBody[A](implicit request:HttpServletRequest, mf:Manifest[A]): Option[A] = {
(request.contentType.map(_.split(";").head.toLowerCase) match{
case Some("application/x-www-form-urlencoded") => multiParams.keys.headOption.map(parse(_))
case Some("application/json") => Some(parsedBody)
case _ => Some(parse(request.body))
}).filterNot(_ == JNothing).flatMap(j => Try(j.extract[A]).toOption)
}
}
/**

View File

@@ -1,16 +1,20 @@
package gitbucket.core.controller
import gitbucket.core.html
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.util.{LDAPUtil, Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import jp.sf.amateras.scalatra.forms._
class IndexController extends IndexControllerBase
with RepositoryService with ActivityService with AccountService with UsersAuthenticator
trait IndexControllerBase extends ControllerBase {
self: RepositoryService with ActivityService with AccountService with UsersAuthenticator =>
@@ -95,7 +99,7 @@ trait IndexControllerBase extends ControllerBase {
get("/_user/proposals")(usersOnly {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("options" -> getAllUsers().filter(!_.isGroupAccount).map(_.userName).toArray)
Map("options" -> getAllUsers(false).filter(!_.isGroupAccount).map(_.userName).toArray)
)
})
@@ -106,4 +110,13 @@ trait IndexControllerBase extends ControllerBase {
getAccountByUserName(params("userName")).isDefined
})
/**
* @see https://developer.github.com/v3/rate_limit/#get-your-current-rate-limit-status
* but not enabled.
*/
get("/api/v3/rate_limit"){
contentType = formats("json")
// this message is same as github enterprise...
org.scalatra.NotFound(ApiError("Rate limiting is not enabled."))
}
}

View File

@@ -1,25 +1,27 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.issues.html
import gitbucket.core.model.Issue
import gitbucket.core.service.IssuesService._
import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.Markdown
import jp.sf.amateras.scalatra.forms._
import IssuesService._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.Ok
class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService
trait IssuesControllerBase extends ControllerBase {
self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with WebHookIssueCommentService =>
case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
@@ -76,6 +78,18 @@ trait IssuesControllerBase extends ControllerBase {
}
})
/**
* https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
*/
get("/api/v3/repos/:owner/:repository/issues/:id/comments")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
comments = getCommentsForApi(repository.owner, repository.name, issueId.toInt)
} yield {
JsonFormat(comments.map{ case (issueComment, user) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user)) })
}).getOrElse(NotFound)
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
html.create(
@@ -112,14 +126,17 @@ trait IssuesControllerBase extends ControllerBase {
// record activity
recordCreateIssueActivity(owner, name, userName, issueId, form.title)
// extract references and create refer comment
getIssue(owner, name, issueId.toString).foreach { issue =>
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
}
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
// call web hooks
callIssuesWebHook("opened", repository, issue, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
redirect(s"/${owner}/${name}/issues/${issueId}")
@@ -163,6 +180,20 @@ trait IssuesControllerBase extends ControllerBase {
} getOrElse NotFound
})
/**
* https://developer.github.com/v3/issues/comments/#create-a-comment
*/
post("/api/v3/repos/:owner/:repository/issues/:id/comments")(readableUsersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
body <- extractFromJsonBody[CreateAComment].map(_.body) if ! body.isEmpty
(issue, id) <- handleComment(issueId, Some(body), repository)()
issueComment <- getComment(repository.owner, repository.name, id.toString())
} yield {
JsonFormat(ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(context.loginAccount.get)))
}) getOrElse NotFound
})
post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) =>
handleComment(form.issueId, form.content, repository)() map { case (issue, id) =>
redirect(s"/${repository.owner}/${repository.name}/${
@@ -317,9 +348,12 @@ trait IssuesControllerBase extends ControllerBase {
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
StringUtil.extractIssueId(message).foreach { issueId =>
val content = fromIssue.issueId + ":" + fromIssue.title
if(getIssue(owner, repository, issueId).isDefined){
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt,
fromIssue.issueId + ":" + fromIssue.title, "refer")
// Not add if refer comment already exist.
if(!getComments(owner, repository, issueId.toInt).exists { x => x.action == "refer" && x.content == content }) {
createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, content, "refer")
}
}
}
}
@@ -367,17 +401,33 @@ trait IssuesControllerBase extends ControllerBase {
createReferComment(owner, name, issue, content)
}
// call web hooks
action match {
case None => commentId.map{ commentIdSome => callIssueCommentWebHook(repository, issue, commentIdSome, context.loginAccount.get) }
case Some(act) => val webHookAction = act match {
case "open" => "opened"
case "reopen" => "reopened"
case "close" => "closed"
case _ => act
}
if(issue.isPullRequest){
callPullRequestWebHook(webHookAction, repository, issue.issueId, context.baseUrl, context.loginAccount.get)
} else {
callIssuesWebHook(webHookAction, repository, issue, context.baseUrl, context.loginAccount.get)
}
}
// notifications
Notifier() match {
case f =>
content foreach {
f.toNotify(repository, issueId, _){
f.toNotify(repository, issue, _){
Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${
if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId.get}")
}
}
action foreach {
f.toNotify(repository, issueId, _){
f.toNotify(repository, issue, _){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}")
}
}
@@ -419,5 +469,4 @@ trait IssuesControllerBase extends ControllerBase {
hasWritePermission(owner, repoName, context.loginAccount))
}
}
}

View File

@@ -1,34 +1,39 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.model.{Account, CommitState, Repository, PullRequest, Issue}
import gitbucket.core.pulls.html
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 gitbucket.core.view
import gitbucket.core.view.helpers
import org.eclipse.jgit.api.Git
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.transport.RefSpec
import scala.collection.JavaConverters._
import org.eclipse.jgit.lib.{ObjectId, CommitBuilder, PersonIdent}
import gitbucket.core.service._
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.MergeService
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._
import gitbucket.core.service.WebHookService.WebHookPayload
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util._
import gitbucket.core.view
import gitbucket.core.view.helpers
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.PersonIdent
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
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 WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with CommitsService with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
@@ -70,6 +75,24 @@ trait PullRequestsControllerBase extends ControllerBase {
}
})
/**
* https://developer.github.com/v3/pulls/#list-pull-requests
*/
get("/api/v3/repos/:owner/:repository/pulls")(referrersOnly { repository =>
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues:List[(Issue, Account, Int, PullRequest, Repository, Account)] = searchPullRequestByApi(condition, (page - 1) * PullRequestLimit, PullRequestLimit, repository.owner -> repository.name)
JsonFormat(issues.map{case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner) =>
ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)) })
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
@@ -78,7 +101,6 @@ trait PullRequestsControllerBase extends ControllerBase {
using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
html.pullreq(
issue, pullreq,
(commits.flatten.map(commit => getCommitComments(owner, name, commit.id, true)).flatten.toList ::: getComments(owner, name, issueId))
@@ -96,14 +118,64 @@ trait PullRequestsControllerBase extends ControllerBase {
} getOrElse NotFound
})
/**
* https://developer.github.com/v3/pulls/#get-a-single-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id")(referrersOnly { repository =>
(for{
issueId <- params("id").toIntOpt
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set())
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName, baseUrl)
} yield {
JsonFormat(ApiPullRequest(
issue,
pullRequest,
ApiRepository(headRepo, ApiUser(headOwner)),
ApiRepository(repository, ApiUser(baseOwner)),
ApiUser(issueUser)))
}).getOrElse(NotFound)
})
/**
* https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
*/
get("/api/v3/repos/:owner/:repository/pulls/:id/commits")(referrersOnly { repository =>
val owner = repository.owner
val name = repository.name
params("id").toIntOpt.flatMap{ issueId =>
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val oldId = git.getRepository.resolve(pullreq.commitIdFrom)
val newId = git.getRepository.resolve(pullreq.commitIdTo)
val repoFullName = RepositoryName(repository)
val commits = git.log.addRange(oldId, newId).call.iterator.asScala.map(c => ApiCommitListItem(new CommitInfo(c), repoFullName)).toList
JsonFormat(commits)
}
}
} getOrElse NotFound
})
ajaxGet("/:owner/:repository/pull/:id/mergeguide")(collaboratorsOnly { 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}"){
checkConflict(owner, name, pullreq.branch, issueId)
}
val hasProblem = hasConfrict || (!statuses.isEmpty && CommitState.combine(statuses.map(_.state).toSet) != CommitState.SUCCESS)
html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
hasConfrict,
hasProblem,
issue,
pullreq,
statuses,
repository,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
}
} getOrElse NotFound
@@ -140,43 +212,10 @@ trait PullRequestsControllerBase extends ControllerBase {
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// merge
val mergeBaseRefName = s"refs/heads/${pullreq.branch}"
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(mergeBaseRefName)
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
if (conflicted) {
throw new RuntimeException("This pull request can't merge automatically.")
}
// creates merge commit
val mergeCommit = new CommitBuilder()
mergeCommit.setTreeId(merger.getResultTreeId)
mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
val personIdent = new PersonIdent(loginAccount.fullName, loginAccount.mailAddress)
mergeCommit.setAuthor(personIdent)
mergeCommit.setCommitter(personIdent)
mergeCommit.setMessage(s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" +
form.message)
// insertObject and got mergeCommit Object Id
val inserter = git.getRepository.newObjectInserter
val mergeCommitId = inserter.insert(mergeCommit)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(mergeBaseRefName)
refUpdate.setNewObjectId(mergeCommitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(personIdent)
refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// merge git repository
mergePullRequest(git, pullreq.branch, issueId,
s"Merge pull request #${issueId} from ${pullreq.requestUserName}/${pullreq.requestBranch}\n\n" + form.message,
new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
@@ -194,17 +233,10 @@ trait PullRequestsControllerBase extends ControllerBase {
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
// call web hook
getWebHookURLs(owner, name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(owner)){
callWebHook(owner, name, webHookURLs,
WebHookPayload(git, loginAccount, mergeBaseRefName, repository, commits.flatten.toList, ownerAccount))
}
case _ =>
}
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier().toNotify(repository, issue, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
@@ -216,6 +248,7 @@ trait PullRequestsControllerBase extends ControllerBase {
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
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 =>
@@ -223,8 +256,8 @@ trait PullRequestsControllerBase extends ControllerBase {
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) =>
val oldBranch = JGitUtil.getDefaultBranch(oldGit, originRepository).get._2
val newBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2
val newBranch = headBranch.getOrElse(JGitUtil.getDefaultBranch(newGit, forkedRepository).get._2)
val oldBranch = originRepository.branchList.find( _ == newBranch).getOrElse(JGitUtil.getDefaultBranch(oldGit, originRepository).get._2)
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
@@ -233,7 +266,7 @@ trait PullRequestsControllerBase extends ControllerBase {
case _ => {
using(Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))){ git =>
JGitUtil.getDefaultBranch(git, forkedRepository).map { case (_, defaultBranch) =>
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${defaultBranch}")
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${defaultBranch}...${headBranch.getOrElse(defaultBranch)}")
} getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
@@ -248,12 +281,18 @@ trait PullRequestsControllerBase extends ControllerBase {
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
originRepositoryName <- if(originOwner == forkedOwner) {
// Self repository
Some(forkedRepository.name)
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
// Original repository
forkedRepository.repository.originRepositoryName
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
// Sibling repository
getUserRepositories(originOwner, context.baseUrl).find { x =>
x.repository.originUserName == forkedRepository.repository.originUserName &&
x.repository.originRepositoryName == forkedRepository.repository.originRepositoryName
}.map(_.repository.repositoryName)
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
@@ -319,10 +358,11 @@ trait PullRequestsControllerBase extends ControllerBase {
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
html.mergecheck(
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch))
forkedRepository.owner, forkedRepository.name, forkedBranch)
}
html.mergecheck(conflict)
}
}) getOrElse NotFound
})
@@ -352,80 +392,24 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdTo = form.commitIdTo)
// fetch requested branch
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.fetch
.setRemote(getRepositoryDir(form.requestUserName, form.requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${form.requestBranch}:refs/pull/${issueId}/head"))
.call
}
fetchAsPullRequest(repository.owner, repository.name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issueId, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
getIssue(repository.owner, repository.name, issueId.toString) foreach { issue =>
Notifier().toNotify(repository, issue, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${repository.owner}/${repository.name}/pull/${issueId}")
}
}
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
})
/**
* Checks whether conflict will be caused in merging. Returns true if conflict will be caused.
*/
private def checkConflict(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}"){
using(Git.open(getRepositoryDir(requestUserName, requestRepositoryName))) { git =>
val remoteRefName = s"refs/heads/${branch}"
val tmpRefName = s"refs/merge-check/${userName}/${branch}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).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 mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
}
}
}
}
/**
* Checks whether conflict will be caused in merging within pull request. Returns true if conflict will be caused.
*/
private def checkConflictInPullRequest(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestBranch: String,
issueId: Int): Boolean = {
LockUtil.lock(s"${userName}/${repositoryName}") {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
// merge
val merger = MergeStrategy.RECURSIVE.newMerger(git.getRepository, true)
val mergeBaseTip = git.getRepository.resolve(s"refs/heads/${branch}")
val mergeTip = git.getRepository.resolve(s"refs/pull/${issueId}/head")
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
}
}
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
@@ -484,5 +468,4 @@ trait PullRequestsControllerBase extends ControllerBase {
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}

View File

@@ -3,7 +3,7 @@ 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.WebHookService.WebHookPayload
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
@@ -15,6 +15,7 @@ import org.scalatra.i18n.Messages
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
with OwnerAuthenticator with UsersAuthenticator
@@ -162,15 +163,15 @@ trait RepositorySettingsControllerBase extends ControllerBase {
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
import scala.collection.JavaConverters._
val commits = git.log
val commits = if(repository.commitCount == 0) List.empty else git.log
.add(git.getRepository.resolve(repository.repository.defaultBranch))
.setMaxCount(3)
.call.iterator.asScala.map(new CommitInfo(_))
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook(repository.owner, repository.name,
callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)),
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
)
}
flash += "url" -> form.url
@@ -273,4 +274,4 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
}
}
}

View File

@@ -1,39 +1,45 @@
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.repo.html
import gitbucket.core.helper
import gitbucket.core.service._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
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
import gitbucket.core.service.WebHookService.WebHookPayload
import gitbucket.core.model.{Account, CommitState}
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
import org.scalatra._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.{ArchiveCommand, Git}
import org.eclipse.jgit.archive.{TgzFormat, ZipFormat}
import org.eclipse.jgit.lib._
import org.apache.commons.io.FileUtils
import org.eclipse.jgit.treewalk._
import jp.sf.amateras.scalatra.forms._
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk.RevCommit
import org.eclipse.jgit.treewalk._
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 ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService
/**
* The repository viewer.
*/
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 ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator with PullRequestService with CommitStatusService
with WebHookPullRequestService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
@@ -109,6 +115,13 @@ trait RepositoryViewerControllerBase extends ControllerBase {
fileList(_)
})
/**
* https://developer.github.com/v3/repos/#get
*/
get("/api/v3/repos/:owner/:repository")(referrersOnly { repository =>
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(repository.owner).get)))
})
/**
* Displays the file list of the specified path and branch.
*/
@@ -140,6 +153,56 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
})
/**
* https://developer.github.com/v3/repos/statuses/#create-a-status
*/
post("/api/v3/repos/:owner/:repo/statuses/:sha")(collaboratorsOnly { repository =>
(for{
ref <- params.get("sha")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
data <- extractFromJsonBody[CreateAStatus] if data.isValid
creator <- context.loginAccount
state <- CommitState.valueOf(data.state)
statusId = createCommitStatus(repository.owner, repository.name, sha, data.context.getOrElse("default"),
state, data.target_url, data.description, new java.util.Date(), creator)
status <- getCommitStatus(repository.owner, repository.name, statusId)
} yield {
JsonFormat(ApiCommitStatus(status, ApiUser(creator)))
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/statuses")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
JsonFormat(getCommitStatuesWithCreator(repository.owner, repository.name, sha).map{ case(status, creator) =>
ApiCommitStatus(status, ApiUser(creator))
})
}) getOrElse NotFound
})
/**
* https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
*
* ref is Ref to list the statuses from. It can be a SHA, a branch name, or a tag name.
*/
get("/api/v3/repos/:owner/:repo/commits/:ref/status")(referrersOnly { repository =>
(for{
ref <- params.get("ref")
owner <- getAccountByUserName(repository.owner)
sha <- JGitUtil.getShaByRef(repository.owner, repository.name, ref)
} yield {
val statuses = getCommitStatuesWithCreator(repository.owner, repository.name, sha)
JsonFormat(ApiCombinedCommitStatus(sha, statuses, ApiRepository(repository, owner)))
}) getOrElse NotFound
})
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
@@ -174,9 +237,16 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), None,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
form.message.getOrElse(s"Create ${form.newFileName}"))
commitFile(
repository = repository,
branch = form.branch,
path = form.path,
newFileName = Some(form.newFileName),
oldFileName = None,
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
charset = form.charset,
message = form.message.getOrElse(s"Create ${form.newFileName}")
)
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
@@ -184,13 +254,20 @@ trait RepositoryViewerControllerBase extends ControllerBase {
})
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, Some(form.newFileName), form.oldFileName,
StringUtil.convertLineSeparator(form.content, form.lineSeparator), form.charset,
if(form.oldFileName.exists(_ == form.newFileName)){
commitFile(
repository = repository,
branch = form.branch,
path = form.path,
newFileName = Some(form.newFileName),
oldFileName = form.oldFileName,
content = appendNewLine(convertLineSeparator(form.content, form.lineSeparator), form.lineSeparator),
charset = form.charset,
message = if(form.oldFileName.exists(_ == form.newFileName)){
form.message.getOrElse(s"Update ${form.newFileName}")
} else {
form.message.getOrElse(s"Rename ${form.oldFileName.get} to ${form.newFileName}")
})
}
)
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
@@ -207,27 +284,60 @@ trait RepositoryViewerControllerBase extends ControllerBase {
/**
* Displays the file content of the specified branch or commit.
*/
get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val blobRoute = get("/:owner/:repository/blob/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
val raw = params.get("raw").getOrElse("false").toBoolean
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))
val lastModifiedCommit = JGitUtil.getLastModifiedCommit(git, revCommit, path)
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
RawData(FileUtil.getContentType(path, bytes), bytes)
}
JGitUtil.getContentFromId(git, objectId, true).map { bytes =>
RawData("application/octet-stream", bytes)
} getOrElse NotFound
} else {
html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(lastModifiedCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
html.blob(id, repository, path.split("/").toList,
JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(JGitUtil.getLastModifiedCommit(git, revCommit, path)),
hasWritePermission(repository.owner, repository.name, context.loginAccount),
request.paths(2) == "blame")
}
} getOrElse NotFound
}
})
get("/:owner/:repository/blame/*"){
blobRoute.action()
}
/**
* Blame data.
*/
ajaxGet("/:owner/:repository/get-blame/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
contentType = formats("json")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val last = git.log.add(git.getRepository.resolve(id)).addPath(path).setMaxCount(1).call.iterator.next.name
Map(
"root" -> s"${context.baseUrl}/${repository.owner}/${repository.name}",
"id" -> id,
"path" -> path,
"last" -> last,
"blame" -> JGitUtil.getBlame(git, id, path).map{ blame =>
Map(
"id" -> blame.id,
"author" -> view.helpers.user(blame.authorName, blame.authorEmailAddress).toString,
"avatar" -> view.helpers.avatarLink(blame.authorName, 32, blame.authorEmailAddress).toString,
"authed" -> helper.html.datetimeago(blame.authorTime).toString,
"prev" -> blame.prev,
"prevPath" -> blame.prevPath,
"commited" -> blame.commitTime.getTime,
"message" -> blame.message,
"lines" -> blame.lines)
})
}
})
/**
* Displays details of the specified commit.
*/
@@ -398,6 +508,34 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repository)
})
/**
* Displays the file find of branch.
*/
get("/:owner/:repository/find/:ref")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getTreeId(git, params("ref")).map{ treeId =>
html.find(params("ref"),
treeId,
repository,
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
})
} getOrElse NotFound
}
})
/**
* Get all file list of branch.
*/
ajaxGet("/:owner/:repository/tree-list/:tree")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val treeId = params("tree")
contentType = formats("json")
Map("paths" -> JGitUtil.getAllFileListByTreeId(git, treeId))
}
})
private def splitPath(repository: RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
@@ -409,7 +547,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
s"readme.${extension}"
} ++ Seq("readme.txt", "readme")
/**
* Provides HTML of the file list.
@@ -448,6 +588,7 @@ trait RepositoryViewerControllerBase extends ControllerBase {
}, // groups of current user
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount),
getPullRequestFromBranch(repository.owner, repository.name, revstr, repository.repository.defaultBranch),
flash.get("info"), flash.get("error"))
}
} getOrElse NotFound
@@ -507,14 +648,12 @@ trait RepositoryViewerControllerBase extends ControllerBase {
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook
callPullRequestWebHookByRequestBranch("synchronize", repository, branch, context.baseUrl, loginAccount)
val commit = new JGitUtil.CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, loginAccount, headName, repository, List(commit), ownerAccount))
}
case _ =>
callWebHookOf(repository.owner, repository.name, "push") {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount)
}
}
}
}

View File

@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
"isCreateRepoOptionPublic" -> trim(label("Default option to create a new repository", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"activityLogLimit" -> trim(label("Limit of activity logs", optional(number()))),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(

View File

@@ -194,7 +194,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
protected def disableByNotYourself(paramName: String): Constraint = new Constraint() {
override def validate(name: String, value: String, messages: Messages): Option[String] = {
params.get(paramName).flatMap { userName =>
if(userName == context.loginAccount.get.userName)
if(userName == context.loginAccount.get.userName && params.get("removed") == Some("true"))
Some("You can't disable your account yourself")
else
None

View File

@@ -3,6 +3,7 @@ package gitbucket.core.controller
import gitbucket.core.wiki.html
import gitbucket.core.service.{RepositoryService, WikiService, ActivityService, AccountService}
import gitbucket.core.util._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
@@ -110,8 +111,16 @@ trait WikiControllerBase extends ControllerBase {
post("/:owner/:repository/wiki/_edit", editForm)(collaboratorsOnly { (form, repository) =>
defining(context.loginAccount.get){ loginAccount =>
saveWikiPage(repository.owner, repository.name, form.currentPageName, form.pageName,
form.content, loginAccount, form.message.getOrElse(""), Some(form.id)).map { commitId =>
saveWikiPage(
repository.owner,
repository.name,
form.currentPageName,
form.pageName,
appendNewLine(convertLineSeparator(form.content, "LF"), "LF"),
loginAccount,
form.message.getOrElse(""),
Some(form.id)
).map { commitId =>
updateLastActivityDate(repository.owner, repository.name)
recordEditWikiPageActivity(repository.owner, repository.name, loginAccount.userName, form.pageName, commitId)
}

View File

@@ -0,0 +1,21 @@
package gitbucket.core.model
trait AccessTokenComponent { self: Profile =>
import profile.simple._
lazy val AccessTokens = TableQuery[AccessTokens]
class AccessTokens(tag: Tag) extends Table[AccessToken](tag, "ACCESS_TOKEN") {
val accessTokenId = column[Int]("ACCESS_TOKEN_ID", O AutoInc)
val userName = column[String]("USER_NAME")
val tokenHash = column[String]("TOKEN_HASH")
val note = column[String]("NOTE")
def * = (accessTokenId, userName, tokenHash, note) <> (AccessToken.tupled, AccessToken.unapply)
}
}
case class AccessToken(
accessTokenId: Int = 0,
userName: String,
tokenHash: String,
note: String
)

View File

@@ -49,6 +49,9 @@ protected[model] trait TemplateComponent { self: Profile =>
def byCommit(owner: String, repository: String, commitId: String) =
byRepository(owner, repository) && (this.commitId === commitId)
def byCommit(owner: Column[String], repository: Column[String], commitId: Column[String]) =
byRepository(userName, repositoryName) && (this.commitId === commitId)
}
}

View File

@@ -0,0 +1,83 @@
package gitbucket.core.model
import scala.slick.lifted.MappedTo
import scala.slick.jdbc._
trait CommitStatusComponent extends TemplateComponent { self: Profile =>
import profile.simple._
import self._
implicit val commitStateColumnType = MappedColumnType.base[CommitState, String](b => b.name , i => CommitState(i))
lazy val CommitStatuses = TableQuery[CommitStatuses]
class CommitStatuses(tag: Tag) extends Table[CommitStatus](tag, "COMMIT_STATUS") with CommitTemplate {
val commitStatusId = column[Int]("COMMIT_STATUS_ID", O AutoInc)
val context = column[String]("CONTEXT")
val state = column[CommitState]("STATE")
val targetUrl = column[Option[String]]("TARGET_URL")
val description = column[Option[String]]("DESCRIPTION")
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 byPrimaryKey(id: Int) = commitStatusId === id.bind
}
}
case class CommitStatus(
commitStatusId: Int = 0,
userName: String,
repositoryName: String,
commitId: String,
context: String,
state: CommitState,
targetUrl: Option[String],
description: Option[String],
creator: String,
registeredDate: java.util.Date,
updatedDate: java.util.Date
)
sealed abstract class CommitState(val name: String)
object CommitState {
object ERROR extends CommitState("error")
object FAILURE extends CommitState("failure")
object PENDING extends CommitState("pending")
object SUCCESS extends CommitState("success")
val values: Vector[CommitState] = Vector(PENDING, SUCCESS, ERROR, FAILURE)
private val map: Map[String, CommitState] = values.map(enum => enum.name -> enum).toMap
def apply(name: String): CommitState = map(name)
def valueOf(name: String): Option[CommitState] = map.get(name)
/**
* failure if any of the contexts report as error or failure
* pending if there are no statuses or a context is pending
* success if the latest status for all contexts is success
*/
def combine(statuses: Set[CommitState]): CommitState = {
if(statuses.isEmpty){
PENDING
} else if(statuses.contains(CommitState.ERROR) || statuses.contains(CommitState.FAILURE)) {
FAILURE
} else if(statuses.contains(CommitState.PENDING)) {
PENDING
} else {
SUCCESS
}
}
implicit val getResult: GetResult[CommitState] = GetResult(r => CommitState(r.<<))
implicit val getResultOpt: GetResult[Option[CommitState]] = GetResult(r => r.<<?[String].map(CommitState(_)))
}

View File

@@ -1,5 +1,6 @@
package gitbucket.core.model
trait Profile {
val profile: slick.driver.JdbcProfile
import profile.simple._
@@ -31,10 +32,12 @@ trait ProfileProvider { self: Profile =>
}
trait CoreProfile extends ProfileProvider with Profile
with AccessTokenComponent
with AccountComponent
with ActivityComponent
with CollaboratorComponent
with CommitCommentComponent
with CommitStatusComponent
with GroupMemberComponent
with IssueComponent
with IssueCommentComponent

View File

@@ -1,5 +1,9 @@
package gitbucket.core.plugin
import javax.servlet.ServletContext
import gitbucket.core.controller.ControllerBase
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Version
/**
@@ -13,16 +17,59 @@ trait Plugin {
val description: String
val versions: Seq[Version]
/**
* Override to declare this plug-in provides images.
*/
val images: Seq[(String, Array[Byte])] = Nil
/**
* Override to declare this plug-in provides controllers.
*/
val controllers: Seq[(String, ControllerBase)] = Nil
/**
* Override to declare this plug-in provides JavaScript.
*/
val javaScripts: Seq[(String, String)] = Nil
/**
* Override to declare this plug-in provides renderers.
*/
val renderers: Seq[(String, Renderer)] = Nil
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
*/
def initialize(registry: PluginRegistry): Unit
def initialize(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {
images.foreach { case (id, in) =>
registry.addImage(id, in)
}
controllers.foreach { case (path, controller) =>
registry.addController(path, controller)
}
javaScripts.foreach { case (path, script) =>
registry.addJavaScript(path, script)
}
renderers.foreach { case (extension, renderer) =>
registry.addRenderer(extension, renderer)
}
}
/**
* This method is invoked in shutdown of plugin system.
* If the plugin has any resources, release them in this method.
*/
def shutdown(registry: PluginRegistry): Unit
def shutdown(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Unit = {}
/**
* Helper method to get a resource from classpath.
*/
protected def fromClassPath(path: String): Array[Byte] =
using(getClass.getClassLoader.getResourceAsStream(path)){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
}

View File

@@ -7,6 +7,7 @@ import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.controller.{Context, ControllerBase}
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.service.SystemSettingsService.SystemSettings
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.JDBCUtil._
@@ -23,6 +24,10 @@ class PluginRegistry {
private val javaScripts = new ListBuffer[(String, String)]
private val controllers = new ListBuffer[(ControllerBase, String)]
private val images = mutable.Map[String, String]()
private val renderers = mutable.Map[String, Renderer]()
renderers ++= Seq(
"md" -> MarkdownRenderer, "markdown" -> MarkdownRenderer
)
def addPlugin(pluginInfo: PluginInfo): Unit = {
plugins += pluginInfo
@@ -30,34 +35,52 @@ class PluginRegistry {
def getPlugins(): List[PluginInfo] = plugins.toList
def addImage(id: String, bytes: Array[Byte]): Unit = {
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
images += ((id, encoded))
}
@deprecated("Use addImage(id: String, bytes: Array[Byte]) instead", "3.4.0")
def addImage(id: String, in: InputStream): Unit = {
val bytes = using(in){ in =>
val bytes = new Array[Byte](in.available)
in.read(bytes)
bytes
}
val encoded = StringUtils.newStringUtf8(Base64.encodeBase64(bytes, false))
images += ((id, encoded))
addImage(id, bytes)
}
def getImage(id: String): String = images(id)
def addController(controller: ControllerBase, path: String): Unit = {
def addController(path: String, controller: ControllerBase): Unit = {
controllers += ((controller, path))
}
@deprecated("Use addController(path: String, controller: ControllerBase) instead", "3.4.0")
def addController(controller: ControllerBase, path: String): Unit = {
addController(path, controller)
}
def getControllers(): List[(ControllerBase, String)] = controllers.toList
def addJavaScript(path: String, script: String): Unit = {
javaScripts += Tuple2(path, script)
javaScripts += ((path, script))
}
//def getJavaScripts(): List[(String, String)] = javaScripts.toList
def getJavaScript(currentPath: String): Option[String] = {
javaScripts.find(x => currentPath.matches(x._1)).map(_._2)
def getJavaScript(currentPath: String): List[String] = {
javaScripts.filter(x => currentPath.matches(x._1)).toList.map(_._2)
}
def addRenderer(extension: String, renderer: Renderer): Unit = {
renderers += ((extension, renderer))
}
def getRenderer(extension: String): Renderer = {
renderers.get(extension).getOrElse(DefaultRenderer)
}
def renderableExtensions: Seq[String] = renderers.keys.toSeq
private case class GlobalAction(
method: String,
path: String,
@@ -89,7 +112,7 @@ object PluginRegistry {
/**
* Initializes all installed plugins.
*/
def initialize(context: ServletContext, conn: java.sql.Connection): Unit = {
def initialize(context: ServletContext, settings: SystemSettings, conn: java.sql.Connection): Unit = {
val pluginDir = new File(PluginHome)
if(pluginDir.exists && pluginDir.isDirectory){
pluginDir.listFiles(new FilenameFilter {
@@ -119,7 +142,7 @@ object PluginRegistry {
}
// Initialize
plugin.initialize(instance)
plugin.initialize(instance, context, settings)
instance.addPlugin(PluginInfo(
pluginId = plugin.pluginId,
pluginName = plugin.pluginName,
@@ -137,10 +160,10 @@ object PluginRegistry {
}
}
def shutdown(context: ServletContext): Unit = {
def shutdown(context: ServletContext, settings: SystemSettings): Unit = {
instance.getPlugins().foreach { pluginInfo =>
try {
pluginInfo.pluginClass.shutdown(instance)
pluginInfo.pluginClass.shutdown(instance, context, settings)
} catch {
case e: Exception => {
logger.error(s"Error during plugin shutdown", e)
@@ -158,4 +181,4 @@ case class PluginInfo(
version: String,
description: String,
pluginClass: Plugin
)
)

View File

@@ -0,0 +1,44 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.service.RepositoryService
import gitbucket.core.view.Markdown
import play.twirl.api.Html
/**
* A render engine to render content to HTML.
*/
trait Renderer {
/**
* Render the given request to HTML.
*/
def render(request: RenderRequest): Html
}
object MarkdownRenderer extends Renderer {
override def render(request: RenderRequest): Html = {
import request._
Html(Markdown.toHtml(fileContent, repository, enableWikiLink, enableRefsLink)(context))
}
}
object DefaultRenderer extends Renderer {
override def render(request: RenderRequest): Html = {
import request._
Html(
s"<tt>${
fileContent.split("(\\r\\n)|\\n").map(xml.Utility.escape(_)).mkString("<br/>")
}</tt>"
)
}
}
case class RenderRequest(filePath: List[String],
fileContent: String,
branch: String,
repository: RepositoryService.RepositoryInfo,
enableWikiLink: Boolean,
enableRefsLink: Boolean,
context: Context)

View File

@@ -0,0 +1,55 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.model.{Account, AccessToken}
import gitbucket.core.util.StringUtil
import scala.util.Random
trait AccessTokenService {
def makeAccessTokenString: String = {
val bytes = new Array[Byte](20)
Random.nextBytes(bytes)
bytes.map("%02x".format(_)).mkString
}
def tokenToHash(token: String): String = StringUtil.sha1(token)
/**
* @retuen (TokenId, Token)
*/
def generateAccessToken(userName: String, note: String)(implicit s: Session): (Int, String) = {
var token: String = null
var hash: String = null
do{
token = makeAccessTokenString
hash = tokenToHash(token)
}while(AccessTokens.filter(_.tokenHash === hash.bind).exists.run)
val newToken = AccessToken(
userName = userName,
note = note,
tokenHash = hash)
val tokenId = (AccessTokens returning AccessTokens.map(_.accessTokenId)) += newToken
(tokenId, token)
}
def getAccountByAccessToken(token: String)(implicit s: Session): Option[Account] =
Accounts
.innerJoin(AccessTokens)
.filter{ case (ac, t) => (ac.userName === t.userName) && (t.tokenHash === tokenToHash(token).bind) && (ac.removed === false.bind) }
.map{ case (ac, t) => ac }
.firstOption
def getAccessTokens(userName: String)(implicit s: Session): List[AccessToken] =
AccessTokens.filter(_.userName === userName.bind).sortBy(_.accessTokenId.desc).list
def deleteAccessToken(userName: String, accessTokenId: Int)(implicit s: Session): Unit =
AccessTokens filter (t => t.userName === userName.bind && t.accessTokenId === accessTokenId) delete
}
object AccessTokenService extends AccessTokenService

View File

@@ -77,6 +77,16 @@ trait AccountService {
def getAccountByUserName(userName: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Accounts filter(t => (t.userName === userName.bind) && (t.removed === false.bind, !includeRemoved)) firstOption
def getAccountsByUserNames(userNames: Set[String], knowns:Set[Account], includeRemoved: Boolean = false)(implicit s: Session): Map[String, Account] = {
val map = knowns.map(a => a.userName -> a).toMap
val needs = userNames -- map.keySet
if(needs.isEmpty){
map
}else{
map ++ Accounts.filter(t => (t.userName inSetBind needs) && (t.removed === false.bind, !includeRemoved)).list.map(a => a.userName -> a).toMap
}
}
def getAccountByMailAddress(mailAddress: String, includeRemoved: Boolean = false)(implicit s: Session): Option[Account] =
Accounts filter(t => (t.mailAddress.toLowerCase === mailAddress.toLowerCase.bind) && (t.removed === false.bind, !includeRemoved)) firstOption

View File

@@ -7,6 +7,12 @@ import profile.simple._
trait ActivityService {
def deleteOldActivities(limit: Int)(implicit s: Session): Int = {
Activities.map(_.activityId).sortBy(_ desc).drop(limit).firstOption.map { id =>
Activities.filter(_.activityId <= id.bind).delete
} getOrElse 0
}
def getActivitiesByUser(activityUserName: String, isPublic: Boolean)(implicit s: Session): List[Activity] =
Activities
.innerJoin(Repositories).on((t1, t2) => t1.byRepository(t2.userName, t2.repositoryName))

View File

@@ -0,0 +1,52 @@
package gitbucket.core.service
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.model.{CommitState, CommitStatus, Account}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.StringUtil._
import gitbucket.core.service.RepositoryService.RepositoryInfo
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 )
.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)
}
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
def getCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) :List[CommitStatus] =
byCommitStatues(userName, repositoryName, sha).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
protected def byCommitStatues(userName: String, repositoryName: String, sha: String)(implicit s: Session) =
CommitStatuses.filter(t => t.byCommit(userName, repositoryName, sha) ).sortBy(_.updatedDate desc)
}

View File

@@ -1,14 +1,16 @@
package gitbucket.core.service
import gitbucket.core.model._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
import gitbucket.core.model.Profile._
import profile.simple._
import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.model._
import scala.slick.jdbc.{StaticQuery => Q}
import Q.interpolation
trait IssuesService {
import IssuesService._
@@ -20,6 +22,13 @@ trait IssuesService {
def getComments(owner: String, repository: String, issueId: Int)(implicit s: Session) =
IssueComments filter (_.byIssue(owner, repository, issueId)) list
/** @return IssueComment and commentedUser */
def getCommentsForApi(owner: String, repository: String, issueId: Int)(implicit s: Session): List[(IssueComment, Account)] =
IssueComments.filter(_.byIssue(owner, repository, issueId))
.filter(_.action inSetBind Set("comment" , "close_comment", "reopen_comment"))
.innerJoin(Accounts).on( (t1, t2) => t1.commentedUserName === t2.userName )
.list
def getComment(owner: String, repository: String, commentId: String)(implicit s: Session) =
if (commentId forall (_.isDigit))
IssueComments filter { t =>
@@ -78,6 +87,47 @@ trait IssuesService {
.toMap
}
def getCommitStatues(issueList:Seq[(String, String, Int)])(implicit s: Session) :Map[(String, String, Int), CommitStatusInfo] ={
if(issueList.isEmpty){
Map.empty
}else{
import scala.slick.jdbc._
val issueIdQuery = issueList.map(i => "(PR.USER_NAME=? AND PR.REPOSITORY_NAME=? AND PR.ISSUE_ID=?)").mkString(" OR ")
implicit val qset = SetParameter[Seq[(String, String, Int)]] {
case (seq, pp) =>
for (a <- seq) {
pp.setString(a._1)
pp.setString(a._2)
pp.setInt(a._3)
}
}
import gitbucket.core.model.Profile.commitStateColumnType
val query = Q.query[Seq[(String, String, Int)], (String, String, Int, Int, Int, Option[String], Option[CommitState], Option[String], Option[String])](s"""
SELECT SUMM.USER_NAME, SUMM.REPOSITORY_NAME, SUMM.ISSUE_ID, CS_ALL, CS_SUCCESS
, CSD.CONTEXT, CSD.STATE, CSD.TARGET_URL, CSD.DESCRIPTION
FROM (SELECT
PR.USER_NAME
, PR.REPOSITORY_NAME
, PR.ISSUE_ID
, COUNT(CS.STATE) AS CS_ALL
, SUM(CS.STATE='success') AS CS_SUCCESS
, PR.COMMIT_ID_TO AS COMMIT_ID
FROM PULL_REQUEST PR
JOIN COMMIT_STATUS CS
ON PR.USER_NAME=CS.USER_NAME
AND PR.REPOSITORY_NAME=CS.REPOSITORY_NAME
AND PR.COMMIT_ID_TO=CS.COMMIT_ID
WHERE $issueIdQuery
GROUP BY PR.USER_NAME, PR.REPOSITORY_NAME, PR.ISSUE_ID) as SUMM
LEFT OUTER JOIN COMMIT_STATUS CSD
ON SUMM.CS_ALL = 1 AND SUMM.COMMIT_ID = CSD.COMMIT_ID""");
query(issueList).list.map{
case(userName, repositoryName, issueId, count, successCount, context, state, targetUrl, description) =>
(userName, repositoryName, issueId) -> CommitStatusInfo(count, successCount, context, state, targetUrl, description)
}.toMap
}
}
/**
* Returns the search result against issues.
*
@@ -90,8 +140,53 @@ trait IssuesService {
*/
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels
val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
val status = getCommitStatues(result.map(_.head._1).map(is => (is.userName, is.repositoryName, is.issueId)))
result.map { issues => issues.head match {
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount,
status.get(issue.userName, issue.repositoryName, issue.issueId))
}} toList
}
/** for api
* @return (issue, issueUser, commentCount, pullRequest, headRepo, headOwner)
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[(Issue, Account, Int, PullRequest, Repository, Account)] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, true, offset, limit, repos)
.innerJoin(PullRequests).on { case ((t1, t2), t3) => t3.byPrimaryKey(t1.userName, t1.repositoryName, t1.issueId) }
.innerJoin(Repositories).on { case (((t1, t2), t3), t4) => t4.byRepository(t1.userName, t1.repositoryName) }
.innerJoin(Accounts).on { case ((((t1, t2), t3), t4), t5) => t5.userName === t1.openedUserName }
.innerJoin(Accounts).on { case (((((t1, t2), t3), t4), t5), t6) => t6.userName === t4.userName }
.map { case (((((t1, t2), t3), t4), t5), t6) =>
(t1, t5, t2.commentCount, t3, t4, t6)
}
.list
}
private def searchIssueQueryBase(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: Seq[(String, String)])
(implicit s: Session) =
searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) =>
@@ -107,28 +202,7 @@ trait IssuesService {
}
}
.drop(offset).take(limit)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
.map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
}
.list
.splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId
}
.map { issues => issues.head match {
case (issue, commentCount, _, _, _, milestone) =>
IssueInfo(issue,
issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList,
milestone,
commentCount)
}} toList
}
/**
* Assembles query for conditional issue searching.
@@ -462,6 +536,8 @@ object IssuesService {
}
}
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
case class CommitStatusInfo(count: Int, successCount: Int, context: Option[String], state: Option[CommitState], targetUrl: Option[String], description: Option[String])
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int, status:Option[CommitStatusInfo])
}

View File

@@ -0,0 +1,172 @@
package gitbucket.core.service
import gitbucket.core.model.Account
import gitbucket.core.util.LockUtil
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.ControlUtil._
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.revwalk.RevWalk
trait MergeService {
import MergeService._
/**
* Checks whether conflict will be caused in merging within pull request.
* Returns true if conflict will be caused.
*/
def checkConflict(userName: String, repositoryName: String, branch: String, issueId: Int): Boolean = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflict()
}
}
/**
* Checks whether conflict will be caused in merging within pull request.
* only cache check.
* Returns Some(true) if conflict will be caused.
* Returns None if cache has not created yet.
*/
def checkConflictCache(userName: String, repositoryName: String, branch: String, issueId: Int): Option[Boolean] = {
using(Git.open(getRepositoryDir(userName, repositoryName))) { git =>
MergeCacheInfo(git, branch, issueId).checkConflictCache()
}
}
/** merge pull request */
def mergePullRequest(git:Git, branch: String, issueId: Int, message:String, committer:PersonIdent): Unit = {
MergeCacheInfo(git, branch, issueId).merge(message, committer)
}
/** fetch remote branch to my repository refs/pull/{issueId}/head */
def fetchAsPullRequest(userName: String, repositoryName: String, requestUserName: String, requestRepositoryName: String, requestBranch:String, issueId:Int){
using(Git.open(getRepositoryDir(userName, repositoryName))){ git =>
git.fetch
.setRemote(getRepositoryDir(requestUserName, requestRepositoryName).toURI.toString)
.setRefSpecs(new RefSpec(s"refs/heads/${requestBranch}:refs/pull/${issueId}/head"))
.call
}
}
/**
* 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}"
val refSpec = new RefSpec(s"${remoteRefName}:${tmpRefName}").setForceUpdate(true)
try {
// fetch objects from origin repository branch
git.fetch
.setRemote(getRepositoryDir(userName, repositoryName).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 mergeTip = git.getRepository.resolve(tmpRefName)
try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
} finally {
val refUpdate = git.getRepository.updateRef(refSpec.getDestination)
refUpdate.setForceUpdate(true)
refUpdate.delete()
}
}
}
}
object MergeService{
case class MergeCacheInfo(git:Git, branch:String, issueId:Int){
val repository = git.getRepository
val mergedBranchName = s"refs/pull/${issueId}/merge"
val conflictedBranchName = s"refs/pull/${issueId}/conflict"
lazy val mergeBaseTip = repository.resolve(s"refs/heads/${branch}")
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 )){
// merged branch exists
Some(false)
}else{
None
}
}.orElse(Option(repository.resolve(conflictedBranchName)).flatMap{ conflicted =>
if(parseCommit( conflicted ).getParents().toSet == Set( mergeBaseTip, mergeTip )){
// conflict branch exists
Some(true)
}else{
None
}
})
}
def checkConflict():Boolean ={
checkConflictCache.getOrElse(checkConflictForce)
}
def checkConflictForce():Boolean ={
val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
val conflicted = try {
!merger.merge(mergeBaseTip, mergeTip)
} catch {
case e: NoMergeBaseException => true
}
val mergeTipCommit = using(new RevWalk( repository ))(_.parseCommit( mergeTip ))
val committer = mergeTipCommit.getCommitterIdent;
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()
}
if(!conflicted){
updateBranch(merger.getResultTreeId, s"Merge ${mergeTip.name} into ${mergeBaseTip.name}", mergedBranchName)
git.branchDelete().setForce(true).setBranchNames(conflictedBranchName).call()
}else{
updateBranch(mergeTipCommit.getTree().getId(), s"can't merge ${mergeTip.name} into ${mergeBaseTip.name}", conflictedBranchName)
git.branchDelete().setForce(true).setBranchNames(mergedBranchName).call()
}
conflicted
}
// update branch from cache
def merge(message:String, committer:PersonIdent) = {
if(checkConflict()){
throw new RuntimeException("This pull request can't merge automatically.")
}
val mergeResultCommit = parseCommit( Option(repository.resolve(mergedBranchName)).getOrElse(throw new RuntimeException(s"not found branch ${mergedBranchName}")) )
// 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()
}
// 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 parseCommit(id:ObjectId) = using(new RevWalk( repository ))(_.parseCommit(id))
}
}

View File

@@ -1,10 +1,11 @@
package gitbucket.core.service
import gitbucket.core.model.{Issue, PullRequest}
import gitbucket.core.model.{Account, Issue, PullRequest, WebHook}
import gitbucket.core.model.Profile._
import gitbucket.core.util.JGitUtil
import profile.simple._
trait PullRequestService { self: IssuesService =>
import PullRequestService._
@@ -82,6 +83,28 @@ trait PullRequestService { self: IssuesService =>
.map { case (t1, t2) => t1 }
.list
/**
* for repository viewer.
* 1. find pull request from from `branch` to othre branch on same repository
* 1. return if exists pull request to `defaultBranch`
* 2. return if exists pull request to othre branch
* 2. return None
*/
def getPullRequestFromBranch(userName: String, repositoryName: String, branch: String, defaultBranch: String)
(implicit s: Session): Option[(PullRequest, Issue)] =
PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) }
.filter { case (t1, t2) =>
(t1.requestUserName === userName.bind) &&
(t1.requestRepositoryName === repositoryName.bind) &&
(t1.requestBranch === branch.bind) &&
(t1.userName === userName.bind) &&
(t1.repositoryName === repositoryName.bind) &&
(t2.closed === false.bind)
}
.sortBy{ case (t1, t2) => t1.branch =!= defaultBranch.bind }
.firstOption
/**
* Fetch pull request contents into refs/pull/${issueId}/head and update pull request table.
*/

View File

@@ -55,6 +55,7 @@ trait RepositoryService { self: AccountService =>
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
Repositories.filter { t =>
@@ -95,6 +96,7 @@ trait RepositoryService { self: AccountService =>
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)) :_*)
// Convert labelId
val oldLabelMap = labels.map(x => (x.labelId, x.labelName)).toMap
@@ -395,5 +397,4 @@ object RepositoryService {
}
case class RepositoryTreeNode(owner: String, name: String, children: List[RepositoryTreeNode])
}

View File

@@ -19,6 +19,7 @@ trait SystemSettingsService {
props.setProperty(IsCreateRepoOptionPublic, settings.isCreateRepoOptionPublic.toString)
props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString)
settings.activityLogLimit.foreach(x => props.setProperty(ActivityLogLimit, x.toString))
props.setProperty(Ssh, settings.ssh.toString)
settings.sshPort.foreach(x => props.setProperty(SshPort, x.toString))
if(settings.notification) {
@@ -65,12 +66,13 @@ trait SystemSettingsService {
}
SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getOptionValue(props, Information, None),
getValue(props, AllowAccountRegistration, false),
getValue(props, AllowAnonymousAccess, true),
getValue(props, IsCreateRepoOptionPublic, true),
getValue(props, Gravatar, true),
getValue(props, Notification, false),
getOptionValue[Int](props, ActivityLogLimit, None),
getValue(props, Ssh, false),
getOptionValue(props, SshPort, Some(DefaultSshPort)),
if(getValue(props, Notification, false)){
@@ -120,6 +122,7 @@ object SystemSettingsService {
isCreateRepoOptionPublic: Boolean,
gravatar: Boolean,
notification: Boolean,
activityLogLimit: Option[Int],
ssh: Boolean,
sshPort: Option[Int],
smtp: Option[Smtp],
@@ -166,6 +169,7 @@ object SystemSettingsService {
private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
private val Gravatar = "gravatar"
private val Notification = "notification"
private val ActivityLogLimit = "activity_log_limit"
private val Ssh = "ssh"
private val SshPort = "ssh.port"
private val SmtpHost = "smtp.host"

View File

@@ -1,18 +1,19 @@
package gitbucket.core.service
import gitbucket.core.model.{WebHook, Account}
import gitbucket.core.api._
import gitbucket.core.model.{WebHook, Account, Issue, PullRequest, IssueComment}
import gitbucket.core.model.Profile._
import gitbucket.core.service.RepositoryService.RepositoryInfo
import gitbucket.core.util.JGitUtil
import profile.simple._
import org.slf4j.LoggerFactory
import RepositoryService.RepositoryInfo
import org.eclipse.jgit.diff.DiffEntry
import JGitUtil.CommitInfo
import org.eclipse.jgit.api.Git
import org.apache.http.message.BasicNameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util.RepositoryName
import gitbucket.core.service.RepositoryService.RepositoryInfo
import org.apache.http.NameValuePair
import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.message.BasicNameValuePair
import org.eclipse.jgit.api.Git
import org.slf4j.LoggerFactory
trait WebHookService {
import WebHookService._
@@ -28,26 +29,29 @@ trait WebHookService {
def deleteWebHookURL(owner: String, repository: String, url :String)(implicit s: Session): Unit =
WebHooks.filter(_.byPrimaryKey(owner, repository, url)).delete
def callWebHook(owner: String, repository: String, webHookURLs: List[WebHook], payload: WebHookPayload): Unit = {
import org.json4s._
import org.json4s.jackson.Serialization
import org.json4s.jackson.Serialization.{read, write}
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, _))
}
}
def callWebHook(eventName: String, webHookURLs: List[WebHook], payload: WebHookPayload)(implicit c: JsonFormat.Context): Unit = {
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClientBuilder
import scala.concurrent._
import ExecutionContext.Implicits.global
logger.debug("start callWebHook")
implicit val formats = Serialization.formats(NoTypeHints)
if(webHookURLs.nonEmpty){
val json = write(payload)
val json = JsonFormat(payload)
val httpClient = HttpClientBuilder.create.build
webHookURLs.foreach { webHookUrl =>
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 params: java.util.List[NameValuePair] = new java.util.ArrayList()
params.add(new BasicNameValuePair("payload", json))
@@ -67,78 +71,209 @@ trait WebHookService {
}
logger.debug("end callWebHook")
}
}
trait WebHookPullRequestService extends WebHookService {
self: AccountService with RepositoryService with PullRequestService with IssuesService =>
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"){
val users = getAccountsByUserNames(Set(repository.owner, issue.openedUserName), Set(sender))
for{
repoOwner <- users.get(repository.owner)
issueUser <- users.get(issue.openedUserName)
} yield {
WebHookIssuesPayload(
action = action,
number = issue.issueId,
repository = ApiRepository(repository, ApiUser(repoOwner)),
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
sender = ApiUser(sender))
}
}
}
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"){
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)
} yield {
WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
baseRepository = repository,
baseOwner = baseOwner,
sender = sender)
}
}
}
/** @return Map[(issue, issueUser, pullRequest, baseOwner, headOwner), webHooks] */
def getPullRequestsByRequestForWebhook(userName:String, repositoryName:String, branch:String)
(implicit s: Session): Map[(Issue, Account, PullRequest, Account, Account), List[WebHook]] =
(for{
is <- Issues if is.closed === false.bind
pr <- PullRequests if pr.byPrimaryKey(is.userName, is.repositoryName, is.issueId)
if pr.requestUserName === userName.bind
if pr.requestRepositoryName === repositoryName.bind
if pr.requestBranch === branch.bind
bu <- Accounts if bu.userName === pr.userName
ru <- Accounts if ru.userName === pr.requestUserName
iu <- Accounts if iu.userName === is.openedUserName
wh <- WebHooks if wh.byRepository(is.userName , is.repositoryName)
} 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 = {
import WebHookService._
for{
((issue, issueUser, pullRequest, baseOwner, headOwner), webHooks) <- getPullRequestsByRequestForWebhook(requestRepository.owner, requestRepository.name, requestBranch)
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName, baseUrl)
} yield {
val payload = WebHookPullRequestPayload(
action = action,
issue = issue,
issueUser = issueUser,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
baseRepository = baseRepo,
baseOwner = baseOwner,
sender = sender)
callWebHook("pull_request", webHooks, payload)
}
}
}
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"){
for{
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
users = getAccountsByUserNames(Set(issue.openedUserName, repository.owner, issueComment.commentedUserName), Set(sender))
issueUser <- users.get(issue.openedUserName)
repoOwner <- users.get(repository.owner)
commenter <- users.get(issueComment.commentedUserName)
} yield {
WebHookIssueCommentPayload(
issue = issue,
issueUser = issueUser,
comment = issueComment,
commentUser = commenter,
repository = repository,
repositoryUser = repoOwner,
sender = sender)
}
}
}
}
object WebHookService {
trait WebHookPayload
case class WebHookPayload(
pusher: WebHookUser,
// https://developer.github.com/v3/activity/events/types/#pushevent
case class WebHookPushPayload(
pusher: ApiUser,
ref: String,
commits: List[WebHookCommit],
repository: WebHookRepository)
commits: List[ApiCommit],
repository: ApiRepository
) extends WebHookPayload
object WebHookPayload {
object WebHookPushPayload {
def apply(git: Git, pusher: Account, refName: String, repositoryInfo: RepositoryInfo,
commits: List[CommitInfo], repositoryOwner: Account): WebHookPayload =
WebHookPayload(
WebHookUser(pusher.fullName, pusher.mailAddress),
commits: List[CommitInfo], repositoryOwner: Account): WebHookPushPayload =
WebHookPushPayload(
ApiUser(pusher),
refName,
commits.map { commit =>
val diffs = JGitUtil.getDiffs(git, commit.id, false)
val commitUrl = repositoryInfo.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/commit/" + commit.id
WebHookCommit(
id = commit.id,
message = commit.fullMessage,
timestamp = commit.commitTime.toString,
url = commitUrl,
added = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.ADD) => x.newPath },
removed = diffs._1.collect { case x if(x.changeType == DiffEntry.ChangeType.DELETE) => x.oldPath },
modified = diffs._1.collect { case x if(x.changeType != DiffEntry.ChangeType.ADD &&
x.changeType != DiffEntry.ChangeType.DELETE) => x.newPath },
author = WebHookUser(
name = commit.committerName,
email = commit.committerEmailAddress
)
)
},
WebHookRepository(
name = repositoryInfo.name,
url = repositoryInfo.httpUrl,
description = repositoryInfo.repository.description.getOrElse(""),
watchers = 0,
forks = repositoryInfo.forkedCount,
`private` = repositoryInfo.repository.isPrivate,
owner = WebHookUser(
name = repositoryOwner.userName,
email = repositoryOwner.mailAddress
)
commits.map{ commit => ApiCommit(git, RepositoryName(repositoryInfo), commit) },
ApiRepository(
repositoryInfo,
owner= ApiUser(repositoryOwner)
)
)
}
case class WebHookCommit(
id: String,
message: String,
timestamp: String,
url: String,
added: List[String],
removed: List[String],
modified: List[String],
author: WebHookUser)
// https://developer.github.com/v3/activity/events/types/#issuesevent
case class WebHookIssuesPayload(
action: String,
number: Int,
repository: ApiRepository,
issue: ApiIssue,
sender: ApiUser) extends WebHookPayload
case class WebHookRepository(
name: String,
url: String,
description: String,
watchers: Int,
forks: Int,
`private`: Boolean,
owner: WebHookUser)
// https://developer.github.com/v3/activity/events/types/#pullrequestevent
case class WebHookPullRequestPayload(
action: String,
number: Int,
repository: ApiRepository,
pull_request: ApiPullRequest,
sender: ApiUser
) extends WebHookPayload
case class WebHookUser(
name: String,
email: String)
object WebHookPullRequestPayload{
def apply(action: String,
issue: Issue,
issueUser: Account,
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
baseRepository: RepositoryInfo,
baseOwner: Account,
sender: Account): WebHookPullRequestPayload = {
val headRepoPayload = ApiRepository(headRepository, headOwner)
val baseRepoPayload = ApiRepository(baseRepository, baseOwner)
val senderPayload = ApiUser(sender)
val pr = ApiPullRequest(issue, pullRequest, headRepoPayload, baseRepoPayload, ApiUser(issueUser))
WebHookPullRequestPayload(
action = action,
number = issue.issueId,
repository = pr.base.repo,
pull_request = pr,
sender = senderPayload
)
}
}
// https://developer.github.com/v3/activity/events/types/#issuecommentevent
case class WebHookIssueCommentPayload(
action: String,
repository: ApiRepository,
issue: ApiIssue,
comment: ApiComment,
sender: ApiUser
) extends WebHookPayload
object WebHookIssueCommentPayload{
def apply(
issue: Issue,
issueUser: Account,
comment: IssueComment,
commentUser: Account,
repository: RepositoryInfo,
repositoryUser: Account,
sender: Account): WebHookIssueCommentPayload =
WebHookIssueCommentPayload(
action = "created",
repository = ApiRepository(repository, repositoryUser),
issue = ApiIssue(issue, RepositoryName(repository), ApiUser(issueUser)),
comment = ApiComment(comment, RepositoryName(repository), issue.issueId, ApiUser(commentUser)),
sender = ApiUser(sender))
}
}

View File

@@ -202,7 +202,7 @@ trait WikiService {
}
/**
* Save the wiki page.
* Save the wiki page and return the commit id.
*/
def saveWikiPage(owner: String, repository: String, currentPageName: String, newPageName: String,
content: String, committer: Account, message: String, currentId: Option[String]): Option[String] = {

View File

@@ -0,0 +1,43 @@
package gitbucket.core.servlet
import javax.servlet._
import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
import gitbucket.core.model.Account
import gitbucket.core.service.AccessTokenService
import gitbucket.core.util.Keys
import org.scalatra.servlet.ServletApiImplicits._
import org.scalatra._
class AccessTokenAuthenticationFilter extends Filter with AccessTokenService {
private val tokenHeaderPrefix = "token "
override def init(filterConfig: FilterConfig): Unit = {}
override def destroy(): Unit = {}
override def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
implicit val request = req.asInstanceOf[HttpServletRequest]
implicit val session = req.getAttribute(Keys.Request.DBSession).asInstanceOf[slick.jdbc.JdbcBackend#Session]
val response = res.asInstanceOf[HttpServletResponse]
Option(request.getHeader("Authorization")).map{
case auth if auth.startsWith("token ") => AccessTokenService.getAccountByAccessToken(auth.substring(6).trim).toRight(Unit)
// TODO Basic Authentication Support
case _ => Left(Unit)
}.orElse{
Option(request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]).map(Right(_))
} match {
case Some(Right(account)) => request.setAttribute(Keys.Session.LoginAccount, account); chain.doFilter(req, res)
case None => chain.doFilter(req, res)
case Some(Left(_)) => {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED)
response.setContentType("Content-Type: application/json; charset=utf-8")
val w = response.getWriter()
w.print("""{ "message": "Bad credentials" }""")
w.close()
}
}
}
}

View File

@@ -0,0 +1,169 @@
package gitbucket.core.servlet
import java.io.File
import java.sql.{DriverManager, Connection}
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.service.SystemSettingsService
import gitbucket.core.util._
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.slf4j.LoggerFactory
import Directory._
import ControlUtil._
import JDBCUtil._
import org.eclipse.jgit.api.Git
import gitbucket.core.util.Versions
import gitbucket.core.util.Directory
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 4),
new Version(3, 3),
new Version(3, 2),
new Version(3, 1),
new Version(3, 0),
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
val repoName = rs.getString("REPOSITORY_NAME")
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
val oldDir = new File(newDir.getParentFile, "issues")
if(oldDir.exists && oldDir.isDirectory){
oldDir.renameTo(newDir)
}
}
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
val originalUserName = rs.getString("ORIGIN_USER_NAME")
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
if(originalUserName != null && originalRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
originalUserName, originalRepoName) == 0){
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
val parentUserName = rs.getString("PARENT_USER_NAME")
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
if(parentUserName != null && parentRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
parentUserName, parentRepoName) == 0){
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
}
}
},
new Version(2, 6),
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (curInfo != newInfo) {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
ignore {
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
override def update(conn: Connection, cl: ClassLoader): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn, cl)
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
}
}
}
}
}
}
},
Version(1, 13),
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),
Version(1, 8),
Version(1, 7),
Version(1, 6),
Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
// Fix wiki repository configuration
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}
}
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0),
Version(0, 0)
)
/**
* The head version of BitBucket.
*/
val headVersion = versions.head
/**
* The version file (GITBUCKET_HOME/version).
*/
lazy val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
*/
def getCurrentVersion(): Version = {
if(versionFile.exists){
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
case Array(majorVersion, minorVersion) => {
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
}.getOrElse(Version(0, 0))
}
case _ => Version(0, 0)
}
} else Version(0, 0)
}
}

View File

@@ -28,7 +28,6 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
}
val isUpdating = request.getRequestURI.endsWith("/git-receive-pack") || "service=git-receive-pack".equals(request.getQueryString)
val settings = loadSystemSettings()
try {
@@ -41,16 +40,22 @@ class BasicAuthenticationFilter extends Filter with RepositoryService with Accou
} else {
request.getHeader("Authorization") match {
case null => requireAuth(response)
case auth => decodeAuthHeader(auth).split(":") match {
case auth => decodeAuthHeader(auth).split(":", 2) match {
case Array(username, password) => {
authenticate(settings, username, password) match {
case Some(account) => {
if(isUpdating && hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
if (isUpdating || repository.repository.isPrivate) {
if(hasWritePermission(repository.owner, repository.name, Some(account))){
request.setAttribute(Keys.Request.UserName, account.userName)
chain.doFilter(req, wrappedResponse)
} else {
requireAuth(response)
}
} else {
chain.doFilter(req, wrappedResponse)
}
chain.doFilter(req, wrappedResponse)
}
case None => requireAuth(response)
case _ => requireAuth(response)
}
}
case _ => requireAuth(response)

View File

@@ -1,8 +1,16 @@
package gitbucket.core.servlet
import gitbucket.core.api
import gitbucket.core.model.Session
import gitbucket.core.service.IssuesService.IssueSearchCondition
import gitbucket.core.service.WebHookService._
import gitbucket.core.service._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.JGitUtil.CommitInfo
import gitbucket.core.util._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.http.server.GitServlet
import org.eclipse.jgit.lib._
import org.eclipse.jgit.transport._
@@ -12,13 +20,7 @@ import org.slf4j.LoggerFactory
import javax.servlet.ServletConfig
import javax.servlet.ServletContext
import javax.servlet.http.{HttpServletResponse, HttpServletRequest}
import gitbucket.core.util.StringUtil
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import WebHookService._
import org.eclipse.jgit.api.Git
import JGitUtil.CommitInfo
import IssuesService.IssueSearchCondition
/**
* Provides Git repository via HTTP.
@@ -98,7 +100,8 @@ 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 RepositoryService with AccountService with IssuesService with ActivityService with PullRequestService with WebHookService
with WebHookPullRequestService {
private val logger = LoggerFactory.getLogger(classOf[CommitLogHook])
private var existIds: Seq[String] = Nil
@@ -122,6 +125,7 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
val pushedIds = scala.collection.mutable.Set[String]()
commands.asScala.foreach { command =>
logger.debug(s"commandType: ${command.getType}, refName: ${command.getRefName}")
implicit val apiContext = api.JsonFormat.Context(baseUrl)
val refName = command.getRefName.split("/")
val branchName = refName.drop(2).mkString("/")
val commits = if (refName(1) == "tags") {
@@ -138,8 +142,10 @@ 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
// Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
val defaultBranch = repositoryInfo.repository.defaultBranch
val newCommits = commits.flatMap { commit =>
if (!existIds.contains(commit.id) && !pushedIds.contains(commit.id)) {
if (issueCount > 0) {
@@ -176,20 +182,19 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
ReceiveCommand.Type.UPDATE |
ReceiveCommand.Type.UPDATE_NONFASTFORWARD =>
updatePullRequests(owner, repository, branchName)
getAccountByUserName(pusher).map{ pusherAccount =>
callPullRequestWebHookByRequestBranch("synchronize", repositoryInfo, branchName, baseUrl, pusherAccount)
}
case _ =>
}
}
// call web hook
getWebHookURLs(owner, repository) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner);
repositoryInfo <- getRepository(owner, repository, baseUrl)){
callWebHook(owner, repository, webHookURLs,
WebHookPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount))
}
case _ =>
callWebHookOf(owner, repository, "push"){
for(pusherAccount <- getAccountByUserName(pusher);
ownerAccount <- getAccountByUserName(owner)) yield {
WebHookPushPayload(git, pusherAccount, command.getRefName, repositoryInfo, newCommits, ownerAccount)
}
}
}
}

View File

@@ -1,175 +1,22 @@
package gitbucket.core.servlet
import java.io.File
import java.sql.{DriverManager, Connection}
import akka.event.Logging
import com.typesafe.config.ConfigFactory
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.util._
import gitbucket.core.service.{ActivityService, SystemSettingsService}
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
import org.slf4j.LoggerFactory
import Directory._
import ControlUtil._
import JDBCUtil._
import org.eclipse.jgit.api.Git
import gitbucket.core.util.Versions
import gitbucket.core.util.Directory
import gitbucket.core.plugin._
object AutoUpdate {
/**
* The history of versions. A head of this sequence is the current BitBucket version.
*/
val versions = Seq(
new Version(3, 0),
new Version(2, 8),
new Version(2, 7) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
val repoName = rs.getString("REPOSITORY_NAME")
defining(Directory.getAttachedDir(userName, repoName)){ newDir =>
val oldDir = new File(newDir.getParentFile, "issues")
if(oldDir.exists && oldDir.isDirectory){
oldDir.renameTo(newDir)
}
}
// Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist
val originalUserName = rs.getString("ORIGIN_USER_NAME")
val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME")
if(originalUserName != null && originalRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
originalUserName, originalRepoName) == 0){
conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
// Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist
val parentUserName = rs.getString("PARENT_USER_NAME")
val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME")
if(parentUserName != null && parentRepoName != null){
if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?",
parentUserName, parentRepoName) == 0){
conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " +
"WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName)
}
}
}
}
},
new Version(2, 6),
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
if (curInfo != newInfo) {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
ignore {
FileUtils.deleteDirectory(Directory.getPluginCacheDir())
//FileUtils.deleteDirectory(new File(Directory.PluginHome))
}
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
override def update(conn: Connection, cl: ClassLoader): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
super.update(conn, cl)
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
dir.listFiles.foreach { file =>
if(file.getName.indexOf('.') < 0){
val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString
if(mimeType.startsWith("image/")){
file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1)))
}
}
}
}
}
}
}
},
Version(1, 13),
Version(1, 12),
Version(1, 11),
Version(1, 10),
Version(1, 9),
Version(1, 8),
Version(1, 7),
Version(1, 6),
Version(1, 5),
Version(1, 4),
new Version(1, 3){
override def update(conn: Connection, cl: ClassLoader): Unit = {
super.update(conn, cl)
// Fix wiki repository configuration
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
defining(git.getRepository.getConfig){ config =>
if(!config.getBoolean("http", "receivepack", false)){
config.setBoolean("http", null, "receivepack", true)
config.save
}
}
}
}
}
},
Version(1, 2),
Version(1, 1),
Version(1, 0),
Version(0, 0)
)
/**
* The head version of BitBucket.
*/
val headVersion = versions.head
/**
* The version file (GITBUCKET_HOME/version).
*/
lazy val versionFile = new File(GitBucketHome, "version")
/**
* Returns the current version from the version file.
*/
def getCurrentVersion(): Version = {
if(versionFile.exists){
FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match {
case Array(majorVersion, minorVersion) => {
versions.find { v =>
v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt
}.getOrElse(Version(0, 0))
}
case _ => Version(0, 0)
}
} else Version(0, 0)
}
}
import akka.actor.{Actor, Props, ActorSystem}
import com.typesafe.akka.extension.quartz.QuartzSchedulerExtension
import AutoUpdate._
/**
* Initialize GitBucket system.
* Update database schema and load plug-ins automatically in the context initializing.
*/
class InitializeListener extends ServletContextListener {
import AutoUpdate._
class InitializeListener extends ServletContextListener with SystemSettingsService {
private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
@@ -180,28 +27,62 @@ class InitializeListener extends ServletContextListener {
}
org.h2.Driver.load()
defining(getConnection()){ conn =>
Database() withTransaction { session =>
val conn = session.conn
// Migration
logger.debug("Start schema update")
Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
}
// Load plugins
logger.debug("Initialize plugins")
PluginRegistry.initialize(event.getServletContext, conn)
PluginRegistry.initialize(event.getServletContext, loadSystemSettings(), conn)
}
// Start Quartz scheduler
val system = ActorSystem("job", ConfigFactory.parseString(
"""
|akka {
| quartz {
| schedules {
| Daily {
| expression = "0 0 0 * * ?"
| }
| }
| }
|}
""".stripMargin))
val scheduler = QuartzSchedulerExtension(system)
scheduler.schedule("Daily", system.actorOf(Props[DeleteOldActivityActor]), "DeleteOldActivity")
}
def contextDestroyed(event: ServletContextEvent): Unit = {
override def contextDestroyed(event: ServletContextEvent): Unit = {
// Shutdown plugins
PluginRegistry.shutdown(event.getServletContext)
PluginRegistry.shutdown(event.getServletContext, loadSystemSettings())
// Close datasource
Database.closeDataSource()
}
private def getConnection(): Connection =
DriverManager.getConnection(
DatabaseConfig.url,
DatabaseConfig.user,
DatabaseConfig.password)
}
class DeleteOldActivityActor extends Actor with SystemSettingsService with ActivityService {
private val logger = Logging(context.system, this)
def receive = {
case s: String => {
loadSystemSettings().activityLogLimit.foreach { limit =>
if(limit > 0){
Database() withTransaction { implicit session =>
val rows = deleteOldActivities(limit)
logger.info(s"Deleted ${rows} activity logs")
}
}
}
}
}
}

View File

@@ -4,6 +4,7 @@ import javax.servlet._
import javax.servlet.http.HttpServletRequest
import com.mchange.v2.c3p0.ComboPooledDataSource
import gitbucket.core.util.DatabaseConfig
import org.scalatra.ScalatraBase
import org.slf4j.LoggerFactory
import slick.jdbc.JdbcBackend.{Database => SlickDatabase, Session}
import gitbucket.core.util.Keys
@@ -20,11 +21,17 @@ class TransactionFilter extends Filter {
def destroy(): Unit = {}
def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = {
if(req.asInstanceOf[HttpServletRequest].getRequestURI().startsWith("/assets/")){
if(req.asInstanceOf[HttpServletRequest].getServletPath().startsWith("/assets/")){
// assets don't need transaction
chain.doFilter(req, res)
} else {
Database() withTransaction { session =>
// Register Scalatra error callback to rollback transaction
ScalatraBase.onFailure { _ =>
logger.debug("Rolled back transaction")
session.rollback()
}(req.asInstanceOf[HttpServletRequest])
logger.debug("begin transaction")
req.setAttribute(Keys.Request.DBSession, session)
chain.doFilter(req, res)
@@ -39,17 +46,18 @@ object Database {
private val logger = LoggerFactory.getLogger(Database.getClass)
private val db: SlickDatabase = {
val datasource = new ComboPooledDataSource
datasource.setDriverClass(DatabaseConfig.driver)
datasource.setJdbcUrl(DatabaseConfig.url)
datasource.setUser(DatabaseConfig.user)
datasource.setPassword(DatabaseConfig.password)
private val dataSource: ComboPooledDataSource = {
val ds = new ComboPooledDataSource
ds.setDriverClass(DatabaseConfig.driver)
ds.setJdbcUrl(DatabaseConfig.url)
ds.setUser(DatabaseConfig.user)
ds.setPassword(DatabaseConfig.password)
logger.debug("load database connection pool")
ds
}
SlickDatabase.forDataSource(datasource)
private val db: SlickDatabase = {
SlickDatabase.forDataSource(dataSource)
}
def apply(): SlickDatabase = db
@@ -57,4 +65,6 @@ object Database {
def getSession(req: ServletRequest): Session =
req.getAttribute(Keys.Request.DBSession).asInstanceOf[Session]
def closeDataSource(): Unit = dataSource.close
}

View File

@@ -1,11 +1,16 @@
package gitbucket.core.util
import gitbucket.core.api.JsonFormat
import gitbucket.core.controller.Context
import gitbucket.core.servlet.Database
import javax.servlet.http.{HttpSession, HttpServletRequest}
import scala.util.matching.Regex
import scala.util.control.Exception._
import slick.jdbc.JdbcBackend
import javax.servlet.http.{HttpSession, HttpServletRequest}
/**
* Provides some usable implicit conversions.
@@ -15,6 +20,8 @@ object Implicits {
// Convert to slick session.
implicit def request2Session(implicit request: HttpServletRequest): JdbcBackend#Session = Database.getSession(request)
implicit def context2ApiJsonFormatContext(implicit context: Context): JsonFormat.Context = JsonFormat.Context(context.baseUrl)
implicit class RichSeq[A](seq: Seq[A]) {
def splitWith(condition: (A, A) => Boolean): Seq[Seq[A]] = split(seq)(condition)
@@ -56,7 +63,10 @@ object Implicits {
implicit class RichRequest(request: HttpServletRequest){
def paths: Array[String] = request.getRequestURI.substring(request.getContextPath.length + 1).split("/")
def paths: Array[String] = (request.getRequestURI.substring(request.getContextPath.length + 1) match{
case path if path.startsWith("api/v3/repos/") => path.substring(13/* "/api/v3/repos".length */)
case path => path
}).split("/")
def hasQueryString: Boolean = request.getQueryString != null

View File

@@ -100,7 +100,8 @@ object JGitUtil {
def isDifferentFromAuthor: Boolean = authorName != committerName || authorEmailAddress != committerEmailAddress
}
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String])
case class DiffInfo(changeType: ChangeType, oldPath: String, newPath: String, oldContent: Option[String], newContent: Option[String],
oldIsImage: Boolean, newIsImage: Boolean, oldObjectId: Option[String], newObjectId: Option[String])
/**
* The file content data for the file content view of the repository viewer.
@@ -138,6 +139,9 @@ object JGitUtil {
case class BranchInfo(name: String, committerName: String, commitTime: Date, committerEmailAddress:String, mergeInfo: Option[BranchMergeInfo], commitId: String)
case class BlameInfo(id: String, authorName: String, authorEmailAddress: String, authorTime:java.util.Date,
prev: Option[String], prevPath: Option[String], commitTime:java.util.Date, message:String, lines:Set[Int])
/**
* Returns RevCommit from the commit or tag id.
*
@@ -176,7 +180,7 @@ object JGitUtil {
git.tagList.call.asScala.map { ref =>
val revCommit = getRevCommitFromId(git, ref.getObjectId)
TagInfo(ref.getName.stripPrefix("refs/tags/"), revCommit.getCommitterIdent.getWhen, revCommit.getName)
}.toList
}.sortBy(_.time).toList
)
} catch {
// not initialized
@@ -196,80 +200,121 @@ object JGitUtil {
* @return HTML of the file list
*/
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
if(objectId==null) return Nil
val revCommit = revWalk.parseCommit(objectId)
val treeWalk = if (path == ".") {
def useTreeWalk(rev:RevCommit)(f:TreeWalk => Any): Unit = if (path == ".") {
val treeWalk = new TreeWalk(git.getRepository)
treeWalk.addTree(revCommit.getTree)
treeWalk
treeWalk.addTree(rev.getTree)
using(treeWalk)(f)
} else {
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
treeWalk.enterSubtree()
treeWalk
val treeWalk = TreeWalk.forPath(git.getRepository, path, rev.getTree)
if(treeWalk != null){
treeWalk.enterSubtree
using(treeWalk)(f)
}
}
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, Option[String], RevCommit)): (ObjectId, FileMode, String, Option[String], RevCommit) = tuple match {
case (oid, FileMode.TREE, name, _, commit ) =>
(using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(oid)
// single tree child, or None
if(walk.next() && walk.getFileMode(0) == FileMode.TREE){
Some((walk.getObjectId(0), walk.getFileMode(0), name + "/" + walk.getNameString, None, commit)).filterNot(_ => walk.next())
} else {
None
}
}) match {
case Some(child) => simplifyPath(child)
case _ => tuple
}
case _ => tuple
}
using(treeWalk) { treeWalk =>
def tupleAdd(tuple:(ObjectId, FileMode, String, Option[String]), rev:RevCommit) = tuple match {
case (oid, fmode, name, opt) => (oid, fmode, name, opt, rev)
}
@tailrec
def findLastCommits(result:List[(ObjectId, FileMode, String, Option[String], RevCommit)],
restList:List[((ObjectId, FileMode, String, Option[String]), Map[RevCommit, RevCommit])],
revIterator:java.util.Iterator[RevCommit]): List[(ObjectId, FileMode, String, Option[String], RevCommit)] ={
if(restList.isEmpty){
result
}else if(!revIterator.hasNext){ // maybe, revCommit has only 1 log. other case, restList be empty
result ++ restList.map{ case (tuple, map) => tupleAdd(tuple, map.values.headOption.getOrElse(revCommit)) }
}else{
val newCommit = revIterator.next
val (thisTimeChecks,skips) = restList.partition{ case (tuple, parentsMap) => parentsMap.contains(newCommit) }
if(thisTimeChecks.isEmpty){
findLastCommits(result, restList, revIterator)
}else{
var nextRest = skips
var nextResult = result
// Map[(name, oid), (tuple, parentsMap)]
val rest = scala.collection.mutable.Map(thisTimeChecks.map{ t => (t._1._3 -> t._1._1) -> t }:_*)
lazy val newParentsMap = newCommit.getParents.map(_ -> newCommit).toMap
useTreeWalk(newCommit){ walk =>
while(walk.next){
rest.remove(walk.getNameString -> walk.getObjectId(0)).map{ case (tuple, _) =>
if(newParentsMap.isEmpty){
nextResult +:= tupleAdd(tuple, newCommit)
}else{
nextRest +:= tuple -> newParentsMap
}
}
}
}
rest.values.map{ case (tuple, parentsMap) =>
val restParentsMap = parentsMap - newCommit
if(restParentsMap.isEmpty){
nextResult +:= tupleAdd(tuple, parentsMap(newCommit))
}else{
nextRest +:= tuple -> restParentsMap
}
}
findLastCommits(nextResult, nextRest, revIterator)
}
}
}
var fileList: List[(ObjectId, FileMode, String, Option[String])] = Nil
useTreeWalk(revCommit){ treeWalk =>
while (treeWalk.next()) {
// submodule
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
val linkUrl =if (treeWalk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == treeWalk.getPathString).map(_.url)
} else None
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
fileList +:= (treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getNameString, linkUrl)
}
list.transform(tuple =>
if (tuple._2 != FileMode.TREE)
tuple
else
simplifyPath(tuple)
)
@tailrec
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
using(new TreeWalk(git.getRepository)) { walk =>
walk.addTree(tuple._1)
while (walk.next() && list.size < 2) {
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
} else None
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
}
}
revWalk.markStart(revCommit)
val it = revWalk.iterator
val lastCommit = it.next
val nextParentsMap = Option(lastCommit).map(_.getParents.map(_ -> lastCommit).toMap).getOrElse(Map())
findLastCommits(List.empty, fileList.map(a => a -> nextParentsMap), it)
.map(simplifyPath)
.map { case (objectId, fileMode, name, linkUrl, commit) =>
FileInfo(
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName,
commit.getAuthorIdent.getWhen,
commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl)
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0
}
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
tuple
else
simplifyPath(list(0))
}
}
}.toList
}
val commits = getLatestCommitFromPaths(git, list.toList.map(_._3), revision)
list.map { case (objectId, fileMode, path, name, linkUrl) =>
defining(commits(path)){ commit =>
FileInfo(
objectId,
fileMode == FileMode.TREE || fileMode == FileMode.GITLINK,
name,
getSummaryMessage(commit.getFullMessage, commit.getShortMessage),
commit.getName,
commit.getAuthorIdent.getWhen,
commit.getAuthorIdent.getName,
commit.getAuthorIdent.getEmailAddress,
linkUrl)
}
}.sortWith { (file1, file2) =>
(file1.isDirectory, file2.isDirectory) match {
case (true , false) => true
case (false, true ) => false
case _ => file1.name.compareTo(file2.name) < 0
}
}.toList
}
/**
@@ -283,6 +328,39 @@ object JGitUtil {
}
}
/**
* get all file list by revision. only file.
*/
def getTreeId(git: Git, revision: String): Option[String] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(revision)
if(objectId==null) return None
val revCommit = revWalk.parseCommit(objectId)
Some(revCommit.getTree.name)
}
}
/**
* get all file list by tree object id.
*/
def getAllFileListByTreeId(git: Git, treeId: String): List[String] = {
using(new RevWalk(git.getRepository)){ revWalk =>
val objectId = git.getRepository.resolve(treeId+"^{tree}")
if(objectId==null) return Nil
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(objectId)
treeWalk.setRecursive(true)
var ret: List[String] = Nil
if(treeWalk != null){
while (treeWalk.next()) {
ret +:= treeWalk.getPathString
}
}
ret.reverse
}
}
}
/**
* Returns the commit list of the specified branch.
*
@@ -313,12 +391,7 @@ object JGitUtil {
} else {
revWalk.markStart(revWalk.parseCommit(objectId))
if(path.nonEmpty){
revWalk.setRevFilter(new RevFilter(){
def include(walk: RevWalk, commit: RevCommit): Boolean = {
getDiffs(git, commit.getName, false)._1.find(_.newPath == path).nonEmpty
}
override def clone(): RevFilter = this
})
revWalk.setTreeFilter(AndTreeFilter.create(PathFilter.create(path), TreeFilter.ANY_DIFF))
}
Right(getCommitLog(revWalk.iterator, 0, Nil))
}
@@ -420,11 +493,13 @@ object JGitUtil {
treeWalk.addTree(revCommit.getTree)
val buffer = new scala.collection.mutable.ListBuffer[DiffInfo]()
while(treeWalk.next){
val newIsImage = FileUtil.isImage(treeWalk.getPathString)
buffer.append((if(!fetchContent){
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None)
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None, None, false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
} else {
DiffInfo(ChangeType.ADD, null, treeWalk.getPathString, None,
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, treeWalk.getObjectId(0), false).filter(FileUtil.isText).map(convertFromByteArray),
false, newIsImage, None, Option(treeWalk.getObjectId(0)).map(_.name))
}))
}
(buffer.toList, None)
@@ -442,13 +517,17 @@ object JGitUtil {
newTreeIter.reset(reader, git.getRepository.resolve(to + "^{tree}"))
import scala.collection.JavaConverters._
git.getRepository.getConfig.setString("diff", null, "renames", "copies")
git.diff.setNewTree(newTreeIter).setOldTree(oldTreeIter).call.asScala.map { diff =>
if(!fetchContent || FileUtil.isImage(diff.getOldPath) || FileUtil.isImage(diff.getNewPath)){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None)
val oldIsImage = FileUtil.isImage(diff.getOldPath)
val newIsImage = FileUtil.isImage(diff.getNewPath)
if(!fetchContent || oldIsImage || newIsImage){
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath, None, None, oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
} else {
DiffInfo(diff.getChangeType, diff.getOldPath, diff.getNewPath,
JGitUtil.getContentFromId(git, diff.getOldId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray))
JGitUtil.getContentFromId(git, diff.getNewId.toObjectId, false).filter(FileUtil.isText).map(convertFromByteArray),
oldIsImage, newIsImage, Option(diff.getOldId).map(_.name), Option(diff.getNewId).map(_.name))
}
}.toList
}
@@ -602,21 +681,24 @@ object JGitUtil {
def getContentInfo(git: Git, path: String, objectId: ObjectId): ContentInfo = {
// Viewer
val large = FileUtil.isLarge(git.getRepository.getObjectDatabase.open(objectId).getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(objectId)
val large = FileUtil.isLarge(loader.getSize)
val viewer = if(FileUtil.isImage(path)) "image" else if(large) "large" else "other"
val bytes = if(viewer == "other") JGitUtil.getContentFromId(git, objectId, false) else None
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
if(viewer == "other"){
if(bytes.isDefined && FileUtil.isText(bytes.get)){
// text
ContentInfo("text", Some(StringUtil.convertFromByteArray(bytes.get)), Some(StringUtil.detectEncoding(bytes.get)))
} else {
// binary
ContentInfo("binary", None, None)
}
} else {
// binary
ContentInfo("binary", None, None)
// image or large
ContentInfo(viewer, None, None)
}
} else {
// image or large
ContentInfo(viewer, None, None)
}
}
@@ -629,12 +711,12 @@ object JGitUtil {
* @return the byte array of content or None if object does not exist
*/
def getContentFromId(git: Git, id: ObjectId, fetchLargeFile: Boolean): Option[Array[Byte]] = try {
val loader = git.getRepository.getObjectDatabase.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
using(git.getRepository.getObjectDatabase){ db =>
Some(db.open(id).getBytes)
using(git.getRepository.getObjectDatabase){ db =>
val loader = db.open(id)
if(fetchLargeFile == false && FileUtil.isLarge(loader.getSize)){
None
} else {
Some(loader.getBytes)
}
}
} catch {
@@ -712,7 +794,12 @@ object JGitUtil {
def getBranches(owner: String, name: String, defaultBranch: String): Seq[BranchInfo] = {
using(Git.open(getRepositoryDir(owner, name))){ git =>
val repo = git.getRepository
val defaultObject = repo.resolve(defaultBranch)
val defaultObject = if (repo.getAllRefs.keySet().contains(defaultBranch)) {
repo.resolve(defaultBranch)
} else {
git.branchList().call().iterator().next().getObjectId
}
git.branchList.call.asScala.map { ref =>
val walk = new RevWalk(repo)
try{
@@ -748,4 +835,47 @@ object JGitUtil {
}
}
}
def getBlame(git: Git, id: String, path: String): Iterable[BlameInfo] = {
Option(git.getRepository.resolve(id)).map{ commitId =>
val blamer = new org.eclipse.jgit.api.BlameCommand(git.getRepository);
blamer.setStartCommit(commitId)
blamer.setFilePath(path)
val blame = blamer.call()
var blameMap = Map[String, JGitUtil.BlameInfo]()
var idLine = List[(String, Int)]()
val commits = 0.to(blame.getResultContents().size()-1).map{ i =>
val c = blame.getSourceCommit(i)
if(!blameMap.contains(c.name)){
blameMap += c.name -> JGitUtil.BlameInfo(
c.name,
c.getAuthorIdent.getName,
c.getAuthorIdent.getEmailAddress,
c.getAuthorIdent.getWhen,
Option(git.log.add(c).addPath(blame.getSourcePath(i)).setSkip(1).setMaxCount(2).call.iterator.next)
.map(_.name),
if(blame.getSourcePath(i)==path){ None }else{ Some(blame.getSourcePath(i)) },
c.getCommitterIdent.getWhen,
c.getShortMessage,
Set.empty)
}
idLine :+= (c.name, i)
}
val limeMap = idLine.groupBy(_._1).mapValues(_.map(_._2).toSet)
blameMap.values.map{b => b.copy(lines=limeMap(b.id))}
}.getOrElse(Seq.empty)
}
/**
* Returns sha1
* @param owner repository owner
* @param name repository name
* @param revstr A git object references expression
* @return sha1
*/
def getShaByRef(owner:String, name:String,revstr: String): Option[String] = {
using(Git.open(getRepositoryDir(owner, name))){ git =>
Option(git.getRepository.resolve(revstr)).map(ObjectId.toString(_))
}
}
}

View File

@@ -71,6 +71,11 @@ object Keys {
*/
val Ajax = "AJAX"
/**
* Request key for the /api/v3 request flag.
*/
val APIv3 = "APIv3"
/**
* Request key for the username which is used during Git repository access.
*/

View File

@@ -15,7 +15,7 @@ import SystemSettingsService.Smtp
import ControlUtil.defining
trait Notifier extends RepositoryService with AccountService with IssuesService {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit
protected def recipients(issue: Issue)(notify: String => Unit)(implicit session: Session, context: Context) =
@@ -67,16 +67,15 @@ object Notifier {
class Mailer(private val smtp: Smtp) extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[Mailer])
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context) = {
val database = Database()
val f = Future {
database withSession { implicit session =>
getIssue(r.owner, r.name, issueId.toString) foreach { issue =>
defining(
s"[${r.name}] ${issue.title} (#${issueId})" ->
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
defining(
s"[${r.name}] ${issue.title} (#${issue.issueId})" ->
msg(Markdown.toHtml(content, r, false, true))) { case (subject, msg) =>
recipients(issue) { to =>
val email = new HtmlEmail
email.setHostName(smtp.host)
@@ -92,14 +91,13 @@ class Mailer(private val smtp: Smtp) extends Notifier {
.orElse (Some("notifications@gitbucket.com" -> context.loginAccount.get.userName))
.foreach { case (address, name) =>
email.setFrom(address, name)
}
}
email.setCharset("UTF-8")
email.setSubject(subject)
email.setHtmlMsg(msg)
email.addTo(to).send
}
}
}
}
"Notifications Successful."
@@ -113,6 +111,6 @@ class Mailer(private val smtp: Smtp) extends Notifier {
}
}
class MockMailer extends Notifier {
def toNotify(r: RepositoryService.RepositoryInfo, issueId: Int, content: String)
def toNotify(r: RepositoryService.RepositoryInfo, issue: Issue, content: String)
(msg: String => String)(implicit context: Context): Unit = {}
}

View File

@@ -0,0 +1,18 @@
package gitbucket.core.util
case class RepositoryName(owner:String, name:String){
val fullName = s"${owner}/${name}"
}
object RepositoryName{
def apply(fullName: String): RepositoryName = {
fullName.split("/").toList match {
case owner :: name :: Nil => RepositoryName(owner, name)
case _ => throw new IllegalArgumentException(s"${fullName} is not repositoryName (only 'owner/name')")
}
}
def apply(repository: gitbucket.core.model.Repository): RepositoryName = RepositoryName(repository.userName, repository.repositoryName)
def apply(repository: gitbucket.core.util.JGitUtil.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name)
def apply(repository: gitbucket.core.service.RepositoryService.RepositoryInfo): RepositoryName = RepositoryName(repository.owner, repository.name)
def apply(repository: gitbucket.core.model.CommitStatus): RepositoryName = RepositoryName(repository.userName, repository.repositoryName)
}

View File

@@ -62,6 +62,21 @@ object StringUtil {
}
}
/**
* Appends LF if the given string does not end with LF.
*
* @param content the content
* @param lineSeparator "LF" or "CRLF"
* @return the converted content
*/
def appendNewLine(content: String, lineSeparator: String): String = {
if(lineSeparator == "CRLF") {
if (content.endsWith("\r\n")) content else content + "\r\n"
} else {
if (content.endsWith("\n")) content else content + "\n"
}
}
/**
* Extract issue id like ```#issueId``` from the given message.
*

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