Compare commits

...

801 Commits
2.2 ... 3.8

Author SHA1 Message Date
Naoki Takezoe
97bd324cb0 Update README.md for GitBucket 3.8 release 2015-10-31 11:53:06 +09:00
Naoki Takezoe
2738406710 Update version number to 3.8 2015-10-31 11:07:02 +09:00
Naoki Takezoe
8e78345cbe Merge pull request #939 from nus/feature/upload-pdf
Support uploading PDF files
2015-10-31 11:05:41 +09:00
Naoki Takezoe
bd9e064137 Merge pull request #925 from team-lab/feature/add-compare-link-for-webhook-push
Feature/add properties for webhook push
2015-10-31 02:57:24 +09:00
Naoki Takezoe
eb49365bcb Merge pull request #960 from arteria/master
Creating repositories over API
2015-10-30 11:38:31 +09:00
Naoki Takezoe
de92e28c7a Adjust whitespace 2015-10-29 01:09:07 +09:00
Naoki Takezoe
c18f8fd87b Merge pull request #961 from superhj1987/master
Update PullRequestsController.scala
2015-10-29 01:06:06 +09:00
Naoki Takezoe
34272cb4c4 Update README.md 2015-10-29 01:00:41 +09:00
Naoki Takezoe
b3f8a02494 Update README.md 2015-10-29 00:59:47 +09:00
hangjian
f767a55350 Merge branch 'master' of github.com:gitbucket/gitbucket 2015-10-28 09:32:44 +08:00
Bryant Hang
9b2c3848d9 Update PullRequestsController.scala
when the forked repository is the original repository(forkedRepository.repository.originRepositoryName&originUserName is None),then 404 will occur,so add the if to solve it.
2015-10-27 23:09:08 +08:00
Jannis Vamvas
e34d016581 Change group repository creation API endpoint to /orgs/:org/repos 2015-10-27 10:27:47 +01:00
Jannis Vamvas
b64b447b42 Extend API to allow creating repositories 2015-10-26 14:30:49 +01:00
Yota Ichino
c6a4c13394 Change messages about file types in the comment form. 2015-10-25 22:56:31 +09:00
Yota Ichino
c8822cb4ca Write a Content-Disposition header for attach files. 2015-10-25 22:37:13 +09:00
Yota Ichino
c05e7218f3 Add document file formats for upload
The following extension is added.
- .docx
- .pptx
- .txt
- .xlsx
2015-10-25 22:03:16 +09:00
Naoki Takezoe
01872d3440 Fix order of updating pull request when repository is renamed 2015-10-24 23:31:37 +09:00
Naoki Takezoe
91bbb3e4dc Update description about support 2015-10-24 20:44:38 +09:00
Naoki Takezoe
80ebd9fb0e (refs #945)Remove ?raw=true from url other than images 2015-10-18 23:04:47 +09:00
Naoki Takezoe
2ab217251a Remove unnecessary TODO 2015-10-18 19:01:04 +09:00
Naoki Takezoe
2b20f6c74c (refs #946)Insert refer comment when pull request is created 2015-10-18 18:57:23 +09:00
Naoki Takezoe
042f855cd5 (refs #947)Fix referenced link from pull request 2015-10-18 18:06:03 +09:00
Naoki Takezoe
720ab7e0a3 Update url in docs 2015-10-17 16:17:43 +09:00
Naoki Takezoe
4b1b100aa5 Update README.md 2015-10-17 02:16:13 +09:00
Naoki Takezoe
541b7e2a79 Merge pull request #944 from lefou/t943-url-in-repo-desc
Detect links and render them as HTML links in repo description
2015-10-15 09:20:31 +09:00
Tobias Roeser
00cd3adc7b Refined regex and removed explicit TLDs
Also made regex val private.
2015-10-14 22:42:35 +02:00
Tobias Roeser
5a97a518a6 Detect links and render them as HTML links in repo description 2015-10-14 14:41:09 +02:00
Naoki Takezoe
d7219068cd (refs #907)Omit diffs if updated files are 100 over 2015-10-14 02:26:59 +09:00
Naoki Takezoe
a79c07f095 Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-10-14 02:24:43 +09:00
Naoki Takezoe
9f27f70c87 (refs #907)Omit the compare view for large diffs 2015-10-14 02:24:38 +09:00
Naoki Takezoe
8fa79db368 Merge pull request #940 from team-lab/fix/933-download-large-file
(ref #933) fix/Unable to download large file
2015-10-14 00:11:29 +09:00
nazoking
a1efa60741 fix return value 2015-10-13 18:57:20 +09:00
Naoki Takezoe
6166eb3743 Merge pull request #905 from kanmi/ssh-host-key
Specify option to generate an RSA host key
2015-10-13 00:16:50 +09:00
nazoking
5194fc5f15 (ref #933) fix/Unable to download large file (fix method name) 2015-10-12 23:25:07 +09:00
nazoking
1a97beb8cf (ref #933) fix/Unable to download large file 2015-10-12 21:58:56 +09:00
Yota Ichino
99d23398ad Change the upload form for PDF files 2015-10-12 21:45:16 +09:00
Yota Ichino
6accdefb8c Change a path for uploading a file 2015-10-12 21:44:58 +09:00
Yota Ichino
98fc64deaa Enable to upload a PDF file 2015-10-12 21:44:41 +09:00
Naoki Takezoe
30a8cefc37 Merge pull request #938 from nus/change-place-of-buttons
Change place of buttons
2015-10-12 17:31:04 +09:00
Naoki Takezoe
9d47c3ccb3 Bump markedj to 1.0.4-SNAPSHOT 2015-10-12 16:52:00 +09:00
Yota Ichino
5a1ab8d485 Set tabindex for the comment form. 2015-10-12 16:41:15 +09:00
Yota Ichino
5d526f243e Change place of Comment button and Close button. 2015-10-12 15:52:47 +09:00
nazoking
d13bb47ee7 remove ApiPushCommit class (merge to ApiCommit) 2015-10-06 02:27:42 +09:00
nazoking
8b8c6ee861 fix repository.url on webhook push payload 2015-10-06 02:15:03 +09:00
nazoking
0a2d95e434 add head_commit on webhook push payload 2015-10-06 02:14:02 +09:00
nazoking
fdd119c477 add compare, after and before property on webhook push payload 2015-10-06 01:39:19 +09:00
Naoki Takezoe
4f94ca1384 Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-10-04 13:17:13 +09:00
Naoki Takezoe
ed21ee8bdb Improve header anchor behavior 2015-10-04 13:17:07 +09:00
Naoki Takezoe
efd257dee0 Update README.md 2015-10-03 19:07:46 +09:00
Naoki Takezoe
bacf391a39 (refs #867)Providing checksum has started since 3.7 2015-10-03 14:26:36 +09:00
Naoki Takezoe
e8a1543466 Fix commit comment style 2015-10-03 13:28:59 +09:00
Naoki Takezoe
81c79003ec GitBucket 3.7.0 release 2015-10-03 13:13:48 +09:00
Naoki Takezoe
73cf9661ac Fix url encoding to encode whitespace to %20 2015-10-03 04:00:08 +09:00
Naoki Takezoe
6e994b0ae1 (refs #918)Fix pull request comment 2015-10-03 03:41:05 +09:00
Naoki Takezoe
c5da975cea Merge pull request #919 from team-lab/fix-push-commit-url
fix commit url in webhook `push` event.
2015-10-02 21:13:58 +09:00
Naoki Takezoe
1f3ef962e8 Merge pull request #921 from team-lab/fix-#700-update-pr-on-web-ui
(refs #700)web ui merge does not update pull request that already exist
2015-10-02 21:02:03 +09:00
Naoki Takezoe
f6066a0361 Merge pull request #920 from team-lab/fix-webhook-issue-comment-url
fix pull-request url on webhook payload
2015-10-02 20:45:23 +09:00
nazoking
ace65cf261 (refs #920) Remove IssueOrPullRequest. it is too much for this purpose 2015-10-02 11:59:25 +09:00
nazoking
1df537ce5c (refs #700)web ui merge does not update pull request that already exist 2015-10-02 00:49:11 +09:00
nazoking
bf64f6b4f4 api url is only issues 2015-10-02 00:33:23 +09:00
nazoking
0283ec574d fix pull-request url on webhook payload 2015-10-02 00:23:13 +09:00
nazoking
d566f64e8b The ApiCommit should be export url and html values in json. 2015-10-01 23:04:35 +09:00
nazoking
bb9add9da9 fix commit url in push webhook. 2015-10-01 22:56:36 +09:00
Naoki Takezoe
75d085a2c4 (refs #917)Fix incorrent HTML escape 2015-10-01 13:04:56 +09:00
Naoki Takezoe
4eab07ffaf Merge branch 'snowgooseyk-master' 2015-10-01 10:23:55 +09:00
Naoki Takezoe
cf7aaa25cd (refs #915)Fixup 2015-10-01 10:22:55 +09:00
Naoki Takezoe
8011b22de6 Merge branch 'master' of https://github.com/snowgooseyk/gitbucket into snowgooseyk-master 2015-10-01 10:16:21 +09:00
Naoki Takezoe
a108489d71 Update README.md 2015-09-30 03:25:43 +09:00
Naoki Takezoe
185c132771 Create CONTRIBUTING.md 2015-09-30 03:22:47 +09:00
Naoki Takezoe
3b11e905a1 Merge branch 'chazmuzz-master' 2015-09-30 02:30:44 +09:00
Naoki Takezoe
5a04fe7ae6 (refs #906)Shrink range which is applied style 2015-09-30 02:30:28 +09:00
Naoki Takezoe
92c73062cc Expatnd wiki page width 2015-09-29 01:20:28 +09:00
Naoki Takezoe
d07624bdc1 Sanitize in markdown 2015-09-29 01:20:10 +09:00
snowgooseyk
e1dbe80ccd Fix ArrayIndexOutOfBoundsException 2015-09-29 00:03:49 +09:00
Naoki Takezoe
6d69a52292 (refs #800)Introduce environment variable JAVA_OPTS to specify JVM options when build GitBucket 2015-09-24 13:13:36 +09:00
Naoki Takezoe
68af5479c8 (refs #823)Enable file finder for branches which contain / 2015-09-24 13:06:53 +09:00
Naoki Takezoe
3970eca8dc Merge branch 'McFoggy-issue-893' 2015-09-24 03:51:02 +09:00
Naoki Takezoe
b11d36c3a5 (refs #896)Add description and migration for separation of notification and SMTP configuration 2015-09-24 03:48:08 +09:00
Naoki Takezoe
3c53fd8618 Merge branch 'issue-893' of https://github.com/McFoggy/gitbucket into McFoggy-issue-893 2015-09-24 01:21:00 +09:00
Naoki Takezoe
4ca4c57fff Merge branch 'master' of https://github.com/chazmuzz/gitbucket into chazmuzz-master 2015-09-24 00:46:58 +09:00
Naoki Takezoe
317a6fde30 (refs #913)Remove Java8 dependency 2015-09-23 19:12:21 +09:00
Naoki Takezoe
ad08d385e6 Bump markedj 1.0.2 to Java7 support 2015-09-23 12:22:55 +09:00
Naoki Takezoe
805e12aceb Update README.md 2015-09-22 11:12:39 +09:00
Naoki Takezoe
3a45912400 Update README.md 2015-09-22 11:12:07 +09:00
Naoki Takezoe
19817e2659 Merge pull request #912 from takezoe/markedj
Switch markdown processor to markedj from pegdown
2015-09-22 11:04:14 +09:00
Naoki Takezoe
50dc205ef7 Use JDK8 for Travis build 2015-09-22 03:26:57 +09:00
Naoki Takezoe
2402a3ac72 Enable task list after update issue 2015-09-22 03:06:53 +09:00
Naoki Takezoe
e1c155d09d Fix Markdown testcase 2015-09-21 12:08:16 +09:00
Naoki Takezoe
84c3bc4ad4 Refactoring 2015-09-21 11:24:59 +09:00
Naoki Takezoe
353784c23e Restore header anchor 2015-09-21 11:01:40 +09:00
Naoki Takezoe
a359624f01 Restore task-list support 2015-09-21 10:48:44 +09:00
Naoki Takezoe
a0f684cfdf Bump markedj to 1.0.1 2015-09-20 14:39:20 +09:00
Naoki Takezoe
1ea1e74a0c Fix links 2015-09-20 12:49:49 +09:00
Naoki Takezoe
8f7c5fc922 Fix code and ordered list style 2015-09-20 04:45:46 +09:00
Naoki Takezoe
667ef680c1 Switch markdown processor to markedj from pegdown 2015-09-20 03:51:46 +09:00
Charlie Murray
972ab0df50 Truncate text inside a box-content-row div rather than flowing out of the box 2015-09-15 20:13:46 +01:00
kanmi
1fddc01f6e Specify option to generate an RSA key 2015-09-16 01:28:31 +09:00
Naoki Takezoe
bb2e77d899 Update README.md 2015-09-13 11:56:18 +09:00
Naoki Takezoe
a3daf13c15 Fix issue link in Markdown 2015-09-07 02:10:37 +09:00
Naoki Takezoe
fb2b2e37ce Remove last committer from the file list 2015-09-07 00:50:03 +09:00
Naoki Takezoe
c1381179aa Fix code style 2015-09-06 23:32:06 +09:00
Naoki Takezoe
9e2dc3f892 Merge pull request #897 from cd01/patch-1
Fix typo in README.md
2015-09-04 13:32:29 +09:00
Naoki Takezoe
5aa548d613 Update octicons 2015-09-03 22:45:52 +09:00
Naoki Takezoe
5225a95d3a Merge branch 'hikaruworld-SupportCloneInDesktop' 2015-09-03 22:31:38 +09:00
Naoki Takezoe
53b7a1fce5 Merge branch 'SupportCloneInDesktop' of https://github.com/hikaruworld/gitbucket into hikaruworld-SupportCloneInDesktop 2015-09-03 22:29:14 +09:00
Naoki Takezoe
02369a4949 (refs #895)Fix generate anchor option in Wiki 2015-09-03 21:25:56 +09:00
cd01
1ca548991b Fix typo in README.md 2015-09-03 21:20:09 +09:00
Matthieu Brouillard
0772070523 split SMTP & notification, fixes #893 2015-09-02 16:22:54 +02:00
Naoki Takezoe
4bf3848856 Merge pull request #894 from McFoggy/new-h2-backup-plugin
reference gitbucket-h2-backup-plugin in README
2015-09-02 01:56:04 +09:00
Matthieu Brouillard
512425de4c reference gitbucket-h2-backup-plugin in README 2015-09-01 14:36:59 +02:00
Naoki Takezoe
7f28bd6a26 Update README.md 2015-08-30 03:57:26 +09:00
Naoki Takezoe
4088b2c1e8 Exclude group name from issue / pull request assignees 2015-08-30 03:33:29 +09:00
Naoki Takezoe
919d55c002 Fix NullPointerException for non-existent branches 2015-08-27 11:16:33 +09:00
Naoki Takezoe
068bbd0c3b Merge branch 'officer-retry_fix_#838' 2015-08-26 22:48:03 +09:00
Naoki Takezoe
9f50528192 Merge branch 'retry_fix_#838' of https://github.com/officer/gitbucket into officer-retry_fix_#838
# Conflicts:
#	src/main/scala/gitbucket/core/view/LinkConverter.scala
2015-08-26 22:47:41 +09:00
Naoki Takezoe
4c149cf01c (refs #831)url encode filename in the redirect path 2015-08-26 22:17:21 +09:00
Naoki Takezoe
c86c706406 (refs #864)Fix blame 2015-08-24 02:40:29 +09:00
Naoki Takezoe
3b0a0f55b5 Fix broken blame 2015-08-24 02:09:50 +09:00
Naoki Takezoe
4232b8184e Change a limit of initial amount of the repositories list 2015-08-23 15:47:07 +09:00
Naoki Takezoe
e5f3dfe293 Update version to 3.6.0 2015-08-23 15:42:52 +09:00
Naoki Takezoe
22af94d36a BugFix and improvement for pull request 2015-08-23 13:44:12 +09:00
Naoki Takezoe
d6b6781861 Add show more link for repositories and wiki pages 2015-08-23 03:19:52 +09:00
Naoki Takezoe
2222299793 Fix merge checking 2015-08-23 02:17:46 +09:00
Naoki Takezoe
fdd9a184b5 Fix presentation of commit list in the pull request detail view 2015-08-22 17:20:59 +09:00
Naoki Takezoe
99492e3f8e Merge pull request #886 from noc06140728/fix-octicon
Replace some icon to octicon
2015-08-21 02:12:15 +09:00
Naoki Takezoe
a42c40bbc1 Fix merge guide to display ssh url 2015-08-21 02:11:42 +09:00
Masahiro Namba
2794f9fcfc Replace some icon to octicon
- replace some of the non-octicon to octicon.
- adjust the color of octicon on the button.

modified icon is as follows.

- .icon-home          -> .octicon-home
- .icon-time          -> .octicon-clock
- .icon-ok            -> .octicon-check
- .icon-lock          -> .octicon-lock
- .icon-envelope      -> .octicon-mail
- .icon-pencil        -> .octicon-pencil
- .icon-remove-circle -> .octicon-x
- .icon-check         -> .octicon-clippy
- .icon-calendar      -> .octicon-calendar
- .icon-cog           -> .octicon-gear
- .icon-th-list       -> .octicon-list-unordered
- .icon-trash         -> .octicon-trashcan
- .icon-arrow-right   -> .octicon-arrow-right
- .icon-retweet       -> .octicon-git-compare
- .icon-comment       -> .octicon-comment
2015-08-20 20:23:47 +09:00
Naoki Takezoe
28c0262e74 Improve issue and pull request creation form 2015-08-19 10:20:52 +09:00
Naoki Takezoe
8634191bd2 Merge pull request #884 from skx/master
Updated to fix truncated name in JSON: watchers_coun
2015-08-18 17:27:29 +09:00
Steve Kemp
f73c86d533 Updated to fix truncated name in JSON: watchers_coun
The correct field in the JSON should be `watchers_count` rather
than the truncated version `watchers_coun`.
2015-08-18 09:14:44 +03:00
Naoki Takezoe
f042d709ac Improve issue creation form 2015-08-18 10:42:16 +09:00
Naoki Takezoe
e2a6149a93 Update auto_update.md 2015-08-17 13:29:41 +09:00
Naoki Takezoe
b2a7e2c7e2 Merge pull request #882 from garygreen/shorten-commit-message
File listing and sidebar display improvements
2015-08-17 06:19:00 +09:00
Naoki Takezoe
89fc143075 Update merge guide 2015-08-16 23:43:57 +09:00
Gary Green
a754a92799 File listing and sidebar display improvements 2015-08-16 14:55:18 +01:00
Naoki Takezoe
dc26fcf609 Merge branch 'garygreen-link-width' 2015-08-16 11:07:08 +09:00
Naoki Takezoe
b9db57eeef Merge branch 'link-width' of https://github.com/garygreen/gitbucket into garygreen-link-width 2015-08-16 11:03:48 +09:00
Naoki Takezoe
9b377c727d Improve the pull request creation form 2015-08-16 02:30:13 +09:00
Naoki Takezoe
e5b8d81bb4 Remove unused code 2015-08-16 01:13:25 +09:00
Naoki Takezoe
c85b31a7d5 Improve comparing view 2015-08-16 01:12:44 +09:00
Naoki Takezoe
6580e5458a Merge branch 'master' of https://github.com/takezoe/gitbucket 2015-08-16 00:55:18 +09:00
Naoki Takezoe
4e4e65eaa6 Improve th background color 2015-08-15 14:31:00 +09:00
Naoki Takezoe
9d19aad384 Merge pull request #880 from garygreen/issues-gui
Improved design of issue
2015-08-15 11:29:56 +09:00
Naoki Takezoe
c16a9f234b (refs #878)Hide the Delete button for other than the head of branch 2015-08-15 11:21:26 +09:00
Naoki Takezoe
ace551c33d (refs #871)Make link for @mention which contains dot 2015-08-15 10:15:44 +09:00
Naoki Takezoe
1e6e686692 Merge pull request #873 from superhj1987/master
Update main.scala.html
2015-08-15 09:59:16 +09:00
Gary Green
afdcc3f7c0 Improved design of issue 2015-08-15 00:12:56 +01:00
Naoki Takezoe
00e64bc46c Remove IntelliJ specific file 2015-08-13 09:24:18 +09:00
Bryant Hang
a959e1820f Update main.scala.html
fix header plus dropdown menu display bug in safari and add 'Your profile' in user dropdown menu
2015-08-12 15:39:54 +08:00
Naoki Takezoe
3dfbdbfe51 (refs #865)Fix styles for repository viewer 2015-08-09 01:41:10 +09:00
Naoki Takezoe
5c46dc0bd3 (refs #865)Fix paginator of the commit list 2015-08-09 01:31:29 +09:00
Naoki Takezoe
db60db674f (refs #865)Update commit list presentation 2015-08-09 01:14:21 +09:00
Naoki Takezoe
687a4f14e1 (refs #865)Fix presentation of file finder and blow view 2015-08-08 21:11:34 +09:00
Naoki Takezoe
bb10365b8b (refs #865)Apply the flat style to box headers 2015-08-08 13:32:42 +09:00
Naoki Takezoe
74ed3bf6a0 Update README.md 2015-08-06 07:17:05 +09:00
Naoki Takezoe
d1d7fdc488 Merge pull request #862 from McFoggy/plugins-info
list installed plugins in the system administration menu
2015-08-06 02:20:07 +09:00
Matthieu Brouillard
67775a4c62 add a comprehensive message when no plugin is detected on the installation 2015-08-05 17:38:14 +02:00
Naoki Takezoe
317b5cb096 Merge pull request #861 from McFoggy/system-admin-extensibility
give an id to system admin menu container to allow plugin extension
2015-08-06 00:16:42 +09:00
Matthieu Brouillard
2929517d7e list installed plugins in the system administration menu 2015-08-05 16:45:00 +02:00
Matthieu Brouillard
51e788396d give an id to system admin menu container to allow plugin extension 2015-08-05 11:38:04 +02:00
Naoki Takezoe
1321653bf6 (refs #848)Additional fix for header width 2015-08-05 02:26:38 +09:00
Naoki Takezoe
3899854854 Merge pull request #848 from garygreen/css-container
Increase container width
2015-08-05 02:21:25 +09:00
Naoki Takezoe
c0ca842ba7 Merge pull request #857 from ssogabe/fixed_build_error
small fixed: env.sh has been already removed.
2015-08-04 00:40:44 +09:00
Seiji Sogabe
24b05d28db env.sh has been already removed. 2015-08-03 19:38:46 +09:00
chocolatle
f0268b105c Fix making bad link from certain references. 2015-08-03 02:14:38 +09:00
Naoki Takezoe
0a46e180a9 Merge pull request #855 from mslinn/master
Updated installation script to GitBucket 3.5
2015-08-03 01:11:42 +09:00
Mike Slinn
e6a215a9c3 Updated to GitBucket 3.5 2015-08-02 08:47:47 -07:00
Naoki Takezoe
8ca7117065 (refs #854)Backup document is moved to Wiki 2015-08-02 12:52:40 +09:00
Naoki Takezoe
ba0a07b835 Merge pull request #854 from McFoggy/backup-gitbucket
add an example of a backup script and some usage instructions
2015-08-02 12:44:22 +09:00
Naoki Takezoe
4a35b65c2c Update release scripts 2015-08-01 03:02:38 +09:00
Naoki Takezoe
836fa47812 GitBucket 3.5.0 release 2015-08-01 01:30:32 +09:00
Matthieu Brouillard
5b658ef6ff add backup script and instructions 2015-07-31 16:39:16 +02:00
Gary Green
e9ff24d9a7 Make header and sidemenu links clickable across full width. 2015-07-29 23:02:29 +01:00
Gary Green
a92051a4c3 Increase container width 2015-07-29 22:52:46 +01:00
Naoki Takezoe
77b3650580 (refs #844)Improve global header menu 2015-07-28 21:53:51 +09:00
Naoki Takezoe
67ee6857ad Remove unnecessary spec 2015-07-28 11:16:02 +09:00
Naoki Takezoe
5ab15c0a14 Merge branch 'beraboris-implement-668' 2015-07-28 11:13:51 +09:00
Naoki Takezoe
96a3f2c301 (refs #810)Some fix about pull request 2015-07-28 11:12:11 +09:00
Naoki Takezoe
85707264c4 Fix position of fork-form 2015-07-28 02:04:49 +09:00
Naoki Takezoe
ed05422ea8 Merge branch 'implement-668' of https://github.com/beraboris/gitbucket into beraboris-implement-668 2015-07-28 01:58:30 +09:00
Naoki Takezoe
8f10c8051e (refs #810)Display Compare (or Pull Request) button for the default branch of the forked repository 2015-07-28 01:57:54 +09:00
Naoki Takezoe
41fff399b5 Redirect to the repository after sign-in by clicking fork button 2015-07-26 03:17:13 +09:00
Naoki Takezoe
9e237647b0 Small fix about icons 2015-07-26 02:59:26 +09:00
Naoki Takezoe
1be53c6746 Merge branch 'sapk-master' 2015-07-23 02:11:17 +09:00
Naoki Takezoe
f2368b03c0 (refs #409)Fix header anchor name in Markdown 2015-07-23 00:53:29 +09:00
Naoki Takezoe
95284c0b36 (refs #409)Fix Markdown style 2015-07-22 02:42:30 +09:00
Naoki Takezoe
f1e21a93fb (refs #409)Fix icons 2015-07-22 02:42:30 +09:00
Naoki Takezoe
689811f659 Add forks icon 2015-07-19 23:23:00 +09:00
Naoki Takezoe
33ccb4e98c Merge branch 'uli-heller-email-sender' 2015-07-19 02:15:03 +09:00
Naoki Takezoe
29f6e98f9c Merge branch 'email-sender' of https://github.com/uli-heller/gitbucket into uli-heller-email-sender 2015-07-19 02:14:36 +09:00
Naoki Takezoe
775c8cc064 Update release operation 2015-07-19 02:04:52 +09:00
Naoki Takezoe
9a974d047c Small fix 2015-07-19 02:01:15 +09:00
Naoki Takezoe
3fd252d2db Remove unnecessary file 2015-07-19 01:47:42 +09:00
Naoki Takezoe
43edb034c8 Merge branch 'linux-ubuntu1404' of https://github.com/uli-heller/gitbucket into uli-heller-linux-ubuntu1404 2015-07-19 01:46:40 +09:00
Naoki Takezoe
e629ca391e Improve env.sh to extract GITBUCKET_VERSION from build.scala 2015-07-19 01:42:42 +09:00
Naoki Takezoe
0db7eba3f2 Merge pull request #836 from shiena/patch/fix_contenttype
Fix duplicated Content-Type
2015-07-18 23:30:27 +09:00
Naoki Takezoe
a472abc88e Merge pull request #829 from muddydixon/fix/group_image_cursor_css
fixed cursor pointer css of group image change
2015-07-18 23:25:04 +09:00
Naoki Takezoe
861c619c19 Merge branch 'master' of https://github.com/sapk/gitbucket into sapk-master 2015-07-18 22:18:02 +09:00
Naoki Takezoe
124f331963 Fix testcase 2015-07-18 22:01:45 +09:00
Mitsuhiro Koga
76fcd44191 Fix duplicated Content-Type 2015-07-16 13:05:40 +09:00
Naoki Takezoe
2124c0b88c Ignore ensime files 2015-07-14 02:49:21 +09:00
Naoki Takezoe
2f5ab8e3b9 (refs #830)Bump pegdown to 1.5.0 and support strike syntax 2015-07-14 02:47:26 +09:00
muddydixon
32c8b6914b fixed cursor pointer css of group image change 2015-07-13 16:54:12 +09:00
Naoki Takezoe
7e9d940f64 Merge branch 'beraboris-better-atom-titles' 2015-07-05 22:17:27 +09:00
Naoki Takezoe
59e826b630 (refs #811)Remove html tags from title of atom feed 2015-07-05 22:15:39 +09:00
Naoki Takezoe
88f3ee4b13 Merge branch 'better-atom-titles' of https://github.com/beraboris/gitbucket into beraboris-better-atom-titles 2015-07-05 21:37:40 +09:00
Naoki Takezoe
b7380a084e Merge pull request #814 from uli-heller/patch-1
Fixes #748 - duplicate c3p0.war
2015-07-05 21:33:36 +09:00
Naoki Takezoe
cb65e790ae (refs #812)Apply GitRepositoryFilter to SSH access too 2015-07-05 15:38:55 +09:00
Naoki Takezoe
573eabee93 (refs #812)Prepend '/' to repository name 2015-07-05 15:08:52 +09:00
Naoki Takezoe
f0d4c6546a (refs #812)SSH support for plug-in served git repository 2015-07-05 14:16:16 +09:00
uli-heller
b0943c87c8 Fixes #748 - duplicate c3p0.war 2015-07-05 06:16:42 +02:00
Boris Bera
4f1208ea98 Atom feed now puts the activity message in title
Originally it used to put the activity type in the message. This gave
no useful information. The user had to open the news item to see what
was going on. Now, the title is the activity's message rendered to html.
Note that this is the same as the content of the news item.

This fixes #481
2015-07-04 15:29:09 -04:00
Naoki Takezoe
02f16639ea Remove twirl-compiler from dependency 2015-07-05 00:50:27 +09:00
Naoki Takezoe
cd5e28c0b8 (refs #812)Add GitRepositoryFilter for authentication 2015-07-05 00:13:34 +09:00
Naoki Takezoe
6f7579f8d9 (refs #812)Add new extension point to serve Git repository by plug-in 2015-07-04 17:42:13 +09:00
Boris Bera
f1d2a71b49 Fixed url generated by pull request/compare button
The `:` character was getting escaped leading to an ugly url. This is
different from the urls generated when editing the branches in a pull
request. In that case the url did not have `:` escaped.
2015-07-03 21:23:53 -04:00
Boris Bera
fd4fe0dc0d Pullreqs in forks now use upstream master as origin
This implements #668.
2015-07-03 20:56:06 -04:00
Naoki Takezoe
3d5e4a4225 (refs #802)Allow '+' in repository name 2015-07-03 21:40:26 +09:00
Naoki Takezoe
10ffb452d0 Fix delete branch confirmation message 2015-07-03 21:19:14 +09:00
Naoki Takezoe
3218bddbdc (refs #808)Add collaborators when fork repository by group 2015-07-03 20:47:37 +09:00
Naoki Takezoe
59f78dcbcb Fix behavior when enableAnchor is false 2015-06-28 03:43:15 +09:00
Naoki Takezoe
0fe062a02f Add option to disable anchor for headline in markdown 2015-06-28 03:20:55 +09:00
Naoki Takezoe
869eaf8cfd Update version to 3.5.0-SNAPSHOT 2015-06-27 23:09:12 +09:00
Naoki Takezoe
5b445c9736 Improve declarative Plugin definition interface 2015-06-27 20:54:19 +09:00
Naoki Takezoe
6f3f3eaa99 Fix Wiki font style of Wiki editing form 2015-06-27 19:41:48 +09:00
Naoki Takezoe
a23ce92676 Add pom.xml for deploying assemble jar 2015-06-27 19:09:43 +09:00
Uli Heller
ad55d5199d Use '{loginName} {fromAddress}' for the from field within the notification emails 2015-06-27 08:45:27 +02:00
Uli Heller
bd05895761 Make the release process work on linux 2015-06-27 07:11:23 +02:00
Uli Heller
c68dd6c891 Another change to make it work on linux - now linux and windows commands are mixed up unfortunately... 2015-06-27 06:56:12 +02:00
uli-heller
daae9ae1e7 Update how_to_run.md
Once "bashism-source" is applied, the build runs smoothly on linux, no need to change anything...
2015-06-27 06:56:12 +02:00
Uli Heller
33dde75ae1 doc change: build.xml -> release/build.xml 2015-06-27 06:56:12 +02:00
Uli Heller
b2e1e1796d Fixed bashism 'source' 2015-06-27 06:16:22 +02:00
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
Antoine GIRARD
1ff68111b4 Merge remote-tracking branch 'upstream/master' 2015-05-18 15:51:03 +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
Antoine GIRARD
dff816324d Rebase + Remove all unused image 2015-03-08 17:15:48 +01: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
Antoine GIRARD
a33e2c6e36 Completely moved to octicon 2015-03-04 23:46:46 +01:00
Antoine GIRARD
75ef82d18a ... 2015-03-04 23:46:46 +01:00
Antoine GIRARD
eb3c522122 Moved to octicon 2015-03-04 23:41:26 +01:00
Antoine GIRARD
6c8bcfc62e Import of octicons 2015-03-04 23:10:55 +01: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
Naoki Takezoe
6b37967162 Add Version 3.0 to versions 2015-03-03 14:05:13 +09:00
Naoki Takezoe
a03b9584ee Update gitbucket.version to 3.0.0 2015-03-03 13:58:06 +09:00
Naoki Takezoe
34649dfeda Update group id 2015-03-03 11:16:48 +09:00
Naoki Takezoe
bc84cfc2c8 Update assembly jar deploy script for 3.0 release 2015-03-03 11:07:29 +09:00
Naoki Takezoe
bcba1f068b Update README.md for 3.0 release 2015-03-03 10:42:36 +09:00
Naoki Takezoe
e6f30ef86b Fix Profile structure 2015-03-03 10:36:14 +09:00
takezoe
9e67999ef0 Temporary fix instantiation error 2015-03-03 02:42:35 +09:00
takezoe
be75cef752 Fix testcase 2015-03-03 02:38:59 +09:00
Naoki Takezoe
19ead71b48 Bump up version number to 3.0.0 2015-03-02 22:22:52 +09:00
Naoki Takezoe
7ebe1d6c62 Merge branch 'master' into profile_generalization 2015-03-02 22:21:00 +09:00
Naoki Takezoe
2331b58b87 Merge branch 'batch-edit'
Conflicts:
	src/main/twirl/issues/list.scala.html
	src/main/twirl/issues/listparts.scala.html
2015-03-02 22:20:22 +09:00
Naoki Takezoe
d495b04d85 Change package name 2015-03-02 22:15:56 +09:00
shimamoto
751a8703ef (refs #632) Implement all the check of javascript. 2015-03-02 06:31:59 +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
shimamoto
9019d93449 (refs #632) Revert. And remove all checked checkbox. 2015-03-02 01:21:11 +09:00
Naoki Takezoe
32006e02c0 Fix build error 2015-03-02 00:45:28 +09:00
Naoki Takezoe
5ba0f6d51e Generalize Profile for plug-ins as ProfileBase 2015-03-02 00:32:59 +09:00
shimamoto
c004d501f6 (refs #632) Implement all the check of javascript. 2015-03-01 23:38:08 +09:00
Naoki Takezoe
6067fa0fca Fix broken layout of Wiki compare view 2015-02-28 12:47:52 +09:00
Naoki Takezoe
e2c6658e59 Fix compilation error 2015-02-28 12:47:06 +09:00
Naoki Takezoe
0a6e50cbbe Compare view supports diff for commit id, not only branch name 2015-02-28 12:43:50 +09:00
Naoki Takezoe
3732963d4b Add image API for plug-in 2015-02-28 03:40:02 +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
Naoki Takezoe
20217058fe Merge branch 'feature/new-branches-ui' of https://github.com/team-lab/gitbucket into team-lab-feature/new-branches-ui
Conflicts:
	src/main/scala/service/PullRequestService.scala
2015-02-27 02:05:02 +09:00
Naoki Takezoe
e67365a19f Fix problem about milestone in issues and pull requests 2015-02-26 11:20:41 +09:00
Naoki Takezoe
c2f1817c6a Avoid exception when filter box is empty 2015-02-25 21:42:22 +09:00
Naoki Takezoe
4a948d9b01 Fix monospace style 2015-02-24 00:10:06 +09:00
Shintaro Murakami
377bc2703b (refs #630) Fix bug on changing issues status. 2015-02-23 02:41:56 +09:00
Naoki Takezoe
196890b26f Fix Test 2015-02-20 13:39:47 +09:00
Naoki Takezoe
491fc2c164 Merge branch 'new-plugin-system'
Conflicts:
	src/main/scala/servlet/InitializeListener.scala
2015-02-20 13:32:50 +09:00
Naoki Takezoe
1bed38f175 Change order of plug-in controller registration 2015-02-20 13:08:38 +09:00
Naoki Takezoe
b124c31f65 Plug-in action to be Scalatra controller 2015-02-20 09:29:44 +09:00
Naoki Takezoe
8c588cbd66 Provides Slick Session to plug-ins via ThreadLocal 2015-02-16 13:14:52 +09:00
shimamoto
334753b1ad Remove unnecessary ServletContext. 2015-02-15 21:39:31 +09:00
Naoki Takezoe
f6e7401d1b Add script to deploy assembly jar 2015-02-14 23:56:42 +09:00
Naoki Takezoe
ab564cc2d4 Merge branch 'master' into new-plugin-system
Conflicts:
	src/main/scala/servlet/AutoUpdateListener.scala
2015-02-14 23:40:43 +09:00
Naoki Takezoe
b10839a5c2 Add comment 2015-02-14 23:28:21 +09:00
shimamoto
bb3323fb0e Merge pull request #605 from rlazoti/add-connection-pool
Add Connection Pool
2015-02-14 22:56:00 +09:00
Naoki Takezoe
0b15ecbacd Add pom.xml for the assembly jar 2015-02-11 22:28:29 +09:00
Naoki Takezoe
9f6afaed07 Add Result case classes for plugin 2015-02-10 02:28:22 +09:00
Naoki Takezoe
925420734e Render Html with layout template 2015-02-08 23:51:21 +09:00
Naoki Takezoe
fb6bb12c52 Give Context to plugin actions 2015-02-08 23:09:30 +09:00
Naoki Takezoe
22d12d0488 Use util.Version for GitBucket migration 2015-02-08 22:35:58 +09:00
Naoki Takezoe
6888d959e1 Add migration system for plugins 2015-02-08 22:31:09 +09:00
Naoki Takezoe
65e66f52f6 Merge branch 'marklacroix-sidemenu-tooltips' 2015-02-07 17:58:04 +09:00
Naoki Takezoe
225abfa126 Merge branch 'sidemenu-tooltips' of https://github.com/marklacroix/gitbucket into marklacroix-sidemenu-tooltips 2015-02-07 17:54:29 +09:00
Naoki Takezoe
876757f2d4 Merge pull request #620 from nus/fix-facebox-resources
Fix facebox resource URLs.
2015-02-07 17:51:32 +09:00
Naoki Takezoe
7e77398645 Add prototype of global action support 2015-02-07 13:12:05 +09:00
Naoki Takezoe
73c76a5a88 Add plugin interfaces 2015-02-07 10:03:23 +09:00
Mark LaCroix
e5fca0d6cc Fix blinking tooltips on side menu 2015-02-06 16:48:54 -05:00
Naoki Takezoe
eed7e5177f Merge branch 'master' into new-plugin-system 2015-02-06 22:39:04 +09:00
Yota Ichino
dd54ab31cb Fix facebox resource URLs. 2015-02-05 01:50:07 +09:00
Naoki Takezoe
0085cb24ad Add description about 2.8 2015-02-01 13:00:31 +09:00
Naoki Takezoe
6a758902ef Small fix for #615 2015-02-01 12:55:37 +09:00
Naoki Takezoe
0d81a9a9b6 Merge pull request #615 from team-lab/fix/xss-by-raw-html
fix/xss by raw html
2015-02-01 12:44:15 +09:00
Naoki Takezoe
6e4f6da633 Merge pull request #612 from team-lab/fix/update-pullrequest-on-commit-by-online-editor
fix/update pullrequest when file edited by online editor
2015-01-31 18:19:46 +09:00
Naoki Takezoe
15118ca5c1 Merge pull request #614 from HairyFotr/patch-typo
Fix typo
2015-01-31 13:58:55 +09:00
HairyFotr
8161560757 Fix typo 2015-01-30 21:34:25 +01:00
nazoking
9ba564c864 test/html is cause of xss 2015-01-30 15:32:53 +09:00
nazoking
06b5b92673 update pullrequest commitId on file edited by online editor 2015-01-30 04:14:04 +09:00
nazoking
a417c373f1 'New Pull Request' button if you logined. 2015-01-30 00:55:02 +09:00
nazoking
5f5cc8d454 add action link to pull request. 2015-01-30 00:30:11 +09:00
Naoki Takezoe
b9b6589bd7 Update README.md 2015-01-29 21:47:54 +09:00
Naoki Takezoe
b79f6a5fa0 Update README.md 2015-01-29 21:47:00 +09:00
nazoking
0acbaeae86 new branches ui like GitHub. 2015-01-29 21:38:50 +09:00
Shintaro Murakami
bd046da3d0 (refs #532) Fix rendering of link over image 2015-01-28 00:09:34 +09:00
Naoki Takezoe
a889ed7c46 Merge pull request #591 from marklacroix/anon-access
(refs #274) Add option to deny anonymous (i.e. unauthorized) access
2015-01-27 10:28:11 +09:00
Naoki Takezoe
e24684cb2b Update favicon 2015-01-25 20:01:12 +09:00
Naoki Takezoe
5f939c18b4 (refs #609)Convert labelId when rename repository 2015-01-25 14:45:37 +09:00
takezoe
140f1eb31b Add sbt-assembly configuration 2015-01-25 02:31:21 +09:00
takezoe
d412dd5009 (refs #600)Fix broken layout 2015-01-25 01:16:03 +09:00
Mark LaCroix
8643bfeb37 Merge remote-tracking branch 'upstream/master' into anon-access
Conflicts:
	src/main/scala/app/SystemSettingsController.scala
	src/main/scala/service/SystemSettingsService.scala
	src/test/scala/view/AvatarImageProviderSpec.scala
2015-01-21 15:49:42 -05:00
Naoki Takezoe
31b6adf0e5 Merge pull request #606 from bati11/fix-mergeguide-text
Fix merge guide's text
2015-01-21 01:39:37 +09:00
bati11
f1ac2b3507 Change checkout branch name from "master" to ${pullreq.branch} 2015-01-20 23:32:47 +09:00
Rodrigo Lazoti
172af307a6 add connection pool to Database object 2015-01-20 12:14:28 -02:00
Naoki Takezoe
135e1ef73d Merge pull request #602 from mrkm4ntr/default-privacy-option-to-create-repo
(refs #495,#595) Add configuration to set default visibility option to create new repositories.
2015-01-20 10:59:02 +09:00
Naoki Takezoe
da55bf6af3 Apply new icon 2015-01-18 14:17:41 +09:00
Naoki Takezoe
883a9c8b17 Improve Wiki rendering performance 2015-01-18 14:06:19 +09:00
Naoki Takezoe
7da89940e3 Use the issues list template for the pull request list in the dashboard 2015-01-18 03:59:33 +09:00
Shintaro Murakami
3233b0ae3c Fix failed test. 2015-01-17 23:28:08 +09:00
Shintaro Murakami
4c2ed09915 (refs #495,#595) Add configuration to set default visibility option to create new repositories. 2015-01-17 23:04:41 +09:00
Naoki Takezoe
256b6c480f Merge pull request #546 from rlazoti/add-link-to-dashboard
Add repository's link to issue and pull request list on dashboard
2015-01-17 16:44:04 +09:00
Naoki Takezoe
dc311837f9 Merge pull request #596 from tnayuki/streaming-archive
not to make a temporary file when archive
2015-01-17 16:29:36 +09:00
Naoki Takezoe
92aec48c99 Merge pull request #547 from mslinn/master
Change Bootstrap's default color pink for code tags to match github's color
2015-01-17 16:17:43 +09:00
Naoki Takezoe
a6ada8c457 Merge pull request #582 from team-lab/add-message-to-login
Show information message on singin view.
2015-01-17 14:50:37 +09:00
Shintaro Murakami
dcc601502e (refs #589) Prevent adding event handler several times 2015-01-14 23:04:23 +09:00
Shintaro Murakami
dd58d8c804 (refs #598) Exclude count of pull requests from that of issues. 2015-01-12 22:50:12 +09:00
Shintaro Murakami
2ade54b7e3 (#refs 549) Change "…" at skipped line to pseudo element 2015-01-11 01:04:13 +09:00
Shintaro Murakami
136c5854f3 (refs #593) Expand column size of FILE_NAME in COMIT_COMMENT 2015-01-11 00:28:52 +09:00
Shintaro Murakami
c597238d9c (refs #549) Selecting lines in diff without line numbers. 2015-01-10 01:22:47 +09:00
Toru Nayuki
2552a58e08 not to make a temporary file when archive 2015-01-09 18:39:48 +09:00
nazoking
74ad5872a3 Revert "add information to singup view"
This reverts commit f7fd53bf09.
2015-01-09 14:55:28 +09:00
Shintaro Murakami
485d502bd3 (refs #584) Refactored 2015-01-09 00:16:09 +09:00
Naoki Takezoe
47bc8d030e Merge pull request #590 from ghmer/master
Allow LDAPS connections instead of only allowing TLS enabled connections
2015-01-08 02:44:36 +09:00
Mark LaCroix
48fe7133f7 Add anonymous access option to tests 2015-01-07 09:47:36 -05:00
Mark LaCroix
5d962dc5e4 Add option to deny anonymous (i.e. unauthorized) access 2015-01-07 09:17:22 -05:00
Mario Enrico Ragucci
31e8e5a951 code alignment. We want a pretty pull request! 2015-01-07 07:46:59 +01:00
Mario Enrico Ragucci
858373c628 small beautifying change to have code properly aligned 2015-01-07 07:45:18 +01:00
Mario Enrico Ragucci
7f142d2c0d Introducing "Enable SSL" option on LDAP settings 2015-01-07 07:41:41 +01:00
Shintaro Murakami
08b86232a8 Merge pull request #589 from mrkm4ntr/toggle-line-notes
Add checkbox to toggle inline notes.
2015-01-06 23:48:11 +09:00
Shintaro Murakami
6bf4f42fdb Add checkbox to toggle line notes 2015-01-06 23:27:03 +09:00
Shintaro Murakami
f3c7de36d8 Remove filter setting for old plugin 2015-01-05 22:28:30 +09:00
Naoki Takezoe
19f556de57 Merge pull request #587 from mrkm4ntr/comment-for-split-diff
(refs #564) Comment for side-by-side diff available
2015-01-04 13:46:33 +09:00
Naoki Takezoe
e4467df411 Merge pull request #586 from team-lab/feature/add-stat-icon-on-diff
add icon on each diff header
2015-01-04 13:25:15 +09:00
Shintaro Murakami
8d305a1fb1 Merge branch 'master' of https://github.com/takezoe/gitbucket into comment-for-split-diff 2015-01-04 10:47:51 +09:00
Shintaro Murakami
b47153e645 Remove old plugin test 2015-01-04 10:40:10 +09:00
Shintaro Murakami
c71766c84b (refs #564) Comment for side-by-side diff available 2015-01-03 17:33:33 +09:00
Naoki Takezoe
23e4d679ae Merge branch 'purge-old-plugin-system' 2015-01-03 04:47:10 +09:00
Naoki Takezoe
182acb2e02 Trim each lines of command guidance 2015-01-02 19:11:50 +09:00
nazoking
b255b15006 add icon on diff view 2015-01-02 17:35:53 +09:00
Naoki Takezoe
b458f88161 Remove enable.plugin flag 2015-01-02 02:27:35 +09:00
Naoki Takezoe
398d8f2f1c Merge branch 'master' into purge-old-plugin-system 2015-01-02 02:03:00 +09:00
Naoki Takezoe
85c1a56cbf Purge old plugin system 2015-01-02 01:59:21 +09:00
Shintaro Murakami
da216c6960 (refs #585) Fix issue in markdown preview 2014-12-31 16:24:30 +09:00
Naoki Takezoe
bc91b153bf Merge pull request #574 from michaeljayt/add-fork-options
Add fork to group options
2014-12-31 01:08:27 +09:00
Shintaro Murakami
bc50b47d3a (refs #584) Fix the activity of commenting to pull request. 2014-12-31 00:27:47 +09:00
michaeljayt
aed15a7f25 Skip the group popup when user has no group 2014-12-30 14:26:30 +08:00
michaeljayt
a1f09117b0 Fix security issue on fork 2014-12-30 08:50:19 +08:00
michaeljayt
0a4a4a51ca Add fork to group options 2014-12-30 08:50:19 +08:00
nazoking
f7fd53bf09 add information to singup view 2014-12-29 20:54:22 +09:00
nazoking
cbfb863a54 Add information message to singin view. 2014-12-29 19:56:52 +09:00
Naoki Takezoe
9d56d72611 Update README.md for 2.7 release 2014-12-29 11:32:23 +09:00
Naoki Takezoe
527c91ff9d Fix for #581. Column name and CSS is changed. 2014-12-29 03:24:04 +09:00
Naoki Takezoe
c58c2d6700 Merge pull request #556 from michaeljayt/add-ssh-clone-url
Add SSH clone url option when enabled SSH access
2014-12-29 02:12:18 +09:00
Naoki Takezoe
5518eca952 Merge pull request #581 from mrkm4ntr/fix-comment-in-pr
Fix bug on showing inline comments in Pull Request.
2014-12-29 02:10:57 +09:00
Shintaro Murakami
6e2b67ec0b Fix bug on showing inline comments in Pull Request. 2014-12-28 20:12:32 +09:00
Naoki Takezoe
837b1e44a7 Merge pull request #561 from torutk/rpm_nonroot
Enable contrib init file and RPM's spec file to run on RHEL 6/CentOS 6 a...
2014-12-27 20:55:26 +09:00
Naoki Takezoe
e04c230c6e Merge pull request #580 from HairyFotr/patch-lint
Fix a few issues detected by static analysis
2014-12-27 20:10:02 +09:00
HairyFotr
a01b5a4a59 Fix a few issues detected by static analysis 2014-12-26 15:40:05 +01:00
Naoki Takezoe
427b6ce846 Merge pull request #579 from banjun/fix-editor-preview-NoSuchElementException-enableTaskList
fix repo editor fails to preview
2014-12-22 23:06:46 +09:00
banjun
b7b5af2b72 add enableTaskList to post params for _preview 2014-12-22 19:36:12 +09:00
Naoki Takezoe
39fec57f72 (refs #578)Add migration for repositories which have removed parent or origin repository. 2014-12-22 01:32:15 +09:00
Naoki Takezoe
238dedb6df (refs #578)Clean original and parent repository information when it's deleted 2014-12-21 21:39:20 +09:00
Naoki Takezoe
af091117b7 (refs #577)Remove all HTML tags in Markdown 2014-12-20 01:51:15 +09:00
michaeljayt
ddea4e12f0 Add SSH clone url option when enabled SSH access 2014-12-12 21:58:21 +08:00
Naoki Takezoe
9767903252 (refs #567)Fix condition of repository search for issues. 2014-12-05 02:10:28 +09:00
takezoe
bc75f9f8a2 (refs #564)Fix for repository renaming 2014-11-28 01:42:44 +09:00
takezoe
63627fc1d0 (refs #564)Change the attached files directory to /commens from /issues 2014-11-28 00:20:46 +09:00
Naoki Takezoe
c23985c1a7 Merge pull request #564 from mrkm4ntr/coment-for-diff
(refs #9) Comments for commit and diff
2014-11-28 00:08:59 +09:00
Shintaro Murakami
af58e99dcf (refs #9) Comments for commit and diff 2014-11-26 22:59:52 +09:00
Toru Takahashi
676670e9e3 Enable contrib init file and RPM's spec file to run on RHEL 6/CentOS 6 as non root user. 2014-11-24 11:51:36 +09:00
Naoki Takezoe
823c52e941 Update README.md 2014-11-24 03:18:10 +09:00
Naoki Takezoe
7f42007648 (refs #507)Small fix about pull request UI 2014-11-23 14:59:03 +09:00
Naoki Takezoe
7214ef21d2 (refs #559)Fix merged message 2014-11-23 11:45:22 +09:00
Naoki Takezoe
18a4492975 Update README.md 2014-11-23 11:19:24 +09:00
Naoki Takezoe
99f73b1016 (refs #560)Replace build status badge with Travis 2014-11-23 01:29:38 +09:00
Naoki Takezoe
0c1ce6a088 (refs #560)Add travis configuration 2014-11-23 01:19:26 +09:00
Naoki Takezoe
ae6291ab83 Update version to 2.6 2014-11-22 22:36:27 +09:00
Naoki Takezoe
617fcf7c99 Fix compilation error in test 2014-11-22 22:34:27 +09:00
Naoki Takezoe
9df4a74837 (refs #507)Applying new UI to pull request detail page has been completed. 2014-11-22 22:30:44 +09:00
Naoki Takezoe
966d4251be (refs #507)Applying new UI to pull request detail page 2014-11-22 22:02:33 +09:00
Naoki Takezoe
84b2e9cdcd Fix compilation error 2014-11-22 21:47:34 +09:00
Naoki Takezoe
e29d63c91a Merge branch 'master' into newui-for-pullreq 2014-11-22 21:46:51 +09:00
Naoki Takezoe
805d2b8e79 (refs #530)Don't re-sort activities by repository renaming. 2014-11-22 21:40:32 +09:00
Naoki Takezoe
9983fd1292 Update README.md 2014-11-22 12:57:33 +09:00
Naoki Takezoe
1de202e927 (refs #554)Search box bug fix 2014-11-19 07:17:37 +09:00
Naoki Takezoe
4eb9f4a485 (refs #551)Adjust wiki buttons 2014-11-17 17:08:51 +09:00
Naoki Takezoe
a8801e4e41 (refs #432)Show the information message at the top page 2014-11-17 00:52:51 +09:00
Naoki Takezoe
ee1c84dbf2 (refs #508)Remove filter parameter 2014-11-16 20:22:36 +09:00
Naoki Takezoe
e40e1fa6cd (refs #508)Add search filter box to the dashboard 2014-11-16 01:38:31 +09:00
Naoki Takezoe
055f648ea2 (refs #508)Remove repository filter 2014-11-11 13:10:07 +09:00
Naoki Takezoe
37a399c3a2 (refs #508)Fix line separator 2014-11-11 12:09:48 +09:00
Naoki Takezoe
bc0b11b60a (refs #508)Basic filter box implementation 2014-11-11 12:07:22 +09:00
Naoki Takezoe
65a1ca7146 (refs #508)Start to add search filter box 2014-11-11 03:19:51 +09:00
Naoki Takezoe
2293030d4e Merge pull request #541 from rlazoti/fix-dashboard
Fix pull request's view on dashboard
2014-11-09 00:21:39 +09:00
Mike Slinn
2848f07b83 Merge remote-tracking branch 'upstream/master' 2014-11-08 04:11:55 -08:00
Mike Slinn
55224ddcd8 Changed Bootstrap's default color pink for code tags to match github's color 2014-11-08 04:07:14 -08:00
Rodrigo Lazoti
054ae75b6b Add repository's link to issues and pull request list on dashboard 2014-11-07 10:55:08 -02:00
Rodrigo Lazoti
c83fab611e Remove the 'All' tab 2014-11-06 19:01:16 -02:00
Rodrigo Lazoti
29baf1223c Fix pull request's view on dashboard 2014-11-04 18:15:07 -02:00
Naoki Takezoe
2a60f607ff Update README.md for 2.5 2014-11-04 01:38:29 +09:00
Naoki Takezoe
78f4d26aa0 Add version 2.5 2014-11-03 05:46:32 +09:00
Naoki Takezoe
f59e86f5ca (refs #529)Add icons 2014-11-03 05:25:03 +09:00
Naoki Takezoe
1c2af36c92 (refs #529)Mentioned filter 2014-11-03 04:35:41 +09:00
takezoe
badbe73f4e Fix for Firefox 2014-11-03 02:30:19 +09:00
takezoe
a9d58698cd (refs #529)Adjust bottom line 2014-11-03 01:40:47 +09:00
Naoki Takezoe
bb3f086aa6 (refs #529)Enhance dashboard header 2014-11-03 00:31:34 +09:00
Naoki Takezoe
2db674bb03 (refs #529)Organization filter 2014-11-02 13:26:46 +09:00
Naoki Takezoe
4bc4a16a80 Merge branch 'master' into newui-for-dashboard
Conflicts:
	src/main/twirl/dashboard/issueslist.scala.html
	src/main/twirl/dashboard/pullslist.scala.html
2014-11-01 03:14:19 +09:00
Naoki Takezoe
d88a105628 Merge pull request #512 from mrkm4ntr/create-branch-ui
(refs #394) Create branch from Web UI
2014-11-01 03:10:38 +09:00
Naoki Takezoe
15d0c5b506 Merge pull request #526 from mrkm4ntr/datetime-format
(ref #519) Change datetime formats
2014-11-01 03:10:18 +09:00
Naoki Takezoe
dbde79d2f2 Merge pull request #342 from bati11/feature-tasklist
Implement "Task List" in markdown
2014-11-01 03:09:24 +09:00
Naoki Takezoe
e6e3786b47 (refs #529)Visibility filter 2014-11-01 03:05:52 +09:00
Tomofumi Tanaka
4c1b8004fc (refs #533)Admin user must not disable self account yourself 2014-10-29 09:15:20 +09:00
shimamoto
ff4052f097 (refs #507) Fix the issue info of conversation page. 2014-10-19 23:39:25 +09:00
Naoki Takezoe
13c206d068 Applying new Issues UI to dashboard 2014-10-19 21:34:12 +09:00
Shintaro Murakami
5b875d7c73 Refactored, sorry. 2014-10-19 01:22:31 +09:00
Shintaro Murakami
e33dd9008b (ref #519) Change datetime formats 2014-10-18 23:21:47 +09:00
Naoki Takezoe
8764910553 (refs #504)Fix word-break of "Pages" table 2014-10-18 19:26:11 +09:00
Naoki Takezoe
4c89c40944 (refs #522)Recover user filter in dashboard 2014-10-18 19:11:30 +09:00
Shintaro Murakami
0f0986afcf (refs #394)Create branch from Web UI 2014-10-16 22:03:49 +09:00
Shintaro Murakami
5d5f1f8bdd (refs #514) Fix problems of renaming repository. 2014-10-16 22:03:49 +09:00
Naoki Takezoe
03e386b3ce Merge pull request #515 from mrkm4ntr/hotfix-514
(refs #514) Fix problems of renaming repository.
2014-10-12 19:05:54 +09:00
takezoe
435eac7ae6 (refs #511)Fix problem which is not possible to choose color at the colorpicker 2014-10-12 16:29:41 +09:00
takezoe
bd5df3977d (refs #518)Compile for Java7 2014-10-12 15:49:49 +09:00
bati11
ba218053f9 Modify to correspond to that "issuedetail.scala.html" has been deleted 2014-10-11 13:50:32 +09:00
bati11
1fe448a83b Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/issuedetail.scala.html
2014-10-11 12:07:14 +09:00
Shintaro Murakami
26a45d0117 (refs #514) Fix problems of renaming repository. 2014-10-09 22:05:42 +09:00
Naoki Takezoe
320585a530 Fix presentation problem on Firefox 2014-10-07 14:52:11 +09:00
Naoki Takezoe
ca0f888a99 Update for 2.4.1 2014-10-06 13:56:33 +09:00
Naoki Takezoe
3b08dc2e41 (refs #510)Fix dropdown presentation 2014-10-06 13:47:41 +09:00
Naoki Takezoe
cc128a49c1 (refs #510)Dirty fix for Firefox 2014-10-06 13:37:32 +09:00
Naoki Takezoe
e0148695f2 (refs #509)Fix link broken bug in Wiki 2014-10-06 13:22:42 +09:00
Naoki Takezoe
afe0b1dd71 Fix link bug in pull requests 2014-10-06 03:08:35 +09:00
Naoki Takezoe
353852d6da Update README.md 2014-10-06 02:14:10 +09:00
Naoki Takezoe
28585d1a3d Merge branch 'new-issue-ui' 2014-10-06 01:54:19 +09:00
shimamoto
9d69a48c65 (refs #488) Fix bug for refer comment. 2014-10-06 01:40:23 +09:00
shimamoto
2f95c76634 (refs #488) Fixed compile error. 2014-10-06 01:39:42 +09:00
shimamoto
eac9f0e6ff (refs #488) Fixed the action for assignee. 2014-10-06 01:30:30 +09:00
Naoki Takezoe
043fc21e05 Add Version 2.4 2014-10-06 01:05:40 +09:00
shimamoto
5854a75615 (refs #488) Fixed the action for label and milestone. 2014-10-06 00:52:14 +09:00
Naoki Takezoe
7b02946496 (refs #506)Fix generated URL for images 2014-10-05 23:07:15 +09:00
Naoki Takezoe
70f0ffd4f4 Fix label text color 2014-10-05 20:01:34 +09:00
Naoki Takezoe
91b82c2652 (refs #505)Disable the plugin system in default 2014-10-05 18:08:33 +09:00
shimamoto
b1017140aa (refs #488) Fixed the action for issue and comment content change. 2014-10-05 17:13:58 +09:00
takezoe
fc806b8813 Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-05 15:37:22 +09:00
takezoe
836913482b (refs #488)Issue label editing is completed 2014-10-05 15:37:09 +09:00
Naoki Takezoe
b3df3f44c6 Merge pull request #500 from mrkm4ntr/split-diff-style
Modify styles of split diff.
2014-10-05 11:56:49 +09:00
shimamoto
4ffbf89e74 (refs #488) Fixed the action for issue title change. 2014-10-05 04:43:11 +09:00
shimamoto
9851c7d93d (refs #488) Changing the issue edit. 2014-10-04 23:53:37 +09:00
Mike Slinn
a10188260c Update README.md 2014-10-03 15:26:42 -07:00
Tomofumi Tanaka
2201f2b202 Simplify query 2014-10-04 00:31:23 +09:00
takezoe
c92e71bb7a Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-10-01 02:25:43 +09:00
takezoe
d271fac350 (refs #488)Add transparent label icons 2014-10-01 02:25:19 +09:00
Naoki Takezoe
ce4522fc30 (refs #488)Remove top margin of the clear condition link 2014-09-30 02:31:11 +09:00
Tomofumi Tanaka
a178c48de6 Merge branch 'fix-498-dash-pr' 2014-09-29 20:51:32 +09:00
Tomofumi Tanaka
9d1323a044 (refs #498)Reformat counting pull request query 2014-09-29 16:38:12 +09:00
Tomofumi Tanaka
43babfed94 (refs #498) Correct pull request counts 2014-09-29 14:00:29 +09:00
Tomofumi Tanaka
6fa7ea30fb (refs #498)Returns private own repository
RepositoryService.getAllRepositories should returns
private own repository too.
2014-09-29 11:53:19 +09:00
Tomofumi Tanaka
d78315695b (refs $#498)Don't show private repo user doesn't have permission
This fix in the dashboard pull request view
But this fix still has a problem show wrong count number pull request.
2014-09-29 11:27:48 +09:00
shimamoto
16021865cb (refs #488) Fixed the screen layout. 2014-09-28 21:15:30 +09:00
Naoki Takezoe
b516be242d (refs #488)Show number of issues for each labels 2014-09-28 11:43:58 +09:00
Naoki Takezoe
0124f7cc3c (refs #488)Change permission to access to labels 2014-09-28 11:35:19 +09:00
Naoki Takezoe
f3eec35287 (refs #488)Fix nav-pills style in the header of the issues system 2014-09-28 11:24:27 +09:00
Naoki Takezoe
fb396a33b0 (refs #488)Remove unnecessary div 2014-09-28 03:05:04 +09:00
Naoki Takezoe
3370499421 (refs #488)Apply new UI to labels 2014-09-28 03:04:36 +09:00
shimamoto
d847e27cf9 (refs #488) Move Milestone and Assignee. 2014-09-27 23:48:38 +09:00
Naoki Takezoe
9684b158ce (refs #488)Apply new UI to the milestone create / edit page 2014-09-26 02:14:30 +09:00
Naoki Takezoe
8456808a8e (refs #488)Add milestone icons 2014-09-25 09:37:13 +09:00
Naoki Takezoe
9747899a19 (refs #488)Update icons 2014-09-25 09:36:48 +09:00
shimamoto
099304605e Merge branch 'new-issue-ui' of https://github.com/takezoe/gitbucket into new-issue-ui 2014-09-25 04:49:07 +09:00
shimamoto
30994d0465 (refs #488) WIP: Fixing the screen layout for the issue detail. 2014-09-25 04:48:39 +09:00
Naoki Takezoe
71fdbe7b71 (refs #488)Apply new UI to Milestones tab 2014-09-25 02:32:55 +09:00
Naoki Takezoe
86432c5ffe (refs #488)Small fix 2014-09-25 01:52:32 +09:00
takezoe
4dfa1fb0f8 Merge remote-tracking branch 'origin/new-issue-ui' into new-issue-ui 2014-09-23 23:26:54 +09:00
takezoe
db59a7652f (refs #488)Add icons for new Issues UI 2014-09-23 23:26:46 +09:00
shimamoto
417470a81c (refs #488) Fixed the screen layout for the issue creation. 2014-09-23 21:42:56 +09:00
Naoki Takezoe
cc639da17e (refs #488)Unify issues and pull requests template 2014-09-23 18:11:46 +09:00
Naoki Takezoe
f619f4a9bc (refs #488)Remove unnecessary template arguments 2014-09-23 17:50:47 +09:00
Naoki Takezoe
5dffc2a64e (refs #488)Batch edit for pull requests 2014-09-23 17:41:53 +09:00
Naoki Takezoe
bb63a8d14c (refs #488)Remove unnecessary template arguments 2014-09-23 17:23:52 +09:00
Naoki Takezoe
c1263cc16d (refs #488)Batch edit for issues 2014-09-23 14:57:29 +09:00
Shintaro Murakami
49f2e7d70f Add empty style. 2014-09-23 02:10:46 +09:00
Shintaro Murakami
f93b535f70 Modify styles of split diff. 2014-09-23 00:07:00 +09:00
shimamoto
e16d3c823b Update slick version. 2014-09-22 21:59:17 +09:00
Naoki Takezoe
7a6fdbcf50 (refs #488)Fix the "New pull request" button 2014-09-22 19:26:36 +09:00
Naoki Takezoe
46041a3762 (refs #488)Apply new UI to the pull request list 2014-09-22 19:10:39 +09:00
Naoki Takezoe
20b0553f7f (refs #488)Exclude pull requests from the issue list 2014-09-22 18:38:28 +09:00
Naoki Takezoe
5870cacf44 (refs #488)Fix presentation of issue list header 2014-09-22 18:29:30 +09:00
Naoki Takezoe
cb512cd98d (refs #488)Add link to clear search condition 2014-09-22 10:18:26 +09:00
Naoki Takezoe
90487eb7b7 (refs #488)Merge user filter into IssueSearchCondition 2014-09-22 10:08:22 +09:00
Naoki Takezoe
706fa77de3 (refs #488)Add service.IssuesService.IssueInfo 2014-09-21 02:06:38 +09:00
bati11
26b14ded58 Add nested task list support 2014-09-20 10:57:33 +09:00
Naoki Takezoe
3b1367dd8e (refs #488)Displays the milestone on the issue list. 2014-09-20 03:02:52 +09:00
bati11
e1f310317d Modify GitBucketHtmlSerializer constructor parameters
- Add to the GitBucketHtmlSerializer constructor parameter "hasWritePermission"
- Remove the call to the RepositoryService.hasWritePermission in GitBucketHtmlSerializer
2014-09-19 14:13:53 +09:00
bati11
937814ec5d Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/scala/app/IssuesController.scala
	src/main/twirl/issues/create.scala.html
2014-09-19 12:45:09 +09:00
bati11
b55fc649a6 Change crlf to lf 2014-09-19 12:43:04 +09:00
Naoki Takezoe
f4e4506517 (refs #488)Start to apply new issue UI to the issue list. 2014-09-16 00:46:38 +09:00
Naoki Takezoe
287a0b6669 (refs #488)Copy listparts.scala.html from issues/pulls to dashboard. 2014-09-15 23:47:06 +09:00
Naoki Takezoe
5bddd352af (refs #453)Fix "Test Hook" behavior 2014-09-15 13:04:39 +09:00
Naoki Takezoe
9c6ea8fb9d Fix client side validation error 2014-09-15 12:39:30 +09:00
Naoki Takezoe
32e8bf46a7 Merge pull request #491 from mrkm4ntr/myBranch
Change to show a shortcut path of the directory whose ancestors have only one child directory.
2014-09-15 02:12:52 +09:00
Naoki Takezoe
d61fe1bf84 (refs #483)Fix link in markdown 2014-09-14 20:31:02 +09:00
Naoki Takezoe
47dbea947d (refs #487) split diff is available 2014-09-14 10:53:17 +09:00
Naoki Takezoe
97c6b0495e Fix Warnings 2014-09-13 19:01:49 +09:00
Tomofumi Tanaka
a602ece8e9 (refs #490)Set HEAD ref when saved default branch
GitBucket allows user to configure default branch in the repository.
But it's only affect repository viewer in the GitBucekt world.
This change set default branch in the Git world.
2014-09-12 22:01:21 +09:00
Shintaro Murakami
cf6dca84d8 Release TreeWalk in recuresive function. 2014-09-11 00:06:28 +09:00
Shintaro Murakami
79432ff8ad Like GitHub, show a shortcut path of the directory whoes ancestors have only one child directory. 2014-09-10 07:07:04 +09:00
tanacasino
b8613431de Merge pull request #484 from douglarek/patch-1
Use a shebang (#!/bin/sh) to run sbt.sh
2014-09-04 11:42:17 +09:00
Lingchao Xin
698eafa562 Use a shebang (#!/bin/sh) to run sbt.sh
If no shebang provided in a shell file, it will be executed by default shell.

However this script is not compatible with Fish Shell, so use sh to execute it.
2014-09-04 10:25:24 +08:00
Naoki Takezoe
d33886db89 Add RawData and 404 error response for plugin action 2014-09-03 02:26:06 +09:00
Naoki Takezoe
cde09d3a59 Fix plugin path problem 2014-09-03 01:46:37 +09:00
Naoki Takezoe
5674f0e980 Update README.md 2014-09-01 14:34:58 +09:00
Naoki Takezoe
b9ade60eb2 (refs #464)Improve plugin installing/updating behavior 2014-09-01 01:11:19 +09:00
Naoki Takezoe
96303723fa (refs #464)Clear plugins before upgrading to 2.3. 2014-09-01 00:29:13 +09:00
Naoki Takezoe
0f5dbc5788 Merge branch 'master' into scala-plugin
Conflicts:
	project/build.scala
2014-09-01 00:02:31 +09:00
Naoki Takezoe
8df0c3a439 (refs #476)Change Jetty temp directory to GITBUCKET_HOME/tmp 2014-08-31 23:31:59 +09:00
Tomofumi Tanaka
ca6a86816a (refs #461)Correct atom feed datetime format 2014-08-25 22:32:07 +09:00
Tomofumi Tanaka
3ea939798f Remove unused import 2014-08-24 22:18:21 +09:00
Tomofumi Tanaka
d947410e3c (refs #434)Refactor to get last modified commit 2014-08-24 22:18:21 +09:00
Tomofumi Tanaka
db59bc08ac (refs #434)Show only last modified commit 2014-08-24 22:18:14 +09:00
Naoki Takezoe
95a8649f79 (refs #464)Add Fragment rendering support for Ajax 2014-08-24 13:56:49 +09:00
Naoki Takezoe
ffd10122ed (refs #464)Some improve for plugin API
- Place holder support for db API
- Redirect support for plugin action
2014-08-23 17:57:06 +09:00
Naoki Takezoe
c4c39f36e9 (refs #464)Add db.update() to update DB from plugin 2014-08-23 03:28:13 +09:00
Naoki Takezoe
96900c3cbf (refs #464)Remove unnecessary App mix-in 2014-08-23 03:26:19 +09:00
Tomofumi Tanaka
69fa370d12 Tweak font size and family in blog/diff view 2014-08-22 00:02:58 +09:00
Tomofumi Tanaka
7496437d11 Remove unused h7 2014-08-20 22:34:48 +09:00
shimamoto
33b7d09af7 Update slick version to 2.1.0. 2014-08-17 18:25:06 +09:00
Naoki Takezoe
53d0974760 (refs #457)Fix the target of updateRef 2014-08-17 13:01:05 +09:00
Naoki Takezoe
a87399f223 (refs #464)Add a new parameter to specify request method for plugin actions 2014-08-16 16:02:02 +09:00
Naoki Takezoe
975dfb17e1 (refs #464)Twirl support for plugin 2014-08-16 02:53:43 +09:00
Naoki Takezoe
8b8bd0289b (refs #464)Fix test case 2014-08-14 22:44:04 +09:00
Naoki Takezoe
3bb69c623b (refs #464)Switch to play-twirl 2014-08-14 18:37:37 +09:00
Naoki Takezoe
dd427bdbef Merge branch 'master' of https://github.com/takezoe/gitbucket 2014-08-14 14:01:33 +09:00
Naoki Takezoe
b40657a14a (refs #467)Reverse tag table ordering 2014-08-14 14:01:11 +09:00
Tomofumi Tanaka
21ca5b2eec (refs #471)Show the copy button only when flash is available
Check flash availability before Showing the copy button.
2014-08-14 12:43:55 +09:00
Naoki Takezoe
b78d584d8a (refs #464)Add drop tables capability when plugin is uninstalled 2014-08-14 01:56:47 +09:00
Naoki Takezoe
e6b666a66a (refs #464)Implementing database migration system for plugin 2014-08-13 23:16:13 +09:00
Naoki Takezoe
bab93ea4f5 (refs #464)Fix compilation error 2014-08-13 22:06:08 +09:00
hikaruworld
70c386a934 Disable icone platform is linux && null 2014-08-13 21:28:27 +09:00
hikaruworld
08eb21844a Check the null value of UserAgent 2014-08-13 18:06:56 +09:00
hikaruworld
7b37d6b571 Change to platform userAgent. 2014-08-13 18:06:35 +09:00
Naoki Takezoe
7fe98253ae Merge pull request #452 from mslinn/master
Enhanced install script so it works under Ubuntu and Mac OS/X
2014-08-13 15:38:44 +09:00
Naoki Takezoe
13385cbced (refs #464)Add PLUGIN table for plugin management 2014-08-13 02:23:29 +09:00
hikaruworld
f52bd2bcc0 support Desktop in Clone 2014-08-12 21:45:03 +09:00
Naoki Takezoe
3f20cec7b2 (refs #464)Add Scaladoc 2014-08-12 00:37:36 +09:00
Naoki Takezoe
a0e4b020ca (refs #464)Authentication for actions which are defined by plugin is completed 2014-08-12 00:33:13 +09:00
Naoki Takezoe
ea5d898b27 (refs #464)Add Security sealed trait which is used by plugin 2014-08-12 00:02:48 +09:00
Naoki Takezoe
4e652b5ccd (refs #464)Add authentication for plugin action 2014-08-11 19:27:24 +09:00
Naoki Takezoe
dd809896c8 (refs #464)Add extension point to inject JavaScript instead of adding button 2014-08-11 00:45:58 +09:00
Naoki Takezoe
93536d3365 (refs #464)Add new extension point to add buttons 2014-08-10 05:42:06 +09:00
Naoki Takezoe
098b18fe6d (refs #464)Experimental implementation of Scala based plugin 2014-08-10 04:33:57 +09:00
tanacasino
66efdac757 Merge pull request #449 from jparound30/fix_423
Change blob view's table-layout property.
2014-08-07 23:08:54 +09:00
Tomofumi Tanaka
45545d3815 Revert "(refs #458)Skip unexpected commit message"
This reverts commit be79ac2eb2.
2014-08-05 23:02:31 +09:00
Tomofumi Tanaka
b65d41731b (refs #458)Correct commit message in activity info 2014-08-05 23:00:56 +09:00
Tomofumi Tanaka
be19e97518 Merge branch '2.2-update' 2014-08-05 08:53:51 +09:00
Naoki Takezoe
2ebf2b99bd Update README.md 2014-08-05 02:15:36 +09:00
Tomofumi Tanaka
be79ac2eb2 (refs #458)Skip unexpected commit message
This patch is temporary measures.
MUST create AutoUpdate before release 2.3.
2014-08-05 00:43:20 +09:00
Tomofumi Tanaka
05afec3236 (refs#458)Correct short and full message
Swaped short and full message in commit info by accident.
2014-08-05 00:07:21 +09:00
Mike Slinn
193a312b22 Made gitbucket run on system startup and stop on shutdown 2014-07-29 15:49:48 -07:00
Mike Slinn
6a2d2ebfd1 Added help info for user about making iptables changes persistent 2014-07-29 15:44:43 -07:00
Mike Slinn
6d200aa340 Works under Ubuntu and Mac OS/X 2014-07-29 07:28:44 -07:00
Mike Slinn
a0fbb90048 Works on Mac, need to retest on Ubuntu 2014-07-29 00:18:09 -07:00
Mike Slinn
08e29e7077 Added install script, made existing RedHat init script also work with Ubuntu 2014-07-28 10:10:27 -07:00
jparound30
3bef71f5f2 (refs #423)Change blob view's table-layout property. 2014-07-29 00:22:22 +09:00
bati11
6175eb7c08 Merge branch 'master' into feature-tasklist
Conflicts:
	src/main/twirl/issues/commentform.scala.html
	src/main/twirl/issues/create.scala.html
	src/main/twirl/pulls/compare.scala.html
	src/main/twirl/wiki/edit.scala.html
2014-05-31 12:17:30 +09:00
bati11
ebb9d9329a Merge branch 'master' into feature-tasklist 2014-05-03 10:51:18 +09:00
bati11
843722f82e Implement the feature "Task List" 2014-04-10 02:08:45 +09:00
bati11
ce79eaada8 Add escapeTaskList method, it escapse '- [] ' characters 2014-04-10 01:21:55 +09:00
443 changed files with 24510 additions and 14731 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
*.class
*.log
.ensime
.ensime_cache
# sbt specific
dist/*

6
.travis.yml Normal file
View File

@@ -0,0 +1,6 @@
language: scala
sudo: false
script:
- sbt test
jdk:
- oraclejdk8

7
CONTRIBUTING.md Normal file
View File

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

181
README.md
View File

@@ -1,7 +1,7 @@
GitBucket [![Gitter chat](https://badges.gitter.im/takezoe/gitbucket.png)](https://gitter.im/takezoe/gitbucket) [![Build Status](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/badge/icon)](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
GitBucket [![Gitter chat](https://badges.gitter.im/gitbucket/gitbucket.png)](https://gitter.im/gitbucket/gitbucket) [![Build Status](https://travis-ci.org/gitbucket/gitbucket.svg?branch=master)](https://travis-ci.org/gitbucket/gitbucket)
=========
GitBucket is the easily installable Github clone written with Scala.
GitBucket is the easily installable GitHub clone powered by Scala.
Features
@@ -14,30 +14,22 @@ The current version of GitBucket provides a basic features below:
- Wiki
- Issues
- Fork / Pull request
- Mail notification
- Email notification
- Activity timeline
- User management (for Administrators)
- Group (like Organization in Github)
- LDAP integration
- Simple user and group management with LDAP integration
- Gravatar support
- Plug-in system
Following features are not implemented, but we will make them in the future release!
- Comment for the changeset
- Network graph
- Statistics
- Watch / Star
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/takezoe/gitbucket/wiki).
If you want to try the development version of GitBucket, see the documentation for developers at [Wiki](https://github.com/gitbucket/gitbucket/wiki).
Installation
--------
1. Download latest **gitbucket.war** from [the release page](https://github.com/takezoe/gitbucket/releases).
1. Download latest **gitbucket.war** from [the release page](https://github.com/gitbucket/gitbucket/releases).
2. Deploy it to the Servlet 3.0 container such as Tomcat 7.x, Jetty 8.x, GlassFish 3.x or higher.
3. Access **http://[hostname]:[port]/gitbucket/** using your web browser.
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nignx)
If you are using Gitbucket behind a webserver please make sure you have increased the **client_max_body_size** (on nginx)
The default administrator account is **root** and password is **root**.
@@ -50,36 +42,159 @@ or you can start GitBucket by `java -jar gitbucket.war` without servlet containe
To upgrade GitBucket, only replace gitbucket.war. All GitBucket data is stored in HOME/.gitbucket. So if you want to back up GitBucket data, copy this directory to the other disk.
For Installation on Windows Server with IIS see [this wiki page](https://github.com/takezoe/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
For Installation on Windows Server with IIS see [this wiki page](https://github.com/gitbucket/gitbucket/wiki/Installation-on-IIS-and-Helicontech-Zoo)
### Mac OS X
#### Installing Via Homebrew
$ brew install gitbucket
==> Downloading https://github.com/takezoe/gitbucket/releases/download/1.10/gitbucket.war
######################################################################## 100.0%
==> Caveats
Note: When using launchctl the port will be 8080.
```
$ 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/`
On OS X, generate `gitbucket.plist` by [this script](https://raw.githubusercontent.com/gitbucket/gitbucket/master/contrib/macosx/makePlist) and copy it to `~/Library/LaunchAgents/`
Run the following commands in `Terminal` to
- start gitbucket: `launchctl load ~/Library/LaunchAgents/gitbucket.plist`
- stop gitbucket: `launchctl unload ~/Library/LaunchAgents/gitbucket.plist`
Plug-ins
--------
GitBucket has the plug-in system to extend GitBucket from outside of GitBucket. Some plug-ins are available now:
- [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
- [gitbucket-announce-plugin](https://github.com/gitbucket-plugins/gitbucket-announce-plugin)
- [gitbucket-h2-backup-plugin](https://github.com/gitbucket-plugins/gitbucket-h2-backup-plugin)
- [gitbucket-desktopnotify-plugin](https://github.com/yoshiyoshifujii/gitbucket-desktopnotify-plugin)
- [gitbucket-commitgraphs-plugin](https://github.com/yoshiyoshifujii/gitbucket-commitgraphs-plugin)
You can find community plugins other than them at [gitbucket community plugins](http://gitbucket-plugins.github.io/).
Support
--------
- If you have any question about GitBucket, send it to [gitter room](https://gitter.im/gitbucket/gitbucket) before raise an issue.
- Make sure check whether there is a same question or request in the past.
- When raise a new issue, write subject in **English** at least.
- We can also support in Japaneses other than English at [gitter room for Japanese](https://gitter.im/gitbucket/gitbucket_ja).
- First priority of GitBucket is easy installation and reproduce GitHub behavior, so we might reject if your request is against it.
Release Notes
--------
### 3.8 - 31 Oct 2015
- Moved to organization
- Omit diff view for large differences
- Repository creation API
- Render url as link in repository description
- Expand attachable file types
### 3.7 - 3 Oct 2015
- Markdown processor has been switched to [markedj](https://github.com/gitbucket/markedj) from pegdown
- Clone in desktop button
- Providing MD5 and SHA-1 checksum for `gitbucket.war` has started
### 3.6 - 30 Aug 2015
- User interface Improvements: Especially, commit list, issues and pull request have been updated largely.
- Installed plugins list has been available at the system administration console.
- Pages and repository list in the sidebar have been limited and more pages and repositories link is available.
- More reference link notation in Markdown has been supported.
### 3.5 - 1 Aug 2015
- Octicons has been applied
- Global header has been enhanced. Now it's further similar to GitHub.
- Default compare / pull request target has been changed to the parent repository
- A lot of updates for [gitbucket-gist-plugin](https://github.com/gitbucket/gitbucket-gist-plugin)
### 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
- New branch UI
- Compare between specified commit ids
### 2.8 - 1 Feb 2015
- New logo and icons
- New system setting options to control visibility
- Comment on side-by-side diff
- Information message on sign-in page
- Fork repository by group account
### 2.7 - 29 Dec 2014
- Comment for commit and diff
- Fix security issue in markdown rendering
- Some bug fix and improvements
### 2.6 - 24 Nov 2014
- Search box at issues and pull requests
- Information from administrator
- Pull request UI has been updated
- Move to TravisCI from Buildhive
- Some bug fix and improvements
### 2.5 - 4 Nov 2014
- New Dashboard
- Change datetime format
- Create branch from Web UI
- Task list in Markdown
- Some bug fix and improvements
### 2.4.1 - 6 Oct 2014
- Bug fix
### 2.4 - 6 Oct 2014
- New UI is applied to Issues and Pull requests
- Side-by-side diff is available
- Fix relative path problem in Markdown links and images
- Plugin System is disabled in default
- Some bug fix and improvements
### 2.3 - 1 Sep 2014
- Scala based plugin system
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
- Some bug fix and improvements
### 2.2.1 - 5 Aug 2014
- Bug fix
### 2.2 - 4 Aug 2014
- Plug-in system is available
- Move to Scala 2.11, Scalatra 2.3 and Slick 2.1
@@ -216,3 +331,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/)

13
contrib/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Contrib Notes #
The configuration script adapts according to the OS.
The `linux` directory contains scripts for Ubuntu and RedHat.
The Mac scripts have been folded in as well.
Common scripts are in this directory.
This version of scripts has so far only been tested on Ubuntu and Mac. Someone else will have to test on RedHat.
To run:
1. Edit `gitbucket.conf` to suit.
2. Type: `install`

62
contrib/gitbucket.conf Normal file
View File

@@ -0,0 +1,62 @@
# Configuration section is below. Ignore this part
function isUbuntu {
if [ -f /etc/lsb-release ]; then
grep -i ubuntu /etc/lsb-release | head -n 1 | cut -d \ -f 1 | cut -d = -f 2
fi
}
function isRedHat {
if [ -d "/etc/rc.d/init.d" ]; then echo yes; fi
}
function isMac {
if [[ "$(uname -a | cut -d \ -f 1 )" == "Darwin" ]]; then echo yes; fi
}
#
# Configuration section start
#
# Bind host
GITBUCKET_HOST=0.0.0.0
# Other Java option
GITBUCKET_JVM_OPTS=-Dmail.smtp.starttls.enable=true
# Data directory, holds repositories
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_LOG_DIR=/var/log/gitbucket
# Server port
GITBUCKET_PORT=8080
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
GITBUCKET_PREFIX=
# Directory where GitBucket is installed
# Configuration is stored here:
GITBUCKET_DIR=/usr/share/gitbucket
GITBUCKET_WAR_DIR=$GITBUCKET_DIR/lib
# Path to the WAR file
GITBUCKET_WAR_FILE=$GITBUCKET_WAR_DIR/gitbucket.war
# GitBucket version to fetch when installing
GITBUCKET_VERSION=3.5
#
# End of configuration section. Ignore this part
#
if [ `isUbuntu` ]; then
GITBUCKET_SERVICE=/etc/init.d/gitbucket
elif [ `isRedHat` ]; then
GITBUCKET_SERVICE=/etc/rc.d/init.d
elif [ `isMac` ]; then
GITBUCKET_SERVICE=/Library/StartupItems/GitBucket/GitBucket
else
echo "Don't know how to install onto this OS"
exit -2
fi

View File

@@ -1,6 +1,8 @@
#!/bin/bash
#
# /etc/rc.d/init.d/gitbucket
# RedHat: /etc/rc.d/init.d/gitbucket
# Ubuntu: /etc/init.d/gitbucket
# Mac OS/X: /Library/StartupItems/GitBucket
#
# Starts the GitBucket server
#
@@ -8,28 +10,44 @@
# description: Run GitBucket server
# processname: java
# Source function library
. /etc/rc.d/init.d/functions
set -e
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
[ -f /etc/rc.common ] && source /etc/rc.common # Mac OS/X
# Default values
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings
[ -f /etc/sysconfig/gitbucket ] && . /etc/sysconfig/gitbucket
[ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
# Location of the log and PID file
LOG_FILE=/var/log/gitbucket/run.log
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
PID_FILE=/var/run/gitbucket.pid
# Default return value
RETVAL=0
RED='\033[1m\E[37;41m'
GREEN='\033[1m\E[37;42m'
OFF='\E[0m'
if [ -z "$(which success)" ]; then
function success {
printf "%b\n" "$GREEN $* $OFF"
}
fi
if [ -z "$(which failure)" ]; then
function failure {
printf "%b\n" "$RED $* $OFF"
}
fi
RETVAL=0
start() {
echo -n $"Starting GitBucket server: "
# Compile statup parameters
START_OPTS=
if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi
@@ -40,17 +58,15 @@ start() {
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
# Run the Java process
GITBUCKET_HOME="${GITBUCKET_HOME}" java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
RETVAL=$?
# Store PID of the Java process into a file
echo $! > $PID_FILE
if [ $RETVAL -eq 0 ] ; then
success "GitBucket startup"
success "Success"
else
failure "GitBucket startup"
failure "Exit code $RETVAL"
fi
echo
@@ -82,25 +98,41 @@ restart() {
start
}
case "$1" in
start)
## MacOS proxies for System V service hooks:
StartService() {
start
;;
stop)
}
StopService() {
stop
;;
restart)
}
RestartService() {
restart
;;
status)
status -p $PID_FILE java
RETVAL=$?
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
}
exit $RETVAL
if [ `isMac` ]; then
RunService "$1"
else
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status -p $PID_FILE java
RETVAL=$?
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL
fi

69
contrib/install Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
# Only tested on Ubuntu 14.04
# Uses information stored in GitBucket git repo on GitHub as defaults.
# Edit gitbucket.conf before running this
set -e
GITBUCKET_VERSION=2.1
if [ ! -f gitbucket.conf ]; then
echo "gitbucket.conf not found, aborting"
exit -3
fi
source gitbucket.conf
function createDir {
if [ ! -d "$1" ]; then
echo "Making $1 directory."
sudo mkdir -p "$1"
fi
}
if [ "$(which iptables)" ]; then
echo "Opening port $GITBUCKET_PORT in firewall."
sudo iptables -A INPUT -p tcp --dport $GITBUCKET_PORT -j ACCEPT
echo "Please use iptables-persistent:"
echo " sudo apt-get install iptables-persistent"
echo "After installed, you can save/reload iptables rules anytime:"
echo " sudo /etc/init.d/iptables-persistent save"
echo " sudo /etc/init.d/iptables-persistent reload"
fi
createDir "$GITBUCKET_HOME"
createDir "$GITBUCKET_WAR_DIR"
createDir "$GITBUCKET_DIR"
createDir "$GITBUCKET_LOG_DIR"
echo "Fetching GitBucket v$GITBUCKET_VERSION and saving as $GITBUCKET_WAR_FILE"
sudo wget -qO "$GITBUCKET_WAR_FILE" https://github.com/gitbucket/gitbucket/releases/download/$GITBUCKET_VERSION/gitbucket.war
sudo rm -f "$GITBUCKET_LOG_DIR/run.log"
echo "Copying gitbucket.conf to $GITBUCKET_DIR"
sudo cp gitbucket.conf $GITBUCKET_DIR
if [ `isUbuntu` ] || [ `isRedHat` ]; then
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
# Install gitbucket as a service that starts when system boots
sudo chown root:root $GITBUCKET_SERVICE
sudo chmod 755 $GITBUCKET_SERVICE
sudo update-rc.d "$(basename $GITBUCKET_SERVICE)" defaults 98 02
echo "Starting GitBucket service"
sudo $GITBUCKET_SERVICE start
elif [ `isMac` ]; then
sudo macosx/makePlist
echo "Starting GitBucket service"
sudo cp gitbucket.conf "$GITBUCKET_SERVICE"
sudo cp gitbucket.init "$GITBUCKET_SERVICE"
sudo chmod a+x "$GITBUCKET_SERVICE"
sudo "$GITBUCKET_SERVICE" start
else
echo "Don't know how to install this OS"
exit -2
fi
if [ $? != 0 ]; then
less "$GITBUCKET_LOG_DIR/run.log"
fi

View File

@@ -0,0 +1,15 @@
# Contrib Notes #
RPM spec file and init script for Red Hat Enterprise Linux 6.x.
To create RPM:
1. Edit `../../gitbucket.conf` to suit.
2. Edit `gitbucket.init` to suit.
3. Edit `gitbucket.spec` to suit.
4. Place `gitbucket.spec` to rpm/SPECS/.
5. Place `gitbucket.init` and `gitbucket.war` to rpm/SOURCES/.
6. Execute `rpmbuild -ba rpm/SPECS/gitbucket.spec`
This rpm runs gitbucket not as root user but as gitbucket user.
This rpm creates user and group named `gitbucket` at installation.
This rpm make chkconfig of gitbucket to be on.

View File

@@ -0,0 +1,108 @@
#!/bin/bash
#
# RedHat: /etc/rc.d/init.d/gitbucket
#
# Starts the GitBucket server
#
# chkconfig: 345 60 40
# description: Run GitBucket server
# processname: java
[ -f /etc/rc.d/init.d/functions ] && source /etc/rc.d/init.d/functions # RedHat
# Default values
GITBUCKET_HOME=/var/lib/gitbucket
GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# Pull in cq settings
[ -f /etc/sysconfig/gitbucket ] && source /etc/sysconfig/gitbucket # RedHat
[ -f gitbucket.conf ] && source gitbucket.conf # For all systems
# Location of the log and PID file
LOG_FILE=$GITBUCKET_LOG_DIR/run.log
RED='\033[1m\E[37;41m'
GREEN='\033[1m\E[37;42m'
OFF='\E[0m'
RETVAL=0
start() {
echo -n $"Starting GitBucket server: "
START_OPTS=
if [ $GITBUCKET_PORT ]; then
START_OPTS="${START_OPTS} --port=${GITBUCKET_PORT}"
fi
if [ $GITBUCKET_PREFIX ]; then
START_OPTS="${START_OPTS} --prefix=${GITBUCKET_PREFIX}"
fi
if [ $GITBUCKET_HOST ]; then
START_OPTS="${START_OPTS} --host=${GITBUCKET_HOST}"
fi
GITBUCKET_HOME="${GITBUCKET_HOME}" daemon --user=gitbucket java $GITBUCKET_JVM_OPTS -jar $GITBUCKET_WAR_FILE $START_OPTS >>$LOG_FILE 2>&1 &
sleep 3
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
success "Success"
else
failure "Exit code $RETVAL"
fi
echo
return $RETVAL
}
stop() {
echo -n $"Stopping GitBucket server: "
# Run the Java process
pkill -f $GITBUCKET_WAR_FILE >>$LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ] ; then
success "GitBucket stopping"
else
failure "GitBucket stopping"
fi
echo
return $RETVAL
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
pgrep -f $GITBUCKET_WAR_FILE >> $LOG_FILE 2>&1
RETVAL=$?
if [ $RETVAL -eq 0 ]; then
echo $"GitBucket is running...."
else
echo $"GitBucket is stopped"
fi
;;
*)
echo $"Usage: $0 [start|stop|restart|status]"
RETVAL=2
esac
exit $RETVAL

View File

@@ -1,9 +1,9 @@
Name: gitbucket
Summary: GitHub clone written with Scala.
Version: 1.7
Version: 2.6
Release: 1%{?dist}
License: Apache
URL: https://github.com/takezoe/gitbucket
URL: https://github.com/gitbucket/gitbucket
Group: System/Servers
Source0: %{name}.war
Source1: %{name}.init
@@ -26,6 +26,25 @@ GitBucket is the easily installable GitHub clone written with Scala.
%{__install} -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name}
touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
%pre
/usr/sbin/groupadd -r gitbucket &> /dev/null || :
/usr/sbin/useradd -g gitbucket -s /bin/false -r -c "GitBucket GitHub clone" -d %{_sharedstatedir}/%{name} gitbucket &> /dev/null || :
%post
/sbin/chkconfig --add gitbucket
%preun
if [ "$1" = 0 ]; then
/sbin/service gitbucket stop > /dev/null 2>&1
/sbin/chkconfig --del gitbucket
fi
exit 0
%postun
if [ "$1" -ge 1 ]; then
/sbin/service gitbucket restart > /dev/null 2>&1
fi
exit 0
%clean
[ "%{buildroot}" != / ] && %{__rm} -rf "%{buildroot}"
@@ -34,12 +53,28 @@ touch %{buildroot}%{_localstatedir}/log/%{name}/run.log
%files
%defattr(-,root,root,-)
%{_datarootdir}/%{name}/lib/%{name}.war
%{_sysconfdir}/init.d/%{name}
%config %{_sysconfdir}/sysconfig/%{name}
%{_localstatedir}/log/%{name}/run.log
%config %{_sysconfdir}/init.d/%{name}
%config(noreplace) %{_sysconfdir}/sysconfig/%{name}
%attr(0755,gitbucket,gitbucket) %{_sharedstatedir}/%{name}
%attr(0750,gitbucket,gitbucket) %{_localstatedir}/log/%{name}
%changelog
* Mon Nov 24 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.6
* Sun Nov 09 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.5
* Sun Oct 26 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.4.1
* Mon Jul 21 2014 Toru Takahashi <torutk at gmail.com>
- execute as gitbucket user
* Sun Jul 20 2014 Toru Takahashi <torutk at gmail.com>
- Version bump to v2.1.
* Mon Oct 28 2013 Jiri Tyr <jiri_DOT_tyr at gmail.com>
- Version bump to v1.7.

View File

@@ -1,3 +1,10 @@
#!/bin/bash
# From http://docstore.mik.ua/orelly/unix3/mac/ch02_02.htm
source gitbucket.conf
GITBUCKET_SERVICE_DIR=`dirname "$GITBUCKET_SERVICE"`
mkdir -p "$GITBUCKET_SERVICE_DIR"
cat << EOF > "$GITBUCKET_SERVICE_DIR/gitbucket.plist"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@@ -7,14 +14,15 @@
<key>ProgramArguments</key>
<array>
<string>/usr/bin/java</string>
<string>-Dmail.smtp.starttls.enable=true</string>
<string>$GITBUCKET_JVM_OPTS</string>
<string>-jar</string>
<string>gitbucket.war</string>
<string>--host=127.0.0.1</string>
<string>--port=8080</string>
<string>--host=$GITBUCKET_HOST</string>
<string>--port=$GITBUCKET_PORT</string>
<string>--https=true</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
EOF

View File

@@ -1,17 +0,0 @@
# Bind host
#GITBUCKET_HOST=0.0.0.0
# Server port
#GITBUCKET_PORT=8080
# Data directory (GITBUCKET_HOME/gitbucket)
#GITBUCKET_HOME=/var/lib/gitbucket
# Path to the WAR file
#GITBUCKET_WAR_FILE=/usr/share/gitbucket/lib/gitbucket.war
# URL prefix for the GitBucket page (http://<host>:<port>/<prefix>/)
#GITBUCKET_PREFIX=
# Other Java option
#GITBUCKET_JVM_OPTS=

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 [gitbucket.core.servlet.AutoUpdate](https://github.com/gitbucket/gitbucket/blob/master/src/main/scala/gitbucket/core/servlet/AutoUpdate.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/gitbucket/gitbucket/tree/master/src/main/resources/update) as ```MAJOR_MINOR.sql```.
GitBucket stores the current version to ```GITBUCKET_HOME/version``` and checks it at start-up. If the stored version differs from the actual version, it executes differences of SQL files between the stored version and the actual version. And ```GITBUCKET_HOME/version``` is updated by the actual version.
We can also add any Scala code for upgrade GitBucket which modifies 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>
```

63
doc/how_to_run.md Normal file
View File

@@ -0,0 +1,63 @@
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:
Windows:
```
C:\gitbucket> sbt
...
> container:start
...
> ~ ;copy-resources;aux-compile
```
Linux:
```
~/gitbucket$ ./sbt.sh
...
> container:start
...
> ~ ;copy-resources;aux-compile
```
Build war file
--------
To build war file, run the following command:
Windows:
```
C:\gitbucket> sbt package
```
Linux:
```
~/gitbucket$ ./sbt.sh package
```
`gitbucket_2.11-x.x.x.war` is generated into `target/scala-2.11`.
To build executable war file, run
* Windows: Not available
* Linux: `./release/make-release-war.sh`
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.

794
doc/icons.svg Normal file
View File

@@ -0,0 +1,794 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="2000" height="2000" viewBox="0, 0, 2000, 2000">
<g id="Layer_1">
<g id="path4000">
<path d="M583.868,482.54 C583.868,482.54 596.594,491.899 620.234,491.631 C643.873,491.363 650.538,482.54 650.538,482.54 L671.558,404.092 L563.665,404.092 z" fill="#B3B3B3"/>
<path d="M583.868,482.54 C583.868,482.54 596.594,491.899 620.234,491.631 C643.873,491.363 650.538,482.54 650.538,482.54 L671.558,404.092 L563.665,404.092 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1"/>
</g>
<path d="M215.018,822.461 C215.018,822.461 217.291,803.608 246.459,799.039 C256.428,797.478 278.667,793.574 278.667,770.933" fill-opacity="0" stroke="#B3B3B3" stroke-width="17.059" id="path3207"/>
<path d="M62.863,746.321 C62.863,801.014 132.287,797.758 132.287,797.758" fill-opacity="0" stroke="#B3B3B3" stroke-width="17.56" id="path4318"/>
<g id="rect3935">
<path d="M9.359,600.772 L185.078,600.772 L185.078,623.958 L9.359,623.958 z" fill="#B3B3B3"/>
<path d="M9.359,600.772 L185.078,600.772 L185.078,623.958 L9.359,623.958 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.781" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g id="path3894-1">
<path d="M450.793,475.007 L401.329,532.789 L414.646,471.965 z" fill="#B3B3B3"/>
<path d="M450.793,475.007 L401.329,532.789 L414.646,471.965 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="7.485"/>
</g>
<g id="rect3088-5-5">
<path d="M373.916,407.856 L485.485,407.856 L485.485,477.7 L373.916,477.7 z" fill="#B3B3B3"/>
<path d="M373.916,407.856 L485.485,407.856 L485.485,477.7 L373.916,477.7 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="19.036" stroke-linejoin="round"/>
</g>
<path d="M306.191,155.761 L306.191,70.894 C306.191,70.894 307.419,60.791 295.143,60.791 C282.868,60.791 261.385,60.791 261.385,60.791" fill-opacity="0" stroke="#008000" stroke-width="15" id="path3850"/>
<g id="path2991">
<path d="M159.256,113.426 C159.256,151.442 128.438,182.26 90.422,182.26 C52.407,182.26 21.589,151.442 21.589,113.426 C21.589,75.411 52.407,44.593 90.422,44.593 C128.438,44.593 159.256,75.411 159.256,113.426 z" fill="#008000"/>
<path d="M159.256,113.426 C159.256,151.442 128.438,182.26 90.422,182.26 C52.407,182.26 21.589,151.442 21.589,113.426 C21.589,75.411 52.407,44.593 90.422,44.593 C128.438,44.593 159.256,75.411 159.256,113.426 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993">
<path d="M148.532,113.917 C148.532,145.702 122.765,171.469 90.98,171.469 C59.194,171.469 33.427,145.702 33.427,113.917 C33.427,82.131 59.194,56.364 90.98,56.364 C122.765,56.364 148.532,82.131 148.532,113.917 z" fill="#FFFFFF"/>
<path d="M148.532,113.917 C148.532,145.702 122.765,171.469 90.98,171.469 C59.194,171.469 33.427,145.702 33.427,113.917 C33.427,82.131 59.194,56.364 90.98,56.364 C122.765,56.364 148.532,82.131 148.532,113.917 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<g id="rect2995">
<path d="M81.339,65.348 L100.605,65.348 L100.605,130.839 L81.339,130.839 z" fill="#008000"/>
<path d="M81.339,65.348 L100.605,65.348 L100.605,130.839 L81.339,130.839 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997">
<path d="M81.509,143.757 L101.128,143.757 L101.128,161.089 L81.509,161.089 z" fill="#008000"/>
<path d="M81.509,143.757 L101.128,143.757 L101.128,161.089 L81.509,161.089 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="rect3818">
<path d="M230.385,74.531 L232.534,74.531 L232.534,143.355 L230.385,143.355 z" fill="#FFFFFF"/>
<path d="M230.385,74.531 L232.534,74.531 L232.534,143.355 L230.385,143.355 z" fill-opacity="0" stroke="#008000" stroke-width="15"/>
</g>
<g id="path3795-4">
<path d="M246.225,160.683 C246.225,168.561 239.776,174.947 231.82,174.947 C223.864,174.947 217.414,168.561 217.414,160.683 C217.414,152.805 223.864,146.419 231.82,146.419 C239.776,146.419 246.225,152.805 246.225,160.683 z" fill="#FFFFFF"/>
<path d="M246.225,160.683 C246.225,168.561 239.776,174.947 231.82,174.947 C223.864,174.947 217.414,168.561 217.414,160.683 C217.414,152.805 223.864,146.419 231.82,146.419 C239.776,146.419 246.225,152.805 246.225,160.683 z" fill-opacity="0" stroke="#008000" stroke-width="7.989"/>
</g>
<g id="path3795">
<path d="M245.212,61.462 C245.212,69.34 238.762,75.726 230.806,75.726 C222.85,75.726 216.4,69.34 216.4,61.462 C216.4,53.585 222.85,47.198 230.806,47.198 C238.762,47.198 245.212,53.585 245.212,61.462 z" fill="#FFFFFF"/>
<path d="M245.212,61.462 C245.212,69.34 238.762,75.726 230.806,75.726 C222.85,75.726 216.4,69.34 216.4,61.462 C216.4,53.585 222.85,47.198 230.806,47.198 C238.762,47.198 245.212,53.585 245.212,61.462 z" fill-opacity="0" stroke="#008000" stroke-width="7.989"/>
</g>
<g id="path3795-4-0">
<path d="M320.671,160.75 C320.671,168.628 314.221,175.014 306.265,175.014 C298.309,175.014 291.86,168.628 291.86,160.75 C291.86,152.872 298.309,146.486 306.265,146.486 C314.221,146.486 320.671,152.872 320.671,160.75 z" fill="#FFFFFF"/>
<path d="M320.671,160.75 C320.671,168.628 314.221,175.014 306.265,175.014 C298.309,175.014 291.86,168.628 291.86,160.75 C291.86,152.872 298.309,146.486 306.265,146.486 C314.221,146.486 320.671,152.872 320.671,160.75 z" fill-opacity="0" stroke="#008000" stroke-width="7.989"/>
</g>
<g id="path3852">
<path d="M279.602,36.54 L279.602,83.2 L249.511,61.99 z" fill="#008000"/>
<path d="M279.602,36.54 L279.602,83.2 L249.511,61.99 z" fill-opacity="0" stroke="#008000" stroke-width="0.55"/>
</g>
<path d="M308.603,323.909 L308.603,239.042 C308.603,239.042 309.831,228.939 297.555,228.939 C285.279,228.939 263.797,228.939 263.797,228.939" fill-opacity="0" stroke="#800000" stroke-width="15" id="path3850-4"/>
<g id="path2991-8">
<path d="M161.667,281.574 C161.667,319.59 130.85,350.407 92.834,350.407 C54.819,350.407 24.001,319.59 24.001,281.574 C24.001,243.558 54.819,212.741 92.834,212.741 C130.85,212.741 161.667,243.558 161.667,281.574 z" fill="#800000"/>
<path d="M161.667,281.574 C161.667,319.59 130.85,350.407 92.834,350.407 C54.819,350.407 24.001,319.59 24.001,281.574 C24.001,243.558 54.819,212.741 92.834,212.741 C130.85,212.741 161.667,243.558 161.667,281.574 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-8">
<path d="M150.944,282.064 C150.944,313.85 125.177,339.617 93.391,339.617 C61.606,339.617 35.839,313.85 35.839,282.064 C35.839,250.279 61.606,224.512 93.391,224.512 C125.177,224.512 150.944,250.279 150.944,282.064 z" fill="#FFFFFF"/>
<path d="M150.944,282.064 C150.944,313.85 125.177,339.617 93.391,339.617 C61.606,339.617 35.839,313.85 35.839,282.064 C35.839,250.279 61.606,224.512 93.391,224.512 C125.177,224.512 150.944,250.279 150.944,282.064 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<g id="rect2995-2">
<path d="M83.75,233.496 L103.017,233.496 L103.017,298.986 L83.75,298.986 z" fill="#800000"/>
<path d="M83.75,233.496 L103.017,233.496 L103.017,298.986 L83.75,298.986 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997-4">
<path d="M83.921,311.905 L103.54,311.905 L103.54,329.237 L83.921,329.237 z" fill="#800000"/>
<path d="M83.921,311.905 L103.54,311.905 L103.54,329.237 L83.921,329.237 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="rect3818-5">
<path d="M232.797,242.679 L234.946,242.679 L234.946,311.502 L232.797,311.502 z" fill="#FFFFFF"/>
<path d="M232.797,242.679 L234.946,242.679 L234.946,311.502 L232.797,311.502 z" fill-opacity="0" stroke="#800000" stroke-width="15"/>
</g>
<g id="path3795-4-5">
<path d="M248.637,328.831 C248.637,336.708 242.187,343.095 234.231,343.095 C226.275,343.095 219.826,336.708 219.826,328.831 C219.826,320.953 226.275,314.566 234.231,314.566 C242.187,314.566 248.637,320.953 248.637,328.831 z" fill="#FFFFFF"/>
<path d="M248.637,328.831 C248.637,336.708 242.187,343.095 234.231,343.095 C226.275,343.095 219.826,336.708 219.826,328.831 C219.826,320.953 226.275,314.566 234.231,314.566 C242.187,314.566 248.637,320.953 248.637,328.831 z" fill-opacity="0" stroke="#800000" stroke-width="7.989"/>
</g>
<g id="path3795-1">
<path d="M247.623,229.61 C247.623,237.488 241.174,243.874 233.218,243.874 C225.262,243.874 218.812,237.488 218.812,229.61 C218.812,221.732 225.262,215.346 233.218,215.346 C241.174,215.346 247.623,221.732 247.623,229.61 z" fill="#FFFFFF"/>
<path d="M247.623,229.61 C247.623,237.488 241.174,243.874 233.218,243.874 C225.262,243.874 218.812,237.488 218.812,229.61 C218.812,221.732 225.262,215.346 233.218,215.346 C241.174,215.346 247.623,221.732 247.623,229.61 z" fill-opacity="0" stroke="#800000" stroke-width="7.989"/>
</g>
<g id="path3795-4-0-7">
<path d="M323.083,328.898 C323.083,336.775 316.633,343.162 308.677,343.162 C300.721,343.162 294.271,336.775 294.271,328.898 C294.271,321.02 300.721,314.633 308.677,314.633 C316.633,314.633 323.083,321.02 323.083,328.898 z" fill="#FFFFFF"/>
<path d="M323.083,328.898 C323.083,336.775 316.633,343.162 308.677,343.162 C300.721,343.162 294.271,336.775 294.271,328.898 C294.271,321.02 300.721,314.633 308.677,314.633 C316.633,314.633 323.083,321.02 323.083,328.898 z" fill-opacity="0" stroke="#800000" stroke-width="7.989"/>
</g>
<g id="path3852-1">
<path d="M282.013,204.688 L282.013,251.348 L251.923,230.137 z" fill="#800000"/>
<path d="M282.013,204.688 L282.013,251.348 L251.923,230.137 z" fill-opacity="0" stroke="#800000" stroke-width="0.55"/>
</g>
<path d="M439.891,42.342 L643.358,42.342 L643.358,245.81 L439.891,245.81 z" fill="#CCCCCC" id="rect2985"/>
<path d="M610.846,124.458 C610.846,161.784 580.587,192.042 543.262,192.042 C505.936,192.042 475.678,161.784 475.678,124.458 C475.678,87.132 505.936,56.874 543.262,56.874 C580.587,56.874 610.846,87.132 610.846,124.458 z" fill="#FFFFFF" id="path2989"/>
<g id="path2993-2">
<path d="M484.685,245.601 L603.47,245.601 L544.162,112.49 z" fill="#FFFFFF"/>
<path d="M484.685,245.601 L603.47,245.601 L544.162,112.49 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="1.054"/>
</g>
<path d="M307.593,489.153 L307.593,404.286 C307.593,404.286 308.82,394.183 296.545,394.183 C284.269,394.183 262.786,394.183 262.786,394.183" fill-opacity="0" stroke="#B3B3B3" stroke-width="15" id="path3850-1"/>
<g id="path2991-7">
<path d="M160.657,446.818 C160.657,484.833 129.84,515.651 91.824,515.651 C53.808,515.651 22.991,484.833 22.991,446.818 C22.991,408.802 53.808,377.984 91.824,377.984 C129.84,377.984 160.657,408.802 160.657,446.818 z" fill="#B3B3B3"/>
<path d="M160.657,446.818 C160.657,484.833 129.84,515.651 91.824,515.651 C53.808,515.651 22.991,484.833 22.991,446.818 C22.991,408.802 53.808,377.984 91.824,377.984 C129.84,377.984 160.657,408.802 160.657,446.818 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4">
<path d="M149.934,447.308 C149.934,479.094 124.167,504.861 92.381,504.861 C60.596,504.861 34.828,479.094 34.828,447.308 C34.828,415.523 60.596,389.756 92.381,389.756 C124.167,389.756 149.934,415.523 149.934,447.308 z" fill="#FFFFFF"/>
<path d="M149.934,447.308 C149.934,479.094 124.167,504.861 92.381,504.861 C60.596,504.861 34.828,479.094 34.828,447.308 C34.828,415.523 60.596,389.756 92.381,389.756 C124.167,389.756 149.934,415.523 149.934,447.308 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<g id="rect2995-0">
<path d="M82.74,398.739 L102.007,398.739 L102.007,464.23 L82.74,464.23 z" fill="#B3B3B3"/>
<path d="M82.74,398.739 L102.007,398.739 L102.007,464.23 L82.74,464.23 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997-9">
<path d="M82.911,477.149 L102.53,477.149 L102.53,494.48 L82.911,494.48 z" fill="#B3B3B3"/>
<path d="M82.911,477.149 L102.53,477.149 L102.53,494.48 L82.911,494.48 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="rect3818-4">
<path d="M231.787,407.923 L233.935,407.923 L233.935,476.746 L231.787,476.746 z" fill="#FFFFFF"/>
<path d="M231.787,407.923 L233.935,407.923 L233.935,476.746 L231.787,476.746 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="15"/>
</g>
<g id="path3795-4-8">
<path d="M247.627,494.074 C247.627,501.952 241.177,508.338 233.221,508.338 C225.265,508.338 218.816,501.952 218.816,494.074 C218.816,486.197 225.265,479.81 233.221,479.81 C241.177,479.81 247.627,486.197 247.627,494.074 z" fill="#FFFFFF"/>
<path d="M247.627,494.074 C247.627,501.952 241.177,508.338 233.221,508.338 C225.265,508.338 218.816,501.952 218.816,494.074 C218.816,486.197 225.265,479.81 233.221,479.81 C241.177,479.81 247.627,486.197 247.627,494.074 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3795-8">
<path d="M246.613,394.854 C246.613,402.732 240.164,409.118 232.208,409.118 C224.252,409.118 217.802,402.732 217.802,394.854 C217.802,386.976 224.252,380.59 232.208,380.59 C240.164,380.59 246.613,386.976 246.613,394.854 z" fill="#FFFFFF"/>
<path d="M246.613,394.854 C246.613,402.732 240.164,409.118 232.208,409.118 C224.252,409.118 217.802,402.732 217.802,394.854 C217.802,386.976 224.252,380.59 232.208,380.59 C240.164,380.59 246.613,386.976 246.613,394.854 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3795-4-0-2">
<path d="M322.072,494.141 C322.072,502.019 315.623,508.405 307.667,508.405 C299.711,508.405 293.261,502.019 293.261,494.141 C293.261,486.264 299.711,479.877 307.667,479.877 C315.623,479.877 322.072,486.264 322.072,494.141 z" fill="#FFFFFF"/>
<path d="M322.072,494.141 C322.072,502.019 315.623,508.405 307.667,508.405 C299.711,508.405 293.261,502.019 293.261,494.141 C293.261,486.264 299.711,479.877 307.667,479.877 C315.623,479.877 322.072,486.264 322.072,494.141 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3852-4">
<path d="M281.003,369.932 L281.003,416.591 L250.912,395.381 z" fill="#B3B3B3"/>
<path d="M281.003,369.932 L281.003,416.591 L250.912,395.381 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="0.55"/>
</g>
<g id="rect3088">
<path d="M394.325,382.21 L518.574,382.21 L518.574,459.992 L394.325,459.992 z" fill="#FFFFFF"/>
<path d="M394.325,382.21 L518.574,382.21 L518.574,459.992 L394.325,459.992 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="21.2" stroke-linejoin="round"/>
</g>
<g id="path3894">
<path d="M452.812,456.961 L505.34,514.54 L491.198,453.931 z" fill="#B3B3B3"/>
<path d="M452.812,456.961 L505.34,514.54 L491.198,453.931 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="7.7"/>
</g>
<g id="rect3088-5">
<path d="M405.586,381.471 L517.155,381.471 L517.155,451.314 L405.586,451.314 z" fill="#B3B3B3"/>
<path d="M405.586,381.471 L517.155,381.471 L517.155,451.314 L405.586,451.314 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="19.036" stroke-linejoin="round"/>
</g>
<g id="path2991-7-7">
<path d="M152.8,612.829 C152.8,643.627 127.833,668.593 97.036,668.593 C66.238,668.593 41.272,643.627 41.272,612.829 C41.272,582.032 66.238,557.065 97.036,557.065 C127.833,557.065 152.8,582.032 152.8,612.829 z" fill="#B3B3B3"/>
<path d="M152.8,612.829 C152.8,643.627 127.833,668.593 97.036,668.593 C66.238,668.593 41.272,643.627 41.272,612.829 C41.272,582.032 66.238,557.065 97.036,557.065 C127.833,557.065 152.8,582.032 152.8,612.829 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="0.535"/>
</g>
<g id="path2993-4-1">
<path d="M135.769,612.856 C135.769,634.203 118.464,651.509 97.117,651.509 C75.769,651.509 58.464,634.203 58.464,612.856 C58.464,591.509 75.769,574.204 97.117,574.204 C118.464,574.204 135.769,591.509 135.769,612.856 z" fill="#FFFFFF"/>
<path d="M135.769,612.856 C135.769,634.203 118.464,651.509 97.117,651.509 C75.769,651.509 58.464,634.203 58.464,612.856 C58.464,591.509 75.769,574.204 97.117,574.204 C118.464,574.204 135.769,591.509 135.769,612.856 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.371"/>
</g>
<g id="path2991-7-1">
<path d="M360.813,616.87 C360.813,654.885 329.996,685.703 291.98,685.703 C253.964,685.703 223.147,654.885 223.147,616.87 C223.147,578.854 253.964,548.036 291.98,548.036 C329.996,548.036 360.813,578.854 360.813,616.87 z" fill="#B3B3B3"/>
<path d="M360.813,616.87 C360.813,654.885 329.996,685.703 291.98,685.703 C253.964,685.703 223.147,654.885 223.147,616.87 C223.147,578.854 253.964,548.036 291.98,548.036 C329.996,548.036 360.813,578.854 360.813,616.87 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-5">
<path d="M350.09,617.36 C350.09,649.146 324.323,674.913 292.537,674.913 C260.752,674.913 234.984,649.146 234.984,617.36 C234.984,585.575 260.752,559.808 292.537,559.808 C324.323,559.808 350.09,585.575 350.09,617.36 z" fill="#FFFFFF"/>
<path d="M350.09,617.36 C350.09,649.146 324.323,674.913 292.537,674.913 C260.752,674.913 234.984,649.146 234.984,617.36 C234.984,585.575 260.752,559.808 292.537,559.808 C324.323,559.808 350.09,585.575 350.09,617.36 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<g id="rect2995-0-2">
<path d="M282.896,568.792 L302.163,568.792 L302.163,634.282 L282.896,634.282 z" fill="#B3B3B3"/>
<path d="M282.896,568.792 L302.163,568.792 L302.163,634.282 L282.896,634.282 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997-9-7">
<path d="M283.067,647.2 L302.686,647.2 L302.686,664.532 L283.067,664.532 z" fill="#B3B3B3"/>
<path d="M283.067,647.2 L302.686,647.2 L302.686,664.532 L283.067,664.532 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="rect4046-3">
<path d="M265.347,590.671 L222.652,566.769 L229.715,603.704 L265.347,590.671 z" fill="#FFFFFF"/>
<path d="M265.347,590.671 L222.652,566.769 L229.715,603.704 L265.347,590.671 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="1.93" stroke-linecap="round"/>
</g>
<g id="rect4046">
<path d="M258.314,586.135 L229.499,570.007 L234.118,595.192 L258.314,586.135 z" fill="#B3B3B3"/>
<path d="M258.314,586.135 L229.499,570.007 L234.118,595.192 L258.314,586.135 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
</g>
<g id="rect4046-3-2">
<path d="M317.971,646.518 L360.666,670.419 L353.603,633.485 L317.971,646.518 z" fill="#FFFFFF"/>
<path d="M317.971,646.518 L360.666,670.419 L353.603,633.485 L317.971,646.518 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="1.93" stroke-linecap="round"/>
</g>
<g id="rect4046-1">
<path d="M323.933,652.482 L352.748,668.61 L348.129,643.425 L323.933,652.482 z" fill="#B3B3B3"/>
<path d="M323.933,652.482 L352.748,668.61 L348.129,643.425 L323.933,652.482 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
</g>
<g id="path2991-7-79">
<path d="M540.417,614.85 C540.417,652.865 509.6,683.683 471.584,683.683 C433.568,683.683 402.751,652.865 402.751,614.85 C402.751,576.834 433.568,546.016 471.584,546.016 C509.6,546.016 540.417,576.834 540.417,614.85 z" fill="#B3B3B3"/>
<path d="M540.417,614.85 C540.417,652.865 509.6,683.683 471.584,683.683 C433.568,683.683 402.751,652.865 402.751,614.85 C402.751,576.834 433.568,546.016 471.584,546.016 C509.6,546.016 540.417,576.834 540.417,614.85 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-54">
<path d="M529.694,615.34 C529.694,647.125 503.927,672.893 472.141,672.893 C440.356,672.893 414.589,647.125 414.589,615.34 C414.589,583.555 440.356,557.787 472.141,557.787 C503.927,557.787 529.694,583.555 529.694,615.34 z" fill="#FFFFFF"/>
<path d="M529.694,615.34 C529.694,647.125 503.927,672.893 472.141,672.893 C440.356,672.893 414.589,647.125 414.589,615.34 C414.589,583.555 440.356,557.787 472.141,557.787 C503.927,557.787 529.694,583.555 529.694,615.34 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<g id="rect4271">
<path d="M521.629,568.481 L546.81,594.751 L505.942,637.386 L480.762,611.116 z" fill="#FFFFFF"/>
<path d="M521.629,568.481 L546.81,594.751 L505.942,637.386 L480.762,611.116 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="4.802" stroke-linecap="round"/>
</g>
<g id="rect2995-0-3-3">
<path d="M486.899,587.752 L498.348,576.303 L525.283,603.238 L513.834,614.687 z" fill="#B3B3B3"/>
<path d="M486.899,587.752 L498.348,576.303 L525.283,603.238 L513.834,614.687 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="0.513"/>
</g>
<g id="rect2995-0-3-2">
<path d="M540.917,564.622 L552.295,576 L513.62,614.676 L502.242,603.298 z" fill="#B3B3B3"/>
<path d="M540.917,564.622 L552.295,576 L513.62,614.676 L502.242,603.298 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="0.613"/>
</g>
<g id="rect2995-0-3">
<path d="M462.5,566.771 L481.767,566.771 L481.767,632.262 L462.5,632.262 z" fill="#B3B3B3"/>
<path d="M462.5,566.771 L481.767,566.771 L481.767,632.262 L462.5,632.262 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997-9-1">
<path d="M462.955,644.956 L482.574,644.956 L482.574,662.287 L462.955,662.287 z" fill="#B3B3B3"/>
<path d="M462.955,644.956 L482.574,644.956 L482.574,662.287 L462.955,662.287 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="rect3818-4-8">
<path d="M62.784,747.977 L64.932,747.977 L64.932,816.8 L62.784,816.8 z" fill="#FFFFFF"/>
<path d="M62.784,747.977 L64.932,747.977 L64.932,816.8 L62.784,816.8 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="15"/>
</g>
<g id="path3795-4-8-7">
<path d="M78.624,834.13 C78.624,842.008 72.174,848.394 64.218,848.394 C56.262,848.394 49.813,842.008 49.813,834.13 C49.813,826.252 56.262,819.866 64.218,819.866 C72.174,819.866 78.624,826.252 78.624,834.13 z" fill="#FFFFFF"/>
<path d="M78.624,834.13 C78.624,842.008 72.174,848.394 64.218,848.394 C56.262,848.394 49.813,842.008 49.813,834.13 C49.813,826.252 56.262,819.866 64.218,819.866 C72.174,819.866 78.624,826.252 78.624,834.13 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3795-8-4">
<path d="M77.61,734.91 C77.61,742.788 71.161,749.174 63.205,749.174 C55.249,749.174 48.799,742.788 48.799,734.91 C48.799,727.032 55.249,720.646 63.205,720.646 C71.161,720.646 77.61,727.032 77.61,734.91 z" fill="#FFFFFF"/>
<path d="M77.61,734.91 C77.61,742.788 71.161,749.174 63.205,749.174 C55.249,749.174 48.799,742.788 48.799,734.91 C48.799,727.032 55.249,720.646 63.205,720.646 C71.161,720.646 77.61,727.032 77.61,734.91 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3795-4-8-7-7">
<path d="M152.898,797.667 C152.898,805.544 146.448,811.931 138.492,811.931 C130.536,811.931 124.087,805.544 124.087,797.667 C124.087,789.789 130.536,783.402 138.492,783.402 C146.448,783.402 152.898,789.789 152.898,797.667 z" fill="#FFFFFF"/>
<path d="M152.898,797.667 C152.898,805.544 146.448,811.931 138.492,811.931 C130.536,811.931 124.087,805.544 124.087,797.667 C124.087,789.789 130.536,783.402 138.492,783.402 C146.448,783.402 152.898,789.789 152.898,797.667 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="rect3818-4-8-4">
<path d="M213.61,747.063 L215.759,747.063 L215.759,815.886 L213.61,815.886 z" fill="#FFFFFF"/>
<path d="M213.61,747.063 L215.759,747.063 L215.759,815.886 L213.61,815.886 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="15"/>
</g>
<g id="path3795-4-8-7-8">
<path d="M229.451,833.216 C229.451,841.094 223.001,847.48 215.045,847.48 C207.089,847.48 200.639,841.094 200.639,833.216 C200.639,825.338 207.089,818.952 215.045,818.952 C223.001,818.952 229.451,825.338 229.451,833.216 z" fill="#FFFFFF"/>
<path d="M229.451,833.216 C229.451,841.094 223.001,847.48 215.045,847.48 C207.089,847.48 200.639,841.094 200.639,833.216 C200.639,825.338 207.089,818.952 215.045,818.952 C223.001,818.952 229.451,825.338 229.451,833.216 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8">
<path d="M228.437,733.996 C228.437,741.873 221.987,748.26 214.031,748.26 C206.075,748.26 199.626,741.873 199.626,733.996 C199.626,726.118 206.075,719.731 214.031,719.731 C221.987,719.731 228.437,726.118 228.437,733.996 z" fill="#FFFFFF"/>
<path d="M228.437,733.996 C228.437,741.873 221.987,748.26 214.031,748.26 C206.075,748.26 199.626,741.873 199.626,733.996 C199.626,726.118 206.075,719.731 214.031,719.731 C221.987,719.731 228.437,726.118 228.437,733.996 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-2">
<path d="M295.005,765.037 C295.005,772.915 288.555,779.301 280.599,779.301 C272.643,779.301 266.193,772.915 266.193,765.037 C266.193,757.159 272.643,750.773 280.599,750.773 C288.555,750.773 295.005,757.159 295.005,765.037 z" fill="#FFFFFF"/>
<path d="M295.005,765.037 C295.005,772.915 288.555,779.301 280.599,779.301 C272.643,779.301 266.193,772.915 266.193,765.037 C266.193,757.159 272.643,750.773 280.599,750.773 C288.555,750.773 295.005,757.159 295.005,765.037 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.989"/>
</g>
<g id="path3992-4">
<path d="M677.589,395.106 C677.589,405.481 650.861,413.892 617.891,413.892 C584.921,413.892 558.193,405.481 558.193,395.106 C558.193,384.731 584.921,376.32 617.891,376.32 C650.861,376.32 677.589,384.731 677.589,395.106 z" fill="#B3B3B3"/>
<path d="M677.589,395.106 C677.589,405.481 650.861,413.892 617.891,413.892 C584.921,413.892 558.193,405.481 558.193,395.106 C558.193,384.731 584.921,376.32 617.891,376.32 C650.861,376.32 677.589,384.731 677.589,395.106 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="10" stroke-linecap="round"/>
</g>
<g id="rect2995-0-2-7">
<path d="M651.087,376.107 L651.087,396.357 L632.275,396.357 L632.275,423.576 L651.087,423.576 L651.087,441.482 L678.275,441.482 L678.275,423.576 L697.65,423.576 L697.65,396.357 L678.275,396.357 L678.275,376.107 L651.087,376.107 z" fill="#B3B3B3"/>
<path d="M651.087,376.107 L651.087,396.357 L632.275,396.357 L632.275,423.576 L651.087,423.576 L651.087,441.482 L678.275,441.482 L678.275,423.576 L697.65,423.576 L697.65,396.357 L678.275,396.357 L678.275,376.107 L651.087,376.107 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="8.571"/>
</g>
<g id="path2991-7-2">
<path d="M723.493,615.976 C723.493,653.992 692.676,684.81 654.66,684.81 C616.644,684.81 585.827,653.992 585.827,615.976 C585.827,577.961 616.644,547.143 654.66,547.143 C692.676,547.143 723.493,577.961 723.493,615.976 z" fill="#B3B3B3"/>
<path d="M723.493,615.976 C723.493,653.992 692.676,684.81 654.66,684.81 C616.644,684.81 585.827,653.992 585.827,615.976 C585.827,577.961 616.644,547.143 654.66,547.143 C692.676,547.143 723.493,577.961 723.493,615.976 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-7">
<path d="M707.365,616.038 C707.365,645.075 683.826,668.615 654.789,668.615 C625.751,668.615 602.212,645.075 602.212,616.038 C602.212,587.001 625.751,563.462 654.789,563.462 C683.826,563.462 707.365,587.001 707.365,616.038 z" fill="#FFFFFF"/>
<path d="M707.365,616.038 C707.365,645.075 683.826,668.615 654.789,668.615 C625.751,668.615 602.212,645.075 602.212,616.038 C602.212,587.001 625.751,563.462 654.789,563.462 C683.826,563.462 707.365,587.001 707.365,616.038 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.504"/>
</g>
<g id="rect2995-0-6">
<path d="M695.28,567.323 L708.018,581.411 L615.14,665.393 L602.401,651.305 z" fill="#B3B3B3"/>
<path d="M695.28,567.323 L708.018,581.411 L615.14,665.393 L602.401,651.305 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.007"/>
</g>
<g id="g4284">
<g id="rect4201">
<path d="M336.17,737.103 C336.17,737.103 372.868,730.771 385.714,731.389 C398.56,732.007 412.402,737.103 412.402,737.103 L412.402,830.478 C412.402,830.478 398.56,825.382 385.714,824.764 C372.868,824.146 336.17,830.478 336.17,830.478 z" fill="#FFFFFF"/>
<path d="M336.17,737.103 C336.17,737.103 372.868,730.771 385.714,731.389 C398.56,732.007 412.402,737.103 412.402,737.103 L412.402,830.478 C412.402,830.478 398.56,825.382 385.714,824.764 C372.868,824.146 336.17,830.478 336.17,830.478 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9.482" stroke-linecap="round"/>
</g>
<g id="rect4203">
<path d="M355.332,755.123 L391.81,755.123 L391.81,758.744 L355.332,758.744 z" fill="#FFFFFF"/>
<path d="M355.332,755.123 L391.81,755.123 L391.81,758.744 L355.332,758.744 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="rect4203-2">
<path d="M356.047,777.981 L392.525,777.981 L392.525,781.602 L356.047,781.602 z" fill="#FFFFFF"/>
<path d="M356.047,777.981 L392.525,777.981 L392.525,781.602 L356.047,781.602 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="rect4203-2-3">
<path d="M356.047,799.981 L392.525,799.981 L392.525,803.602 L356.047,803.602 z" fill="#FFFFFF"/>
<path d="M356.047,799.981 L392.525,799.981 L392.525,803.602 L356.047,803.602 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="path4245">
<path d="M332.988,837.946 C332.988,837.946 372.01,827.288 386.458,828.206 C400.906,829.124 417.012,837.946 417.012,837.946" fill="#B3B3B3"/>
<path d="M332.988,837.946 C332.988,837.946 372.01,827.288 386.458,828.206 C400.906,829.124 417.012,837.946 417.012,837.946" fill-opacity="0" stroke="#B3B3B3" stroke-width="12.961"/>
</g>
<g id="g4277">
<g id="rect4201-2">
<path d="M494.67,736.807 C494.67,736.807 457.853,730.474 444.965,731.092 C432.078,731.71 418.191,736.807 418.191,736.807 L418.191,830.181 C418.191,830.181 432.078,825.085 444.965,824.467 C457.853,823.849 494.67,830.181 494.67,830.181 z" fill="#FFFFFF"/>
<path d="M494.67,736.807 C494.67,736.807 457.853,730.474 444.965,731.092 C432.078,731.71 418.191,736.807 418.191,736.807 L418.191,830.181 C418.191,830.181 432.078,825.085 444.965,824.467 C457.853,823.849 494.67,830.181 494.67,830.181 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9.513" stroke-linecap="round"/>
</g>
<g id="rect4203-21">
<path d="M475.446,754.826 L438.849,754.826 L438.849,758.447 L475.446,758.447 z" fill="#FFFFFF"/>
<path d="M475.446,754.826 L438.849,754.826 L438.849,758.447 L475.446,758.447 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="rect4203-2-6">
<path d="M474.729,777.684 L438.133,777.684 L438.133,781.305 L474.729,781.305 z" fill="#FFFFFF"/>
<path d="M474.729,777.684 L438.133,777.684 L438.133,781.305 L474.729,781.305 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="rect4203-2-3-8">
<path d="M474.729,799.684 L438.133,799.684 L438.133,803.305 L474.729,803.305 z" fill="#FFFFFF"/>
<path d="M474.729,799.684 L438.133,799.684 L438.133,803.305 L474.729,803.305 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="path4245-5">
<path d="M497.863,837.649 C497.863,837.649 458.714,826.991 444.219,827.909 C429.724,828.827 413.566,837.649 413.566,837.649" fill="#B3B3B3"/>
<path d="M497.863,837.649 C497.863,837.649 458.714,826.991 444.219,827.909 C429.724,828.827 413.566,837.649 413.566,837.649" fill-opacity="0" stroke="#B3B3B3" stroke-width="13.003"/>
</g>
</g>
</g>
<g id="g3107">
<g id="rect3075">
<path d="M563.777,776.482 L610,730.259 L672.149,792.408 L625.926,838.631 z" fill="#B3B3B3"/>
<path d="M563.777,776.482 L610,730.259 L672.149,792.408 L625.926,838.631 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9.707" stroke-linecap="round"/>
</g>
<g id="rect3075-1">
<path d="M559.935,726.033 L607.36,726.566 L606.827,773.992 L559.402,773.459 z" fill="#B3B3B3"/>
<path d="M559.935,726.033 L607.36,726.566 L606.827,773.992 L559.402,773.459 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="6.074" stroke-linecap="round"/>
</g>
<g id="path3100">
<path d="M592.33,748.581 C595.261,751.513 595.261,756.266 592.33,759.198 C589.398,762.13 584.644,762.13 581.712,759.198 C578.78,756.266 578.78,751.513 581.712,748.581 C584.644,745.649 589.398,745.649 592.33,748.581 z" fill="#FFFFFF"/>
<path d="M592.33,748.581 C595.261,751.513 595.261,756.266 592.33,759.198 C589.398,762.13 584.644,762.13 581.712,759.198 C578.78,756.266 578.78,751.513 581.712,748.581 C584.644,745.649 589.398,745.649 592.33,748.581 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="9.535" stroke-linecap="round"/>
</g>
</g>
<g id="rect2995-0-2-7-7">
<path d="M631.179,718.824 L631.179,739.074 L612.367,739.074 L612.367,766.292 L631.179,766.292 L631.179,784.199 L658.367,784.199 L658.367,766.292 L677.742,766.292 L677.742,739.074 L658.367,739.074 L658.367,718.824 L631.179,718.824 z" fill="#B3B3B3"/>
<path d="M631.179,718.824 L631.179,739.074 L612.367,739.074 L612.367,766.292 L631.179,766.292 L631.179,784.199 L658.367,784.199 L658.367,766.292 L677.742,766.292 L677.742,739.074 L658.367,739.074 L658.367,718.824 L631.179,718.824 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="8.571"/>
</g>
<path d="M37.874,887.379 L150.697,887.379 L150.697,1024.488 L37.874,1024.488 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="6.166" id="rect3083"/>
<path d="M37.568,887.787 L151.004,887.787 L151.004,998.366 L37.568,998.366 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="5.553" id="rect3083-7"/>
<path d="M40.533,888.354 L60.182,888.354 L60.182,996.37 L40.533,996.37 z" fill="#B3B3B3" id="rect2995-0-4"/>
<path d="M69.033,901.926 L80.11,901.926 L80.11,913.513 L69.033,913.513 z" fill="#B3B3B3" id="rect2995-0-4-0"/>
<path d="M69.033,924.949 L80.11,924.949 L80.11,936.537 L69.033,936.537 z" fill="#B3B3B3" id="rect2995-0-4-0-9"/>
<path d="M69.033,947.973 L80.11,947.973 L80.11,959.561 L69.033,959.561 z" fill="#B3B3B3" id="rect2995-0-4-0-9-4"/>
<path d="M69.033,970.997 L80.11,970.997 L80.11,982.585 L69.033,982.585 z" fill="#B3B3B3" id="rect2995-0-4-0-9-4-8"/>
<path d="M58.747,1008.069 L86.967,1008.069 L86.967,1028.227 L58.747,1028.227 z" fill="#B3B3B3" id="rect2995-0-4-8"/>
<path d="M74.13,1027.791 L66.438,1034.293 L58.747,1040.796 L58.747,1027.791 L58.747,1014.786 L66.438,1021.288 z" fill="#B3B3B3" id="path4002"/>
<path d="M73.027,1027.791 L79.978,1034.293 L86.93,1040.796 L86.93,1027.791 L86.93,1014.786 L79.978,1021.288 z" fill="#B3B3B3" id="path4002-2"/>
<path d="M197.589,886.909 L310.411,886.909 L310.411,1024.017 L197.589,1024.017 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="6.166" id="rect3083-4"/>
<path d="M197.282,887.317 L310.718,887.317 L310.718,997.896 L197.282,997.896 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="5.553" id="rect3083-7-5"/>
<path d="M200.247,887.884 L219.896,887.884 L219.896,995.9 L200.247,995.9 z" fill="#B3B3B3" id="rect2995-0-4-5"/>
<path d="M218.461,1007.598 L246.682,1007.598 L246.682,1027.757 L218.461,1027.757 z" fill="#B3B3B3" id="rect2995-0-4-8-5"/>
<path d="M233.844,1027.321 L226.153,1033.824 L218.461,1040.326 L218.461,1027.321 L218.461,1014.316 L226.153,1020.819 z" fill="#B3B3B3" id="path4002-27"/>
<path d="M232.741,1027.321 L239.693,1033.824 L246.644,1040.326 L246.644,1027.321 L246.644,1014.316 L239.693,1020.819 z" fill="#B3B3B3" id="path4002-2-6"/>
<path d="M253.805,948.352 L273.454,948.352 L273.454,986.667 L253.805,986.667 z" fill="#B3B3B3" id="rect2995-0-4-5-7"/>
<path d="M228.066,900.37 L247.715,900.37 L247.715,933.129 L228.066,933.129 z" fill="#B3B3B3" id="rect2995-0-4-5-7-6"/>
<path d="M227.906,932.653 L241.436,918.405 L269.22,944.789 L255.69,959.037 z" fill="#B3B3B3" id="rect2995-0-4-5-7-8"/>
<path d="M278.632,900.37 L298.281,900.37 L298.281,933.129 L278.632,933.129 z" fill="#B3B3B3" id="rect2995-0-4-5-7-6-9"/>
<path d="M298.392,932.121 L285.456,918.405 L258.894,943.805 L271.829,957.522 z" fill="#B3B3B3" id="rect2995-0-4-5-7-8-2"/>
<g id="rect3083-7-5-7">
<path d="M362.125,942.66 L476.301,942.66 L476.301,1025.189 L362.125,1025.189 z" fill="#FFFFFF"/>
<path d="M362.125,942.66 L476.301,942.66 L476.301,1025.189 L362.125,1025.189 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="4.813"/>
</g>
<path d="M450.3,943.806 L475.505,943.806 L475.505,1025.558 L450.3,1025.558 z" fill="#B3B3B3" id="rect2995-0-4-5-9"/>
<path d="M376.811,954.433 L454.039,954.433 L454.039,964.969 L376.811,964.969 z" fill="#B3B3B3" id="rect2995-0-4-5-9-5"/>
<path d="M376.237,978.161 L453.464,978.161 L453.464,988.697 L376.237,988.697 z" fill="#B3B3B3" id="rect2995-0-4-5-9-5-4"/>
<path d="M376.237,1002.161 L453.464,1002.161 L453.464,1012.697 L376.237,1012.697 z" fill="#B3B3B3" id="rect2995-0-4-5-9-5-4-3"/>
<path d="M381.162,940.8 L381.162,910.941 C381.162,910.941 390.092,887.418 418.377,887.055 C445.32,886.708 454.992,909.954 454.992,909.954 L455.107,941.947" fill-opacity="0" stroke="#B3B3B3" stroke-width="15.226" id="path4310"/>
<g id="path2991-7-1-4">
<path d="M691.408,957.408 C691.408,995.423 660.59,1026.241 622.575,1026.241 C584.559,1026.241 553.742,995.423 553.742,957.408 C553.742,919.392 584.559,888.575 622.575,888.575 C660.59,888.575 691.408,919.392 691.408,957.408 z" fill="#B3B3B3"/>
<path d="M691.408,957.408 C691.408,995.423 660.59,1026.241 622.575,1026.241 C584.559,1026.241 553.742,995.423 553.742,957.408 C553.742,919.392 584.559,888.575 622.575,888.575 C660.59,888.575 691.408,919.392 691.408,957.408 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-5-8">
<path d="M680.685,957.898 C680.685,989.684 654.917,1015.451 623.132,1015.451 C591.346,1015.451 565.579,989.684 565.579,957.898 C565.579,926.113 591.346,900.346 623.132,900.346 C654.917,900.346 680.685,926.113 680.685,957.898 z" fill="#FFFFFF"/>
<path d="M680.685,957.898 C680.685,989.684 654.917,1015.451 623.132,1015.451 C591.346,1015.451 565.579,989.684 565.579,957.898 C565.579,926.113 591.346,900.346 623.132,900.346 C654.917,900.346 680.685,926.113 680.685,957.898 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<path d="M617.433,939.273 L631.101,939.273 L631.101,1004.878 L617.433,1004.878 z" fill="#B3B3B3" id="rect2995-0-2-8"/>
<g id="rect4046-3-4">
<path d="M595.942,931.209 L553.247,907.307 L560.31,944.242 L595.942,931.209 z" fill="#FFFFFF"/>
<path d="M595.942,931.209 L553.247,907.307 L560.31,944.242 L595.942,931.209 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="1.93" stroke-linecap="round"/>
</g>
<g id="rect4046-5">
<path d="M588.909,926.673 L560.094,910.545 L564.713,935.73 L588.909,926.673 z" fill="#B3B3B3"/>
<path d="M588.909,926.673 L560.094,910.545 L564.713,935.73 L588.909,926.673 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="1.313" stroke-linecap="round"/>
</g>
<g id="rect3075-11">
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill="#FFFFFF"/>
<path d="M1389.845,733.625 C1389.679,748.281 1389.512,762.937 1389.345,777.593 C1391.543,777.615 1393.741,777.635 1395.939,777.656 C1394.387,779.219 1392.835,780.781 1391.283,782.344 C1411.991,803.052 1432.7,823.76 1453.408,844.469 C1468.814,829.062 1484.221,813.656 1499.627,798.25 C1478.918,777.531 1458.21,756.812 1437.502,736.094 C1435.773,737.833 1434.043,739.573 1432.314,741.312 C1432.335,738.917 1432.356,736.521 1432.377,734.125 C1418.2,733.958 1404.023,733.792 1389.846,733.625 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="9" stroke-linecap="round"/>
</g>
<path d="M606.483,964.91 L606.483,951.243 L672.089,951.243 L672.089,964.91 z" fill="#B3B3B3" id="rect2995-0-2-8-6"/>
<g id="rect3075-11-7">
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill="#FFFFFF"/>
<path d="M1396.666,740.606 C1396.539,751.76 1396.412,762.914 1396.286,774.068 C1397.958,774.084 1399.631,774.099 1401.304,774.115 C1400.122,775.304 1398.941,776.494 1397.76,777.683 C1413.52,793.442 1429.28,809.202 1445.039,824.962 C1456.764,813.237 1468.489,801.513 1480.213,789.788 C1464.454,774.02 1448.694,758.253 1432.934,742.485 C1431.618,743.809 1430.302,745.133 1428.986,746.457 C1429.002,744.633 1429.017,742.81 1429.034,740.987 C1418.244,740.86 1407.455,740.733 1396.666,740.606 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
</g>
<g id="path3100-2">
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill="#FFFFFF"/>
<path d="M1424.031,752.219 C1428.538,756.726 1428.538,764.032 1424.031,768.539 C1419.525,773.045 1412.218,773.045 1407.712,768.539 C1403.205,764.032 1403.205,756.726 1407.712,752.219 C1412.218,747.713 1419.525,747.713 1424.031,752.219 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.585" stroke-linecap="round"/>
</g>
<g id="rect4114">
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill="#FFFFFF"/>
<path d="M1424.128,790.638 L1445.171,769.595 L1474.295,798.719 L1453.252,819.762 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="7.133"/>
</g>
<g id="path2991-7-6">
<path d="M969.889,84.636 C969.889,122.652 939.072,153.469 901.056,153.469 C863.04,153.469 832.223,122.652 832.223,84.636 C832.223,46.62 863.04,15.803 901.056,15.803 C939.072,15.803 969.889,46.62 969.889,84.636 z" fill="#A0A0A0"/>
<path d="M969.889,84.636 C969.889,122.652 939.072,153.469 901.056,153.469 C863.04,153.469 832.223,122.652 832.223,84.636 C832.223,46.62 863.04,15.803 901.056,15.803 C939.072,15.803 969.889,46.62 969.889,84.636 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-8">
<path d="M959.166,85.127 C959.166,116.912 933.399,142.679 901.613,142.679 C869.828,142.679 844.061,116.912 844.061,85.127 C844.061,53.341 869.828,27.574 901.613,27.574 C933.399,27.574 959.166,53.341 959.166,85.127 z" fill="#FFFFFF"/>
<path d="M959.166,85.127 C959.166,116.912 933.399,142.679 901.613,142.679 C869.828,142.679 844.061,116.912 844.061,85.127 C844.061,53.341 869.828,27.574 901.613,27.574 C933.399,27.574 959.166,53.341 959.166,85.127 z" fill-opacity="0" stroke="#808080" stroke-width="0.552"/>
</g>
<g id="rect2995-0-8">
<path d="M891.972,36.558 L911.239,36.558 L911.239,102.049 L891.972,102.049 z" fill="#A0A0A0"/>
<path d="M891.972,36.558 L911.239,36.558 L911.239,102.049 L891.972,102.049 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997-9-2">
<path d="M892.143,114.967 L911.762,114.967 L911.762,132.299 L892.143,132.299 z" fill="#A0A0A0"/>
<path d="M892.143,114.967 L911.762,114.967 L911.762,132.299 L892.143,132.299 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="g4284-1">
<g id="rect4201-26">
<path d="M829.877,204.061 C829.877,204.061 866.575,197.729 879.421,198.347 C892.267,198.964 906.109,204.061 906.109,204.061 L906.109,297.436 C906.109,297.436 892.267,292.339 879.421,291.721 C866.575,291.104 829.877,297.436 829.877,297.436 z" fill="#FFFFFF"/>
<path d="M829.877,204.061 C829.877,204.061 866.575,197.729 879.421,198.347 C892.267,198.964 906.109,204.061 906.109,204.061 L906.109,297.436 C906.109,297.436 892.267,292.339 879.421,291.721 C866.575,291.104 829.877,297.436 829.877,297.436 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="9.482" stroke-linecap="round"/>
</g>
<g id="rect4203-0">
<path d="M849.039,222.081 L885.517,222.081 L885.517,225.701 L849.039,225.701 z" fill="#FFFFFF"/>
<path d="M849.039,222.081 L885.517,222.081 L885.517,225.701 L849.039,225.701 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="rect4203-2-4">
<path d="M849.754,244.938 L886.232,244.938 L886.232,248.559 L849.754,248.559 z" fill="#FFFFFF"/>
<path d="M849.754,244.938 L886.232,244.938 L886.232,248.559 L849.754,248.559 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="rect4203-2-3-9">
<path d="M849.754,266.939 L886.232,266.939 L886.232,270.559 L849.754,270.559 z" fill="#FFFFFF"/>
<path d="M849.754,266.939 L886.232,266.939 L886.232,270.559 L849.754,270.559 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="path4245-4">
<path d="M826.695,304.903 C826.695,304.903 865.716,294.245 880.165,295.163 C894.613,296.081 910.719,304.903 910.719,304.903" fill="#B3B3B3"/>
<path d="M826.695,304.903 C826.695,304.903 865.716,294.245 880.165,295.163 C894.613,296.081 910.719,304.903 910.719,304.903" fill-opacity="0" stroke="#A0A0A0" stroke-width="12.961"/>
</g>
<g id="g4277-6">
<g id="rect4201-2-0">
<path d="M988.377,203.764 C988.377,203.764 951.56,197.432 938.672,198.05 C925.784,198.668 911.898,203.764 911.898,203.764 L911.898,297.139 C911.898,297.139 925.784,292.042 938.672,291.425 C951.56,290.807 988.377,297.139 988.377,297.139 z" fill="#FFFFFF"/>
<path d="M988.377,203.764 C988.377,203.764 951.56,197.432 938.672,198.05 C925.784,198.668 911.898,203.764 911.898,203.764 L911.898,297.139 C911.898,297.139 925.784,292.042 938.672,291.425 C951.56,290.807 988.377,297.139 988.377,297.139 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="9.513" stroke-linecap="round"/>
</g>
<g id="rect4203-21-3">
<path d="M969.152,221.784 L932.556,221.784 L932.556,225.405 L969.152,225.405 z" fill="#FFFFFF"/>
<path d="M969.152,221.784 L932.556,221.784 L932.556,225.405 L969.152,225.405 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="rect4203-2-6-6">
<path d="M968.436,244.641 L931.84,244.641 L931.84,248.262 L968.436,248.262 z" fill="#FFFFFF"/>
<path d="M968.436,244.641 L931.84,244.641 L931.84,248.262 L968.436,248.262 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="rect4203-2-3-8-2">
<path d="M968.436,266.642 L931.84,266.642 L931.84,270.263 L968.436,270.263 z" fill="#FFFFFF"/>
<path d="M968.436,266.642 L931.84,266.642 L931.84,270.263 L968.436,270.263 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="path4245-5-4">
<path d="M991.569,304.606 C991.569,304.606 952.421,293.949 937.926,294.866 C923.431,295.784 907.273,304.606 907.273,304.606" fill="#B3B3B3"/>
<path d="M991.569,304.606 C991.569,304.606 952.421,293.949 937.926,294.866 C923.431,295.784 907.273,304.606 907.273,304.606" fill-opacity="0" stroke="#A0A0A0" stroke-width="13.003"/>
</g>
</g>
</g>
<path d="M942.845,472.215 L942.845,387.348 C942.845,387.348 944.073,377.245 931.797,377.245 C919.521,377.245 898.039,377.245 898.039,377.245" fill-opacity="0" stroke="#A0A0A0" stroke-width="15" id="path3850-1-1"/>
<g id="rect3818-4-7">
<path d="M867.039,390.985 L869.187,390.985 L869.187,459.808 L867.039,459.808 z" fill="#FFFFFF"/>
<path d="M867.039,390.985 L869.187,390.985 L869.187,459.808 L867.039,459.808 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="15"/>
</g>
<g id="path3795-4-8-4">
<path d="M882.879,477.136 C882.879,485.014 876.429,491.4 868.473,491.4 C860.517,491.4 854.068,485.014 854.068,477.136 C854.068,469.259 860.517,462.872 868.473,462.872 C876.429,462.872 882.879,469.259 882.879,477.136 z" fill="#FFFFFF"/>
<path d="M882.879,477.136 C882.879,485.014 876.429,491.4 868.473,491.4 C860.517,491.4 854.068,485.014 854.068,477.136 C854.068,469.259 860.517,462.872 868.473,462.872 C876.429,462.872 882.879,469.259 882.879,477.136 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3795-8-0">
<path d="M881.865,377.916 C881.865,385.794 875.416,392.18 867.46,392.18 C859.504,392.18 853.054,385.794 853.054,377.916 C853.054,370.038 859.504,363.652 867.46,363.652 C875.416,363.652 881.865,370.038 881.865,377.916 z" fill="#FFFFFF"/>
<path d="M881.865,377.916 C881.865,385.794 875.416,392.18 867.46,392.18 C859.504,392.18 853.054,385.794 853.054,377.916 C853.054,370.038 859.504,363.652 867.46,363.652 C875.416,363.652 881.865,370.038 881.865,377.916 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3795-4-0-2-9">
<path d="M957.324,477.203 C957.324,485.081 950.875,491.467 942.919,491.467 C934.963,491.467 928.513,485.081 928.513,477.203 C928.513,469.326 934.963,462.939 942.919,462.939 C950.875,462.939 957.324,469.326 957.324,477.203 z" fill="#FFFFFF"/>
<path d="M957.324,477.203 C957.324,485.081 950.875,491.467 942.919,491.467 C934.963,491.467 928.513,485.081 928.513,477.203 C928.513,469.326 934.963,462.939 942.919,462.939 C950.875,462.939 957.324,469.326 957.324,477.203 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3852-4-4">
<path d="M916.255,352.994 L916.255,399.653 L886.165,378.443 z" fill="#A0A0A0"/>
<path d="M916.255,352.994 L916.255,399.653 L886.165,378.443 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="0.55"/>
</g>
<g id="rect3953">
<path d="M871.854,546.111 L878.997,553.254 L833.283,598.968 L826.14,591.825 z" fill="#A0A0A0"/>
<path d="M871.854,546.111 L878.997,553.254 L833.283,598.968 L826.14,591.825 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.148"/>
</g>
<g id="rect3953-8">
<path d="M877.987,632.232 L870.844,639.374 L825.13,593.66 L832.273,586.517 z" fill="#A0A0A0"/>
<path d="M877.987,632.232 L870.844,639.374 L825.13,593.66 L832.273,586.517 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.148"/>
</g>
<g id="rect3953-82">
<path d="M959.87,591.826 L952.728,598.968 L907.013,553.254 L914.156,546.111 z" fill="#A0A0A0"/>
<path d="M959.87,591.826 L952.728,598.968 L907.013,553.254 L914.156,546.111 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.148"/>
</g>
<g id="rect3953-82-4">
<path d="M910.116,641.395 L902.973,634.252 L948.687,588.538 L955.83,595.68 z" fill="#A0A0A0"/>
<path d="M910.116,641.395 L902.973,634.252 L948.687,588.538 L955.83,595.68 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.148"/>
</g>
<g id="g4016">
<g id="rect3953-82-4-1-4">
<path d="M901.66,795.087 L917.47,779.277 L966.852,828.659 L951.042,844.469 z" fill="#A0A0A0"/>
<path d="M901.66,795.087 L917.47,779.277 L966.852,828.659 L951.042,844.469 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="11.053"/>
</g>
<g id="rect3953-82-4-1-7-0">
<path d="M866.314,748.302 L870.684,743.931 L923.627,796.873 L919.256,801.244 z" fill="#A0A0A0"/>
<path d="M866.314,748.302 L870.684,743.931 L923.627,796.873 L919.256,801.244 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="6.017"/>
</g>
<g id="path3226">
<path d="M834.333,751.795 C824.112,741.574 824.591,724.521 835.405,713.708 C846.218,702.894 863.271,702.414 873.493,712.636 C883.714,722.858 883.235,739.911 872.421,750.724 C861.608,761.538 844.555,762.017 834.333,751.795 z" fill="#A0A0A0"/>
<path d="M834.333,751.795 C824.112,741.574 824.591,724.521 835.405,713.708 C846.218,702.894 863.271,702.414 873.493,712.636 C883.714,722.858 883.235,739.911 872.421,750.724 C861.608,761.538 844.555,762.017 834.333,751.795 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.536"/>
</g>
<path d="M830.328,738.658 C819.882,728.213 817.963,713.195 826.042,705.117 C834.121,697.038 849.138,698.957 859.584,709.402 C870.029,719.848 871.948,734.865 863.869,742.944 C855.791,751.023 840.774,749.104 830.328,738.658 z" fill="#FFFFFF" id="path3226-9"/>
</g>
<path d="M912.995,736.468 L947.28,770.754 L897.28,820.754 L862.995,786.468 z" fill="#FFFFFF" id="rect4027"/>
<g id="g4022">
<g id="rect3953-82-4-1">
<path d="M914.66,784.426 L898.85,768.616 L948.231,719.234 L964.042,735.045 z" fill="#A0A0A0"/>
<path d="M914.66,784.426 L898.85,768.616 L948.231,719.234 L964.042,735.045 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="11.053"/>
</g>
<g id="rect3953-82-4-1-7">
<path d="M862.174,826.216 L857.803,821.845 L910.746,768.902 L915.117,773.273 z" fill="#A0A0A0"/>
<path d="M862.174,826.216 L857.803,821.845 L910.746,768.902 L915.117,773.273 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="6.017"/>
</g>
<g id="rect3182">
<path d="M851.456,814.022 L836.452,848.251 L870.703,833.269 L872.227,829.8 L854.925,812.498 L851.456,814.022 z" fill="#A0A0A0"/>
<path d="M851.456,814.022 L836.452,848.251 L870.703,833.269 L872.227,829.8 L854.925,812.498 L851.456,814.022 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="4.636"/>
</g>
</g>
<g id="path2991-7-6-1">
<path d="M1228.488,89.805 C1228.488,127.82 1197.671,158.638 1159.655,158.638 C1121.64,158.638 1090.822,127.82 1090.822,89.805 C1090.822,51.789 1121.64,20.971 1159.655,20.971 C1197.671,20.971 1228.488,51.789 1228.488,89.805 z" fill="#3C3C3C"/>
<path d="M1228.488,89.805 C1228.488,127.82 1197.671,158.638 1159.655,158.638 C1121.64,158.638 1090.822,127.82 1090.822,89.805 C1090.822,51.789 1121.64,20.971 1159.655,20.971 C1197.671,20.971 1228.488,51.789 1228.488,89.805 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-8-7">
<path d="M1217.765,90.295 C1217.765,122.081 1191.998,147.848 1160.212,147.848 C1128.427,147.848 1102.66,122.081 1102.66,90.295 C1102.66,58.51 1128.427,32.742 1160.212,32.742 C1191.998,32.742 1217.765,58.51 1217.765,90.295 z" fill="#FFFFFF"/>
<path d="M1217.765,90.295 C1217.765,122.081 1191.998,147.848 1160.212,147.848 C1128.427,147.848 1102.66,122.081 1102.66,90.295 C1102.66,58.51 1128.427,32.742 1160.212,32.742 C1191.998,32.742 1217.765,58.51 1217.765,90.295 z" fill-opacity="0" stroke="#3C3C80" stroke-width="0.552"/>
</g>
<g id="rect2995-0-8-4">
<path d="M1150.571,41.726 L1169.838,41.726 L1169.838,107.217 L1150.571,107.217 z" fill="#3C3C3C"/>
<path d="M1150.571,41.726 L1169.838,41.726 L1169.838,107.217 L1150.571,107.217 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.733"/>
</g>
<g id="rect2997-9-2-0">
<path d="M1150.742,120.136 L1170.361,120.136 L1170.361,137.467 L1150.742,137.467 z" fill="#3C3C3C"/>
<path d="M1150.742,120.136 L1170.361,120.136 L1170.361,137.467 L1150.742,137.467 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.381"/>
</g>
<g id="g4284-1-9">
<g id="rect4201-26-4">
<path d="M1088.476,209.229 C1088.476,209.229 1125.174,202.897 1138.02,203.515 C1150.866,204.133 1164.708,209.229 1164.708,209.229 L1164.708,302.604 C1164.708,302.604 1150.866,297.508 1138.02,296.89 C1125.174,296.272 1088.476,302.604 1088.476,302.604 z" fill="#FFFFFF"/>
<path d="M1088.476,209.229 C1088.476,209.229 1125.174,202.897 1138.02,203.515 C1150.866,204.133 1164.708,209.229 1164.708,209.229 L1164.708,302.604 C1164.708,302.604 1150.866,297.508 1138.02,296.89 C1125.174,296.272 1088.476,302.604 1088.476,302.604 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="9.482" stroke-linecap="round"/>
</g>
<g id="rect4203-0-8">
<path d="M1107.638,227.249 L1144.116,227.249 L1144.116,230.87 L1107.638,230.87 z" fill="#FFFFFF"/>
<path d="M1107.638,227.249 L1144.116,227.249 L1144.116,230.87 L1107.638,230.87 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="rect4203-2-4-8">
<path d="M1108.353,250.107 L1144.831,250.107 L1144.831,253.728 L1108.353,253.728 z" fill="#FFFFFF"/>
<path d="M1108.353,250.107 L1144.831,250.107 L1144.831,253.728 L1108.353,253.728 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="rect4203-2-3-9-2">
<path d="M1108.353,272.107 L1144.831,272.107 L1144.831,275.728 L1108.353,275.728 z" fill="#FFFFFF"/>
<path d="M1108.353,272.107 L1144.831,272.107 L1144.831,275.728 L1108.353,275.728 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.807" stroke-linecap="round"/>
</g>
<g id="path4245-4-4">
<path d="M1085.294,310.072 C1085.294,310.072 1124.315,299.414 1138.764,300.332 C1153.212,301.25 1169.318,310.072 1169.318,310.072" fill="#B3B3B3"/>
<path d="M1085.294,310.072 C1085.294,310.072 1124.315,299.414 1138.764,300.332 C1153.212,301.25 1169.318,310.072 1169.318,310.072" fill-opacity="0" stroke="#3C3C3C" stroke-width="12.961"/>
</g>
<g id="g4277-6-5">
<g id="rect4201-2-0-5">
<path d="M1246.976,208.933 C1246.976,208.933 1210.159,202.601 1197.271,203.218 C1184.384,203.836 1170.497,208.933 1170.497,208.933 L1170.497,302.308 C1170.497,302.308 1184.384,297.211 1197.271,296.593 C1210.159,295.976 1246.976,302.308 1246.976,302.308 z" fill="#FFFFFF"/>
<path d="M1246.976,208.933 C1246.976,208.933 1210.159,202.601 1197.271,203.218 C1184.384,203.836 1170.497,208.933 1170.497,208.933 L1170.497,302.308 C1170.497,302.308 1184.384,297.211 1197.271,296.593 C1210.159,295.976 1246.976,302.308 1246.976,302.308 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="9.513" stroke-linecap="round"/>
</g>
<g id="rect4203-21-3-1">
<path d="M1227.751,226.953 L1191.155,226.953 L1191.155,230.573 L1227.751,230.573 z" fill="#FFFFFF"/>
<path d="M1227.751,226.953 L1191.155,226.953 L1191.155,230.573 L1227.751,230.573 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="rect4203-2-6-6-7">
<path d="M1227.035,249.81 L1190.439,249.81 L1190.439,253.431 L1227.035,253.431 z" fill="#FFFFFF"/>
<path d="M1227.035,249.81 L1190.439,249.81 L1190.439,253.431 L1227.035,253.431 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="rect4203-2-3-8-2-1">
<path d="M1227.035,271.811 L1190.439,271.811 L1190.439,275.431 L1227.035,275.431 z" fill="#FFFFFF"/>
<path d="M1227.035,271.811 L1190.439,271.811 L1190.439,275.431 L1227.035,275.431 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.833" stroke-linecap="round"/>
</g>
<g id="path4245-5-4-1">
<path d="M1250.169,309.775 C1250.169,309.775 1211.02,299.117 1196.525,300.035 C1182.03,300.953 1165.872,309.775 1165.872,309.775" fill="#B3B3B3"/>
<path d="M1250.169,309.775 C1250.169,309.775 1211.02,299.117 1196.525,300.035 C1182.03,300.953 1165.872,309.775 1165.872,309.775" fill-opacity="0" stroke="#3C3C3C" stroke-width="13.003"/>
</g>
</g>
</g>
<path d="M1201.444,477.383 L1201.444,392.516 C1201.444,392.516 1202.672,382.413 1190.396,382.413 C1178.12,382.413 1156.638,382.413 1156.638,382.413" fill-opacity="0" stroke="#3C3C3C" stroke-width="15" id="path3850-1-1-5"/>
<g id="rect3818-4-7-2">
<path d="M1125.638,396.153 L1127.786,396.153 L1127.786,464.977 L1125.638,464.977 z" fill="#FFFFFF"/>
<path d="M1125.638,396.153 L1127.786,396.153 L1127.786,464.977 L1125.638,464.977 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="15"/>
</g>
<g id="path3795-4-8-4-7">
<path d="M1141.478,482.305 C1141.478,490.183 1135.028,496.569 1127.072,496.569 C1119.116,496.569 1112.667,490.183 1112.667,482.305 C1112.667,474.427 1119.116,468.041 1127.072,468.041 C1135.028,468.041 1141.478,474.427 1141.478,482.305 z" fill="#FFFFFF"/>
<path d="M1141.478,482.305 C1141.478,490.183 1135.028,496.569 1127.072,496.569 C1119.116,496.569 1112.667,490.183 1112.667,482.305 C1112.667,474.427 1119.116,468.041 1127.072,468.041 C1135.028,468.041 1141.478,474.427 1141.478,482.305 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.989"/>
</g>
<g id="path3795-8-0-6">
<path d="M1140.464,383.085 C1140.464,390.962 1134.015,397.349 1126.059,397.349 C1118.103,397.349 1111.653,390.962 1111.653,383.085 C1111.653,375.207 1118.103,368.82 1126.059,368.82 C1134.015,368.82 1140.464,375.207 1140.464,383.085 z" fill="#FFFFFF"/>
<path d="M1140.464,383.085 C1140.464,390.962 1134.015,397.349 1126.059,397.349 C1118.103,397.349 1111.653,390.962 1111.653,383.085 C1111.653,375.207 1118.103,368.82 1126.059,368.82 C1134.015,368.82 1140.464,375.207 1140.464,383.085 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.989"/>
</g>
<g id="path3795-4-0-2-9-1">
<path d="M1215.923,482.372 C1215.923,490.25 1209.474,496.636 1201.518,496.636 C1193.562,496.636 1187.112,490.25 1187.112,482.372 C1187.112,474.494 1193.562,468.108 1201.518,468.108 C1209.474,468.108 1215.923,474.494 1215.923,482.372 z" fill="#FFFFFF"/>
<path d="M1215.923,482.372 C1215.923,490.25 1209.474,496.636 1201.518,496.636 C1193.562,496.636 1187.112,490.25 1187.112,482.372 C1187.112,474.494 1193.562,468.108 1201.518,468.108 C1209.474,468.108 1215.923,474.494 1215.923,482.372 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.989"/>
</g>
<g id="path3852-4-4-4">
<path d="M1174.854,358.162 L1174.854,404.822 L1144.764,383.612 z" fill="#3C3C3C"/>
<path d="M1174.854,358.162 L1174.854,404.822 L1144.764,383.612 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="0.55"/>
</g>
<g id="rect3953-2">
<path d="M1130.453,551.28 L1137.596,558.423 L1091.882,604.137 L1084.739,596.994 z" fill="#3C3C3C"/>
<path d="M1130.453,551.28 L1137.596,558.423 L1091.882,604.137 L1084.739,596.994 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.148"/>
</g>
<g id="rect3953-8-3">
<path d="M1136.586,637.4 L1129.443,644.543 L1083.729,598.829 L1090.872,591.686 z" fill="#3C3C3C"/>
<path d="M1136.586,637.4 L1129.443,644.543 L1083.729,598.829 L1090.872,591.686 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.148"/>
</g>
<g id="rect3953-82-2">
<path d="M1218.469,596.994 L1211.327,604.137 L1165.612,558.423 L1172.755,551.28 z" fill="#3C3C3C"/>
<path d="M1218.469,596.994 L1211.327,604.137 L1165.612,558.423 L1172.755,551.28 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.148"/>
</g>
<g id="rect3953-82-4-2">
<path d="M1168.715,646.563 L1161.572,639.42 L1207.286,593.706 L1214.429,600.849 z" fill="#3C3C3C"/>
<path d="M1168.715,646.563 L1161.572,639.42 L1207.286,593.706 L1214.429,600.849 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.148"/>
</g>
<g id="g4138">
<g id="rect3953-82-4-1-4-6">
<path d="M1160.259,800.256 L1176.069,784.446 L1225.451,833.827 L1209.641,849.638 z" fill="#3C3C3C"/>
<path d="M1160.259,800.256 L1176.069,784.446 L1225.451,833.827 L1209.641,849.638 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="11.053"/>
</g>
<g id="rect3953-82-4-1-7-0-8">
<path d="M1124.913,753.47 L1129.283,749.099 L1182.226,802.042 L1177.855,806.413 z" fill="#3C3C3C"/>
<path d="M1124.913,753.47 L1129.283,749.099 L1182.226,802.042 L1177.855,806.413 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="6.017"/>
</g>
<g id="path3226-5">
<path d="M1092.932,756.964 C1082.711,746.742 1083.19,729.69 1094.004,718.876 C1104.817,708.063 1121.87,707.583 1132.092,717.805 C1142.313,728.027 1141.834,745.079 1131.02,755.893 C1120.207,766.706 1103.154,767.186 1092.932,756.964 z" fill="#3C3C3C"/>
<path d="M1092.932,756.964 C1082.711,746.742 1083.19,729.69 1094.004,718.876 C1104.817,708.063 1121.87,707.583 1132.092,717.805 C1142.313,728.027 1141.834,745.079 1131.02,755.893 C1120.207,766.706 1103.154,767.186 1092.932,756.964 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="7.536"/>
</g>
<path d="M1088.927,743.827 C1078.481,733.381 1076.562,718.364 1084.641,710.285 C1092.72,702.206 1107.737,704.125 1118.183,714.571 C1128.628,725.017 1130.547,740.034 1122.469,748.113 C1114.39,756.191 1099.373,754.273 1088.927,743.827 z" fill="#FFFFFF" id="path3226-9-7"/>
<path d="M1170.308,742.065 L1204.594,776.351 L1154.594,826.351 L1120.308,792.065 z" fill="#FFFFFF" id="rect4027-6"/>
<g id="rect3953-82-4-1-8">
<path d="M1173.259,789.595 L1157.449,773.785 L1206.83,724.403 L1222.641,740.213 z" fill="#3C3C3C"/>
<path d="M1173.259,789.595 L1157.449,773.785 L1206.83,724.403 L1222.641,740.213 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="11.053"/>
</g>
<g id="rect3953-82-4-1-7-9">
<path d="M1120.773,831.384 L1116.402,827.013 L1169.345,774.071 L1173.716,778.442 z" fill="#3C3C3C"/>
<path d="M1120.773,831.384 L1116.402,827.013 L1169.345,774.071 L1173.716,778.442 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="6.017"/>
</g>
<g id="rect3182-2">
<path d="M1110.055,819.191 L1095.051,853.419 L1129.302,838.438 L1130.827,834.968 L1113.525,817.666 L1110.055,819.191 z" fill="#3C3C3C"/>
<path d="M1110.055,819.191 L1095.051,853.419 L1129.302,838.438 L1130.827,834.968 L1113.525,817.666 L1110.055,819.191 z" fill-opacity="0" stroke="#3C3C3C" stroke-width="4.636"/>
</g>
</g>
<g id="path2991-7-1-4-1">
<path d="M148.345,1166.648 C148.345,1204.663 117.528,1235.481 79.512,1235.481 C41.496,1235.481 10.679,1204.663 10.679,1166.648 C10.679,1128.632 41.496,1097.815 79.512,1097.815 C117.528,1097.815 148.345,1128.632 148.345,1166.648 z" fill="#BEBEFF"/>
<path d="M148.345,1166.648 C148.345,1204.663 117.528,1235.481 79.512,1235.481 C41.496,1235.481 10.679,1204.663 10.679,1166.648 C10.679,1128.632 41.496,1097.815 79.512,1097.815 C117.528,1097.815 148.345,1128.632 148.345,1166.648 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.66"/>
</g>
<g id="path2993-4-5-8-7">
<path d="M137.622,1167.138 C137.622,1198.924 111.855,1224.691 80.069,1224.691 C48.284,1224.691 22.516,1198.924 22.516,1167.138 C22.516,1135.353 48.284,1109.586 80.069,1109.586 C111.855,1109.586 137.622,1135.353 137.622,1167.138 z" fill="#FFFFFF"/>
<path d="M137.622,1167.138 C137.622,1198.924 111.855,1224.691 80.069,1224.691 C48.284,1224.691 22.516,1198.924 22.516,1167.138 C22.516,1135.353 48.284,1109.586 80.069,1109.586 C111.855,1109.586 137.622,1135.353 137.622,1167.138 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="0.552"/>
</g>
<path d="M74.371,1148.512 L88.038,1148.512 L88.038,1214.118 L74.371,1214.118 z" fill="#BEBEFA" id="rect2995-0-2-8-4"/>
<g id="rect4046-3-4-0">
<path d="M52.879,1140.449 L10.184,1116.547 L17.247,1153.482 L52.879,1140.449 z" fill="#FFFFFF"/>
<path d="M52.879,1140.449 L10.184,1116.547 L17.247,1153.482 L52.879,1140.449 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="1.93" stroke-linecap="round"/>
</g>
<g id="rect4046-5-9">
<path d="M45.846,1135.913 L17.031,1119.785 L21.65,1144.97 L45.846,1135.913 z" fill="#BEBEFF"/>
<path d="M45.846,1135.913 L17.031,1119.785 L21.65,1144.97 L45.846,1135.913 z" fill-opacity="0" stroke="#BEBEFF" stroke-width="1.313" stroke-linecap="round"/>
</g>
<g id="rect3075-11-4">
<path d="M236.5,1107.334 C236.333,1121.99 236.166,1136.646 236,1151.302 C238.198,1151.324 240.395,1151.344 242.593,1151.365 C241.041,1152.928 239.489,1154.49 237.937,1156.053 C258.646,1176.761 279.354,1197.469 300.062,1218.178 C315.468,1202.771 330.875,1187.365 346.281,1171.959 C325.573,1151.24 304.864,1130.521 284.156,1109.803 C282.427,1111.542 280.698,1113.282 278.968,1115.021 C278.99,1112.626 279.01,1110.23 279.031,1107.834 C264.854,1107.667 250.677,1107.501 236.5,1107.334 z" fill="#FFFFFF"/>
<path d="M236.5,1107.334 C236.333,1121.99 236.166,1136.646 236,1151.302 C238.198,1151.324 240.395,1151.344 242.593,1151.365 C241.041,1152.928 239.489,1154.49 237.937,1156.053 C258.646,1176.761 279.354,1197.469 300.062,1218.178 C315.468,1202.771 330.875,1187.365 346.281,1171.959 C325.573,1151.24 304.864,1130.521 284.156,1109.803 C282.427,1111.542 280.698,1113.282 278.968,1115.021 C278.99,1112.626 279.01,1110.23 279.031,1107.834 C264.854,1107.667 250.677,1107.501 236.5,1107.334 z" fill-opacity="0" stroke="#BEBEFA" stroke-width="9" stroke-linecap="round"/>
</g>
<path d="M63.42,1174.15 L63.42,1160.483 L129.026,1160.483 L129.026,1174.15 z" fill="#BEBEFA" id="rect2995-0-2-8-6-8"/>
<g id="rect3075-11-7-8">
<path d="M243.32,1114.315 C243.193,1125.469 243.067,1136.623 242.94,1147.777 C244.613,1147.793 246.285,1147.808 247.958,1147.824 C246.777,1149.013 245.595,1150.203 244.414,1151.392 C260.174,1167.151 275.934,1182.911 291.693,1198.671 C303.418,1186.946 315.143,1175.222 326.867,1163.497 C311.108,1147.729 295.348,1131.962 279.588,1116.194 C278.272,1117.518 276.956,1118.842 275.64,1120.166 C275.656,1118.342 275.671,1116.519 275.688,1114.696 C264.899,1114.569 254.109,1114.442 243.32,1114.315 z" fill="#FFFFFF"/>
<path d="M243.32,1114.315 C243.193,1125.469 243.067,1136.623 242.94,1147.777 C244.613,1147.793 246.285,1147.808 247.958,1147.824 C246.777,1149.013 245.595,1150.203 244.414,1151.392 C260.174,1167.151 275.934,1182.911 291.693,1198.671 C303.418,1186.946 315.143,1175.222 326.867,1163.497 C311.108,1147.729 295.348,1131.962 279.588,1116.194 C278.272,1117.518 276.956,1118.842 275.64,1120.166 C275.656,1118.342 275.671,1116.519 275.688,1114.696 C264.899,1114.569 254.109,1114.442 243.32,1114.315 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="6.849" stroke-linecap="round"/>
</g>
<g id="path3100-2-2">
<path d="M270.685,1125.928 C275.192,1130.435 275.192,1137.741 270.685,1142.248 C266.179,1146.754 258.872,1146.754 254.366,1142.248 C249.859,1137.741 249.859,1130.435 254.366,1125.928 C258.872,1121.422 266.179,1121.422 270.685,1125.928 z" fill="#FFFFFF"/>
<path d="M270.685,1125.928 C275.192,1130.435 275.192,1137.741 270.685,1142.248 C266.179,1146.754 258.872,1146.754 254.366,1142.248 C249.859,1137.741 249.859,1130.435 254.366,1125.928 C258.872,1121.422 266.179,1121.422 270.685,1125.928 z" fill-opacity="0" stroke="#BEBEFA" stroke-width="7.585" stroke-linecap="round"/>
</g>
<g id="rect4114-4">
<path d="M270.782,1164.347 L291.825,1143.305 L320.949,1172.429 L299.906,1193.471 z" fill="#FFFFFF"/>
<path d="M270.782,1164.347 L291.825,1143.305 L320.949,1172.429 L299.906,1193.471 z" fill-opacity="0" stroke="#BEBEFF" stroke-width="7.133"/>
</g>
<path d="M444.846,1211.217 C444.846,1211.217 447.118,1192.364 476.287,1187.796 C486.256,1186.234 508.495,1182.331 508.495,1159.69" fill-opacity="0" stroke="#BEBEFF" stroke-width="17.059" id="path3207-5"/>
<g id="rect3818-4-8-4-5">
<path d="M443.438,1135.819 L445.587,1135.819 L445.587,1204.643 L443.438,1204.643 z" fill="#FFFFFF"/>
<path d="M443.438,1135.819 L445.587,1135.819 L445.587,1204.643 L443.438,1204.643 z" fill-opacity="0" stroke="#BEBEFA" stroke-width="15"/>
</g>
<g id="path3795-4-8-7-8-1">
<path d="M459.278,1221.972 C459.278,1229.85 452.829,1236.236 444.873,1236.236 C436.917,1236.236 430.467,1229.85 430.467,1221.972 C430.467,1214.094 436.917,1207.708 444.873,1207.708 C452.829,1207.708 459.278,1214.094 459.278,1221.972 z" fill="#FFFFFF"/>
<path d="M459.278,1221.972 C459.278,1229.85 452.829,1236.236 444.873,1236.236 C436.917,1236.236 430.467,1229.85 430.467,1221.972 C430.467,1214.094 436.917,1207.708 444.873,1207.708 C452.829,1207.708 459.278,1214.094 459.278,1221.972 z" fill-opacity="0" stroke="#BEBEFF" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-7">
<path d="M458.265,1122.752 C458.265,1130.63 451.815,1137.016 443.859,1137.016 C435.903,1137.016 429.453,1130.63 429.453,1122.752 C429.453,1114.874 435.903,1108.488 443.859,1108.488 C451.815,1108.488 458.265,1114.874 458.265,1122.752 z" fill="#FFFFFF"/>
<path d="M458.265,1122.752 C458.265,1130.63 451.815,1137.016 443.859,1137.016 C435.903,1137.016 429.453,1130.63 429.453,1122.752 C429.453,1114.874 435.903,1108.488 443.859,1108.488 C451.815,1108.488 458.265,1114.874 458.265,1122.752 z" fill-opacity="0" stroke="#BEBEFF" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-2-1">
<path d="M524.832,1153.794 C524.832,1161.672 518.383,1168.058 510.427,1168.058 C502.471,1168.058 496.021,1161.672 496.021,1153.794 C496.021,1145.916 502.471,1139.53 510.427,1139.53 C518.383,1139.53 524.832,1145.916 524.832,1153.794 z" fill="#FFFFFF"/>
<path d="M524.832,1153.794 C524.832,1161.672 518.383,1168.058 510.427,1168.058 C502.471,1168.058 496.021,1161.672 496.021,1153.794 C496.021,1145.916 502.471,1139.53 510.427,1139.53 C518.383,1139.53 524.832,1145.916 524.832,1153.794 z" fill-opacity="0" stroke="#BEBEFF" stroke-width="7.989"/>
</g>
<g id="g3992">
<path d="M1704.368,51.875 L1720.533,68.041 L1646.34,142.233 L1630.175,126.068 z" fill="#3C3C3C" id="rect2995-0-8-4-1"/>
<path d="M1599.987,96.742 L1615.64,81.088 L1660.886,126.335 L1645.233,141.988 z" fill="#3C3C3C" id="rect2995-0-8-4-1-4"/>
</g>
<g id="g4112">
<path d="M1468.322,48.548 L1484.487,64.713 L1410.294,138.906 L1394.129,122.741 z" fill="#A0A0A0" id="rect2995-0-8-4-1-5"/>
<path d="M1363.94,93.415 L1379.593,77.761 L1424.84,123.008 L1409.187,138.661 z" fill="#A0A0A0" id="rect2995-0-8-4-1-4-5"/>
</g>
<path d="M1454.823,275.861 L1382.568,332.264 L1402.021,272.893 z" fill="#B3B3B3" id="path3894-1-1"/>
<g id="rect3088-5-5-7">
<path d="M1372.165,222.803 L1487.252,222.803 L1487.252,281.66 L1372.165,281.66 z" fill="#B3B3B3"/>
<path d="M1372.165,222.803 L1487.252,222.803 L1487.252,281.66 L1372.165,281.66 z" fill-opacity="0" stroke="#B3B3B3" stroke-width="32.985" stroke-linejoin="round"/>
</g>
<g id="rect4170">
<path d="M1405.281,364.617 L1428.986,364.617 L1428.986,500.829 L1405.281,500.829 z" fill="#DCDCDC"/>
<path d="M1405.281,364.617 L1428.986,364.617 L1428.986,500.829 L1405.281,500.829 z" fill-opacity="0" stroke="#DCDCDC" stroke-width="1.392" stroke-miterlimit="4.3"/>
</g>
<g id="rect4166">
<path d="M1355.731,391.55 L1470.966,391.55 L1470.966,442.009 L1355.731,442.009 z" fill="#DCDCDC"/>
<path d="M1355.731,391.55 L1470.966,391.55 L1470.966,442.009 L1355.731,442.009 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="9.98" stroke-miterlimit="4.3"/>
</g>
<g id="rect4174">
<path d="M1465.955,397.292 L1486.652,417.137 L1465.955,436.982 L1445.258,417.137 z" fill="#DCDCDC"/>
<path d="M1465.955,397.292 L1486.652,417.137 L1465.955,436.982 L1445.258,417.137 z" fill-opacity="0" stroke="#DCDCDC" stroke-width="0.942" stroke-miterlimit="4.3"/>
</g>
<g id="path4364">
<path d="M1426.457,416.302 C1426.457,421.48 1422.26,425.677 1417.082,425.677 C1411.904,425.677 1407.706,421.48 1407.706,416.302 C1407.706,411.124 1411.904,406.926 1417.082,406.926 C1422.26,406.926 1426.457,411.124 1426.457,416.302 z" fill="#FFFFE6"/>
<path d="M1426.457,416.302 C1426.457,421.48 1422.26,425.677 1417.082,425.677 C1411.904,425.677 1407.706,421.48 1407.706,416.302 C1407.706,411.124 1411.904,406.926 1417.082,406.926 C1422.26,406.926 1426.457,411.124 1426.457,416.302 z" fill-opacity="0" stroke="#FFFFFF" stroke-width="8.603" stroke-miterlimit="4.3"/>
</g>
<path d="M1684.796,276.068 L1612.54,332.47 L1631.994,273.099 z" fill="#DCDCDC" id="path3894-1-1-1"/>
<g id="rect3088-5-5-7-7">
<path d="M1602.137,223.01 L1717.224,223.01 L1717.224,281.866 L1602.137,281.866 z" fill="#DCDCDC"/>
<path d="M1602.137,223.01 L1717.224,223.01 L1717.224,281.866 L1602.137,281.866 z" fill-opacity="0" stroke="#DCDCDC" stroke-width="32.985" stroke-linejoin="round"/>
</g>
<g id="rect3220">
<path d="M42.774,1300.197 L111.779,1300.197 L111.779,1369.202 L42.774,1369.202 z" fill="#3C3C3C"/>
<path d="M42.774,1300.197 L111.779,1300.197 L111.779,1369.202 L42.774,1369.202 z" fill-opacity="0" stroke="#888888" stroke-width="48.237" stroke-linejoin="round"/>
</g>
<path d="M112.129,1315.562 L57.129,1370.562 L42.843,1356.276 L97.843,1301.276 z" fill="#FFFFFF" id="rect3998-1"/>
<path d="M56.4,1300.576 L111.4,1355.576 L97.114,1369.862 L42.114,1314.862 z" fill="#FFFFFF" id="rect3998-1-7"/>
<g id="rect3220-4">
<path d="M235.208,1299.692 L304.213,1299.692 L304.213,1368.697 L235.208,1368.697 z" fill="#0088CC"/>
<path d="M235.208,1299.692 L304.213,1299.692 L304.213,1368.697 L235.208,1368.697 z" fill-opacity="0" stroke="#0088CC" stroke-width="48.237" stroke-linejoin="round"/>
</g>
<path d="M304.563,1315.057 L249.563,1370.057 L235.277,1355.771 L290.277,1300.771 z" fill="#FFFFFF" id="rect3998-1-0"/>
<path d="M248.834,1300.071 L303.834,1355.071 L289.549,1369.357 L234.549,1314.357 z" fill="#FFFFFF" id="rect3998-1-7-9"/>
<g id="path3795-4-8-4">
<path d="M1412.392,641.966 C1412.392,650.212 1405.641,656.896 1397.313,656.896 C1388.985,656.896 1382.235,650.212 1382.235,641.966 C1382.235,633.72 1388.985,627.035 1397.313,627.035 C1405.641,627.035 1412.392,633.72 1412.392,641.966 z" fill="#A0A0A0"/>
<path d="M1412.392,641.966 C1412.392,650.212 1405.641,656.896 1397.313,656.896 C1388.985,656.896 1382.235,650.212 1382.235,641.966 C1382.235,633.72 1388.985,627.035 1397.313,627.035 C1405.641,627.035 1412.392,633.72 1412.392,641.966 z" fill="#A0A0A0"/>
</g>
<path d="M1396.792,592.168 C1426.908,592.168 1450.613,610.989 1450.613,639.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
<path d="M1397.792,545.653 C1453.613,544.493 1499.627,588.735 1499.627,636.54" fill-opacity="0" stroke="#A0A0A0" stroke-width="20" stroke-linecap="round"/>
<path d="M871.125,1039.025 C871.125,1039.025 873.794,1016.889 908.043,1011.524 C919.748,1009.691 945.861,1005.107 945.861,978.522" fill-opacity="0" stroke="#A0A0A0" stroke-width="17.059" id="path3207"/>
<g id="rect3818-4-8-4">
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
<g>
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill="#A0A0A0"/>
<path d="M869.474,950.497 L871.997,950.497 L871.997,1031.308 L869.474,1031.308 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="15"/>
</g>
</g>
<g id="path3795-4-8-7-8">
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill="#FFFFFF"/>
<path d="M888.074,1051.656 C888.074,1060.906 880.5,1068.405 871.159,1068.405 C861.817,1068.405 854.243,1060.906 854.243,1051.656 C854.243,1042.406 861.817,1034.908 871.159,1034.908 C880.5,1034.908 888.074,1042.406 888.074,1051.656 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8">
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill="#FFFFFF"/>
<path d="M886.883,935.155 C886.883,944.404 879.31,951.903 869.968,951.903 C860.626,951.903 853.054,944.404 853.054,935.155 C853.054,925.904 860.626,918.405 869.968,918.405 C879.31,918.405 886.883,925.904 886.883,935.155 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-2">
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill="#FFFFFF"/>
<path d="M965.046,971.602 C965.046,980.852 957.472,988.351 948.13,988.351 C938.789,988.351 931.215,980.852 931.215,971.602 C931.215,962.352 938.789,954.854 948.13,954.854 C957.472,954.854 965.046,962.352 965.046,971.602 z" fill-opacity="0" stroke="#A0A0A0" stroke-width="7.989"/>
</g>
<path d="M1114.353,1042.412 C1114.353,1042.412 1117.022,1020.275 1151.271,1014.91 C1162.976,1013.077 1189.089,1008.493 1189.089,981.909" fill-opacity="0" stroke="#000000" stroke-width="17.059" id="path3207"/>
<g id="rect3818-4-8-4">
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
<g>
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill="#A0A0A0"/>
<path d="M1112.701,953.884 L1115.225,953.884 L1115.225,1034.695 L1112.701,1034.695 z" fill-opacity="0" stroke="#000000" stroke-width="15"/>
</g>
</g>
<g id="path3795-4-8-7-8">
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill="#FFFFFF"/>
<path d="M1131.302,1055.043 C1131.302,1064.293 1123.728,1071.791 1114.386,1071.791 C1105.045,1071.791 1097.471,1064.293 1097.471,1055.043 C1097.471,1045.792 1105.045,1038.294 1114.386,1038.294 C1123.728,1038.294 1131.302,1045.792 1131.302,1055.043 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8">
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill="#FFFFFF"/>
<path d="M1130.111,938.542 C1130.111,947.791 1122.537,955.29 1113.196,955.29 C1103.854,955.29 1096.282,947.791 1096.282,938.542 C1096.282,929.291 1103.854,921.792 1113.196,921.792 C1122.537,921.792 1130.111,929.291 1130.111,938.542 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
</g>
<g id="path3795-8-4-8-2">
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill="#FFFFFF"/>
<path d="M1208.274,974.989 C1208.274,984.239 1200.7,991.738 1191.358,991.738 C1182.016,991.738 1174.443,984.239 1174.443,974.989 C1174.443,965.739 1182.016,958.241 1191.358,958.241 C1200.7,958.241 1208.274,965.739 1208.274,974.989 z" fill-opacity="0" stroke="#000000" stroke-width="7.989"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 82 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)

54
doc/release.md Normal file
View File

@@ -0,0 +1,54 @@
Release Operation
========
Update version number
--------
Note to update version number in files below:
### project/build.scala
```scala
object MyBuild extends Build {
val Organization = "gitbucket"
val Name = "gitbucket"
val Version = "3.3.0" // <---- update version!!
val ScalaVersion = "2.11.6"
val ScalatraVersion = "2.3.1"
```
### 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),
```
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

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 87 KiB

View File

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

View File

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

View File

@@ -1,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")
resolvers += "spray repo" at "http://repo.spray.io"
addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
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="0.0.1"/>
<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">
@@ -54,7 +55,12 @@
tofile="${target.dir}/scala-${scala.version}/gitbucket.war"/>
</target>
<target name="all" depends="rename">
<target name="checksum" depends="rename">
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="MD5" format="MD5SUM" forceOverwrite="yes" fileext=".md5"/>
<checksum file="${target.dir}/scala-${scala.version}/gitbucket.war" algorithm="SHA" format="MD5SUM" forceOverwrite="yes" fileext=".sha1"/>
</target>
<target name="all" depends="checksum">
</target>

15
release/deploy-assembly-jar.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
. ./env.sh
cd ../
./sbt.sh clean assembly
cd release
mvn deploy:deploy-file \
-DgroupId=gitbucket\
-DartifactId=gitbucket-assembly\
-Dversion=$GITBUCKET_VERSION\
-Dpackaging=jar\
-Dfile=../target/scala-2.11/gitbucket-assembly-$GITBUCKET_VERSION.jar\
-DrepositoryId=sourceforge.jp\
-Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/

3
release/env.sh Normal file
View File

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

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

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

17
release/pom.xml Normal file
View File

@@ -0,0 +1,17 @@
<?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>

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.1.jar" %*
java %JAVA_OPTS% -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar "%SCRIPT_DIR%\sbt-launch-0.13.8.jar" %*

3
sbt.sh
View File

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

View File

@@ -1,10 +1,8 @@
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.nio.SelectChannelConnector;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.IOException;
import java.io.File;
import java.net.URL;
import java.security.ProtectionDomain;
@@ -44,6 +42,14 @@ public class JettyLauncher {
server.addConnector(connector);
WebAppContext context = new WebAppContext();
File tmpDir = new File(getGitBucketHome(), "tmp");
if(tmpDir.exists()){
deleteDirectory(tmpDir);
}
tmpDir.mkdirs();
context.setTempDirectory(tmpDir);
ProtectionDomain domain = JettyLauncher.class.getProtectionDomain();
URL location = domain.getCodeSource().getLocation();
@@ -59,4 +65,27 @@ public class JettyLauncher {
server.start();
server.join();
}
private static File getGitBucketHome(){
String home = System.getProperty("gitbucket.home");
if(home != null && home.length() > 0){
return new File(home);
}
home = System.getenv("GITBUCKET_HOME");
if(home != null && home.length() > 0){
return new File(home);
}
return new File(System.getProperty("user.home"), ".gitbucket");
}
private static void deleteDirectory(File dir){
for(File file: dir.listFiles()){
if(file.isFile()){
file.delete();
} else if(file.isDirectory()){
deleteDirectory(file);
}
}
dir.delete();
}
}

View File

@@ -1,4 +1,4 @@
package util;
package gitbucket.core.util;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.diff.RawText;

View File

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

View File

@@ -128,7 +128,7 @@ INSERT INTO ACCOUNT (
'root@localhost',
'dc76e9f0c0006e8f919e0c515c66dbba3982f785',
true,
'https://github.com/takezoe/gitbucket',
'https://github.com/gitbucket/gitbucket',
SYSDATE,
SYSDATE,
NULL

View File

@@ -0,0 +1,6 @@
CREATE TABLE PLUGIN (
PLUGIN_ID VARCHAR(100) NOT NULL,
VERSION VARCHAR(100) NOT NULL
);
ALTER TABLE PLUGIN ADD CONSTRAINT IDX_PLUGIN_PK PRIMARY KEY (PLUGIN_ID);

View File

@@ -0,0 +1,18 @@
CREATE TABLE COMMIT_COMMENT (
USER_NAME VARCHAR(100) NOT NULL,
REPOSITORY_NAME VARCHAR(100) NOT NULL,
COMMIT_ID VARCHAR(100) NOT NULL,
COMMENT_ID INT AUTO_INCREMENT,
COMMENTED_USER_NAME VARCHAR(100) NOT NULL,
CONTENT TEXT NOT NULL,
FILE_NAME NVARCHAR(100),
OLD_LINE_NUMBER INT,
NEW_LINE_NUMBER INT,
REGISTERED_DATE TIMESTAMP NOT NULL,
UPDATED_DATE TIMESTAMP NOT NULL,
PULL_REQUEST BOOLEAN NOT NULL
);
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_PK PRIMARY KEY (COMMENT_ID);
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_FK0 FOREIGN KEY (USER_NAME, REPOSITORY_NAME) REFERENCES REPOSITORY (USER_NAME, REPOSITORY_NAME);
ALTER TABLE COMMIT_COMMENT ADD CONSTRAINT IDX_COMMIT_COMMENT_1 UNIQUE (USER_NAME, REPOSITORY_NAME, COMMIT_ID, COMMENT_ID);

View File

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

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,27 +1,38 @@
import _root_.servlet.{PluginActionInvokeFilter, BasicAuthenticationFilter, TransactionFilter}
import app._
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
import gitbucket.core.controller._
import gitbucket.core.plugin.PluginRegistry
import gitbucket.core.servlet.{AccessTokenAuthenticationFilter, BasicAuthenticationFilter, Database, TransactionFilter}
import gitbucket.core.util.Directory
import java.util.EnumSet
import javax.servlet._
import org.scalatra._
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
// Register TransactionFilter and BasicAuthenticationFilter at first
context.addFilter("transactionFilter", new TransactionFilter)
context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*")
context.addFilter("pluginActionInvokeFilter", new PluginActionInvokeFilter)
context.getFilterRegistration("pluginActionInvokeFilter").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, "/*")
PluginRegistry().getControllers.foreach { case (controller, path) =>
context.mount(controller, path)
}
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
context.mount(new DashboardController, "/*")
context.mount(new UserManagementController, "/*")
context.mount(new SystemSettingsController, "/*")
context.mount(new PluginsController, "/*")
context.mount(new AccountController, "/*")
context.mount(new RepositoryViewerController, "/*")
context.mount(new WikiController, "/*")
@@ -32,9 +43,13 @@ class ScalatraBootstrap extends LifeCycle {
context.mount(new RepositorySettingsController, "/*")
// Create GITBUCKET_HOME directory if it does not exist
val dir = new java.io.File(_root_.util.Directory.GitBucketHome)
val dir = new java.io.File(Directory.GitBucketHome)
if(!dir.exists){
dir.mkdirs()
}
}
}
override def destroy(context: ServletContext): Unit = {
Database.closeDataSource()
}
}

View File

@@ -1,110 +0,0 @@
package app
import service._
import util.{UsersAuthenticator, Keys}
import util.Implicits._
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator =>
get("/dashboard/issues/repos")(usersOnly {
searchIssues("all")
})
get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned")
})
get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by")
})
get("/dashboard/pulls")(usersOnly {
searchPullRequests("created_by", None)
})
get("/dashboard/pulls/owned")(usersOnly {
searchPullRequests("created_by", None)
})
get("/dashboard/pulls/public")(usersOnly {
searchPullRequests("not_created_by", None)
})
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
})
private def searchIssues(filter: String) = {
import IssuesService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardIssues,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
)
val userName = context.loginAccount.get.userName
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
dashboard.html.issues(
issues.html.listparts(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*),
condition),
countIssue(condition, Map.empty, false, userRepos: _*),
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
condition,
filter)
}
private def searchPullRequests(filter: String, repository: Option[String]) = {
import IssuesService._
import PullRequestService._
// condition
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
}.copy(repo = repository))
val userName = context.loginAccount.get.userName
val allRepos = getAllRepositories()
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*)
dashboard.html.pulls(
pulls.html.listparts(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*),
condition,
None,
false),
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
userRepos.map { case (userName, repoName) =>
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
}.sortBy(_._3).reverse,
condition,
filter)
}
}

View File

@@ -1,66 +0,0 @@
package app
import jp.sf.amateras.scalatra.forms._
import service._
import util.CollaboratorsAuthenticator
import util.Implicits._
import org.scalatra.i18n.Messages
class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String)
val newForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
val editForm = mapping(
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"editColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
})
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
})
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository)
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
})
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
})
/**
* Constraint for the identifier such as user name, repository name or page name.
*/
private def labelName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.contains(',')){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
}

View File

@@ -1,479 +0,0 @@
package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
import util.Directory._
import util.Implicits._
import util.ControlUtil._
import service._
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 service.IssuesService._
import service.PullRequestService._
import util.JGitUtil.DiffInfo
import util.JGitUtil.CommitInfo
import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with ActivityService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with ActivityService with PullRequestService with WebHookService with ReferrerAuthenticator with CollaboratorsAuthenticator =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))),
"requestRepositoryName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40)))
)(PullRequestForm.apply)
val mergeForm = mapping(
"message" -> trim(label("Message", text(required)))
)(MergeForm.apply)
case class PullRequestForm(
title: String,
content: Option[String],
targetUserName: String,
targetBranch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String)
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
searchPullRequests(None, repository)
})
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))){ git =>
val (commits, diffs) =
getRequestCompareInfo(owner, name, pullreq.commitIdFrom, owner, name, pullreq.commitIdTo)
pulls.html.pullreq(
issue, pullreq,
getComments(owner, name, issueId),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
}
}
} 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) =>
pulls.html.mergeguide(
checkConflictInPullRequest(owner, name, pullreq.branch, pullreq.requestUserName, name, pullreq.requestBranch, issueId),
pullreq,
s"${context.baseUrl}/git/${pullreq.requestUserName}/${pullreq.requestRepositoryName}.git")
}
} getOrElse NotFound
})
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
val name = repository.name
LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// 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()
val (commits, _) = getRequestCompareInfo(owner, name, pullreq.commitIdFrom,
pullreq.requestUserName, pullreq.requestRepositoryName, pullreq.commitIdTo)
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
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 _ =>
}
// notifications
Notifier().toNotify(repository, issueId, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
}
}
} getOrElse NotFound
})
get("/:owner/:repository/compare")(referrersOnly { forkedRepository =>
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(originUserName), Some(originRepositoryName)) => {
getRepository(originUserName, originRepositoryName, context.baseUrl).map { originRepository =>
using(
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
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/${originUserName}:${oldBranch}...${newBranch}")
}
} getOrElse NotFound
}
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}")
} getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val forkedId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
val oldId = oldGit.getRepository.resolve(forkedId)
val newId = newGit.getRepository.resolve(forkedBranch)
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
pulls.html.compare(
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
originBranch,
forkedBranch,
oldId.getName,
newId.getName,
forkedRepository,
originRepository,
forkedRepository,
hasWritePermission(forkedRepository.owner, forkedRepository.name, context.loginAccount))
}
}) getOrElse NotFound
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
pulls.html.mergecheck(
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch))
}
}) getOrElse NotFound
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = None,
milestoneId = None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
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
}
// record activity
recordPullRequestActivity(repository.owner, repository.name, loginUserName, issueId, form.title)
// notifications
Notifier().toNotify(repository, issueId, 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.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
} else {
(defaultOwner, value)
}
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
using(
Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
){ (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId)
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
}.toList.splitWith { (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
(commits, diffs)
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
pulls.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
userName,
page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
countIssue(condition, Map.empty, true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}

View File

@@ -1,442 +0,0 @@
package app
import _root_.util.JGitUtil.CommitInfo
import util.Directory._
import util.Implicits._
import _root_.util.ControlUtil._
import _root_.util._
import service._
import org.scalatra._
import java.io.File
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.revwalk.RevCommit
import service.WebHookService.WebHookPayload
class RepositoryViewerController extends RepositoryViewerControllerBase
with RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator
/**
* The repository viewer.
*/
trait RepositoryViewerControllerBase extends ControllerBase {
self: RepositoryService with AccountService with ActivityService with IssuesService with WebHookService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class EditorForm(
branch: String,
path: String,
content: String,
message: Option[String],
charset: String,
lineSeparator: String,
newFileName: String,
oldFileName: Option[String]
)
case class DeleteForm(
branch: String,
path: String,
message: Option[String],
fileName: String
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"lineSeparator" -> trim(label("Line Separator", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply)
val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))),
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
/**
* Returns converted HTML from Markdown for preview.
*/
post("/:owner/:repository/_preview")(referrersOnly { repository =>
contentType = "text/html"
view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean)
})
/**
* Displays the file list of the repository root and the default branch.
*/
get("/:owner/:repository")(referrersOnly {
fileList(_)
})
/**
* Displays the file list of the specified path and branch.
*/
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
if(path.isEmpty){
fileList(repository, id)
} else {
fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext)
case Left(_) => NotFound
}
}
})
get("/:owner/:repository/new/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
repo.html.editor(branch, repository, if(path.length == 0) Nil else path.split("/").toList,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
repo.html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
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}"))
redirect(s"/${repository.owner}/${repository.name}/blob/${form.branch}/${
if(form.path.length == 0) form.newFileName else s"${form.path}/${form.newFileName}"
}")
})
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)){
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}"
}")
})
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
/**
* Displays the file content of the specified branch or commit.
*/
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))
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
defining(JGitUtil.getContentFromId(git, objectId, false).get){ bytes =>
contentType = FileUtil.getContentType(path, bytes)
bytes
}
} else {
repo.html.blob(id, repository, path.split("/").toList, JGitUtil.getContentInfo(git, path, objectId),
new JGitUtil.CommitInfo(revCommit), hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
} getOrElse NotFound
}
})
/**
* Displays details of the specified commit.
*/
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id")
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))){ revCommit =>
JGitUtil.getDiffs(git, id) match { case (diffs, oldCommitId) =>
repo.html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
repository, diffs, oldCommitId)
}
}
}
})
/**
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// retrieve latest update date of each branch
val branchInfo = repository.branchList.map { branchName =>
val revCommit = git.log.add(git.getRepository.resolve(branchName)).setMaxCount(1).call.iterator.next
(branchName, revCommit.getCommitterIdent.getWhen)
}
repo.html.branches(branchInfo, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
}
})
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
redirect(s"/${repository.owner}/${repository.name}/branches")
})
/**
* Displays tags.
*/
get("/:owner/:repository/tags")(referrersOnly {
repo.html.tags(_)
})
/**
* Download repository contents as an archive.
*/
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
multiParams("splat").head match {
case name if name.endsWith(".zip") =>
archiveRepository(name, ".zip", repository)
case name if name.endsWith(".tar.gz") =>
archiveRepository(name, ".tar.gz", repository)
case _ => BadRequest
}
})
get("/:owner/:repository/network/members")(referrersOnly { repository =>
repo.html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
context.baseUrl),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
repository)
})
private def splitPath(repository: service.RepositoryService.RepositoryInfo, path: String): (String, String) = {
val id = repository.branchList.collectFirst {
case branch if(path == branch || path.startsWith(branch + "/")) => branch
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} getOrElse path.split("/")(0)
(id, path.substring(id.length).stripPrefix("/"))
}
private val readmeFiles = view.helpers.renderableSuffixes.map(suffix => s"readme${suffix}") ++ Seq("readme.txt", "readme")
/**
* Provides HTML of the file list.
*
* @param repository the repository information
* @param revstr the branch name or commit id(optional)
* @param path the directory path (optional)
* @return HTML of the file list
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){
repo.html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
//val revisions = Seq(if(revstr.isEmpty) repository.repository.defaultBranch else revstr, repository.branchList.head)
// get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
// get files
val files = JGitUtil.getFileList(git, revision, path)
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(revCommit), // latest commit
files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
} getOrElse NotFound
}
}
}
private def commitFile(repository: service.RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
LockUtil.lock(s"${repository.owner}/${repository.name}"){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(s"refs/heads/${branch}")
JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// close issue by commit message
closeIssuesFromMessage(message, loginAccount.userName, repository.owner, repository.name)
// call web hook
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 _ =>
}
}
}
}
private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
_getPathObjectId(path, treeWalk)
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): File = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val file = new File(workDir, repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
using(new java.io.FileOutputStream(file)) { out =>
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(out)
.call()
}
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${file.getName}")
file
}
}
}

View File

@@ -1,185 +0,0 @@
package app
import service.{AccountService, SystemSettingsService}
import SystemSettingsService._
import util.AdminAuthenticator
import util.Directory._
import util.ControlUtil._
import jp.sf.amateras.scalatra.forms._
import ssh.SshServer
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import plugin.{Plugin, PluginSystem}
import org.scalatra.Ok
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
self: AccountService with AdminAuthenticator =>
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())),
"ssh" -> trim(label("SSH access", boolean())),
"sshPort" -> trim(label("SSH port", optional(number()))),
"smtp" -> optionalIfNotChecked("notification", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
}
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly {
admin.html.system(flash.get("info"))
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(request.getServletContext,
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop()
}
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})
get("/admin/plugins")(adminOnly {
val installedPlugins = plugin.PluginSystem.plugins
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
admin.plugins.html.installed(installedPlugins, updatablePlugins)
})
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
deletePlugins(form.pluginIds)
installPlugins(form.pluginIds)
redirect("/admin/plugins")
})
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
deletePlugins(form.pluginIds)
redirect("/admin/plugins")
})
get("/admin/plugins/available")(adminOnly {
val installedPlugins = plugin.PluginSystem.plugins
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
admin.plugins.html.available(availablePlugins)
})
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
installPlugins(form.pluginIds)
redirect("/admin/plugins")
})
// get("/admin/plugins/console")(adminOnly {
// admin.plugins.html.console()
// })
//
// post("/admin/plugins/console")(adminOnly {
// val script = request.getParameter("script")
// val result = plugin.JavaScriptPlugin.evaluateJavaScript(script)
// Ok(result)
// })
// TODO Move these methods to PluginSystem or Service?
private def deletePlugins(pluginIds: List[String]): Unit = {
pluginIds.foreach { pluginId =>
plugin.PluginSystem.uninstall(pluginId)
val dir = new java.io.File(PluginHome, pluginId)
if(dir.exists && dir.isDirectory){
FileUtils.deleteQuietly(dir)
PluginSystem.uninstall(pluginId)
}
}
}
private def installPlugins(pluginIds: List[String]): Unit = {
val dir = getPluginCacheDir()
val installedPlugins = plugin.PluginSystem.plugins
getAvailablePlugins(installedPlugins).filter(x => pluginIds.contains(x.id)).foreach { plugin =>
val pluginDir = new java.io.File(PluginHome, plugin.id)
if(!pluginDir.exists){
FileUtils.copyDirectory(new java.io.File(dir, plugin.repository + "/" + plugin.id), pluginDir)
}
PluginSystem.installPlugin(plugin.id)
}
}
private def getAvailablePlugins(installedPlugins: List[Plugin]): List[SystemSettingsControllerBase.AvailablePlugin] = {
val repositoryRoot = getPluginCacheDir()
if(repositoryRoot.exists && repositoryRoot.isDirectory){
PluginSystem.repositories.flatMap { repo =>
val repoDir = new java.io.File(repositoryRoot, repo.id)
if(repoDir.exists && repoDir.isDirectory){
repoDir.listFiles.filter(d => d.isDirectory && !d.getName.startsWith(".")).map { plugin =>
val propertyFile = new java.io.File(plugin, "plugin.properties")
val properties = new java.util.Properties()
if(propertyFile.exists && propertyFile.isFile){
using(new FileInputStream(propertyFile)){ in =>
properties.load(in)
}
}
SystemSettingsControllerBase.AvailablePlugin(
repository = repo.id,
id = properties.getProperty("id"),
version = properties.getProperty("version"),
author = properties.getProperty("author"),
url = properties.getProperty("url"),
description = properties.getProperty("description"),
status = installedPlugins.find(_.id == properties.getProperty("id")) match {
case Some(x) if(PluginSystem.isUpdatable(x.version, properties.getProperty("version")))=> "updatable"
case Some(x) => "installed"
case None => "available"
})
}
} else Nil
}
} else Nil
}
}
object SystemSettingsControllerBase {
case class AvailablePlugin(repository: String, id: String, version: String,
author: String, url: String, description: String, status: String)
}

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, isPullRequest: Boolean){
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "issues" }}/${issueId}#comment-${id}")
}
object ApiComment{
def apply(comment: IssueComment, repositoryName: RepositoryName, issueId: Int, user: ApiUser, isPullRequest: Boolean): ApiComment =
ApiComment(
id = comment.commentId,
user = user,
body = comment.content,
created_at = comment.registeredDate,
updated_at = comment.updatedDate)(repositoryName, issueId, isPullRequest)
}

View File

@@ -0,0 +1,57 @@
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, urlIsHtmlUrl: Boolean) extends FieldSerializable{
val url = if(urlIsHtmlUrl){
ApiPath(s"/${repositoryName.fullName}/commit/${id}")
}else{
ApiPath(s"/api/v3/${repositoryName.fullName}/commits/${id}")
}
val html_url = if(urlIsHtmlUrl){
None
}else{
Some(ApiPath(s"/${repositoryName.fullName}/commit/${id}"))
}
}
object ApiCommit{
def apply(git: Git, repositoryName: RepositoryName, commit: CommitInfo, urlIsHtmlUrl: Boolean = false): 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, urlIsHtmlUrl)
}
def forPushPayload(git: Git, repositoryName: RepositoryName, commit: CommitInfo): ApiCommit = apply(git, repositoryName, commit, true)
}

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, isPullRequest: Boolean){
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if(isPullRequest){ "pull" }else{ "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, issue.isPullRequest)
}

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,56 @@
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)(urlIsHtmlUrl: Boolean) {
val forks_count = forks
val watchers_count = watchers
val url = if(urlIsHtmlUrl){
ApiPath(s"/${full_name}")
}else{
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,
urlIsHtmlUrl: Boolean = false): 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
)(urlIsHtmlUrl)
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))
def forPushPayload(repositoryInfo: RepositoryInfo, owner: ApiUser): ApiRepository =
ApiRepository(repositoryInfo.repository, owner, forkedCount=repositoryInfo.forkedCount, urlIsHtmlUrl=true)
}

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,19 @@
package gitbucket.core.api
/**
* https://developer.github.com/v3/repos/#create
* api form
*/
case class CreateARepository(
name: String,
description: Option[String],
`private`: Boolean = false,
auto_init: Boolean = false
) {
def isValid: Boolean = {
name.length<=40 &&
name.matches("[a-zA-Z0-9\\-\\+_.]+") &&
!name.startsWith("_") &&
!name.startsWith("-")
}
}

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,4 @@
package gitbucket.core.api
/** export fields for json */
trait FieldSerializable

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[FieldSerializable]() + 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,27 +1,35 @@
package app
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.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 service._
import util._
import util.StringUtil._
import util.Directory._
import util.ControlUtil._
import util.Implicits._
import ssh.SshUtil
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 model.GroupMember
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])
@@ -31,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)))),
@@ -54,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)
@@ -77,7 +91,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
val newRepositoryForm = mapping(
"owner" -> trim(label("Owner" , text(required, maxlength(40), identifier, existsAccount))),
"name" -> trim(label("Repository name", text(required, maxlength(40), identifier, uniqueRepository))),
"name" -> trim(label("Repository name", text(required, maxlength(40), repository, uniqueRepository))),
"description" -> trim(label("Description" , optional(text()))),
"isPrivate" -> trim(label("Repository Type", boolean())),
"createReadme" -> trim(label("Create README" , boolean()))
@@ -88,6 +102,12 @@ trait AccountControllerBase extends AccountManagementControllerBase {
"name" -> trim(label("Repository name", text(required)))
)(ForkRepositoryForm.apply)
case class AccountForm(accountName: String)
val accountForm = mapping(
"account" -> trim(label("Group/User name", text(required, validAccountName)))
)(AccountForm.apply)
/**
* Displays user information.
*/
@@ -97,21 +117,21 @@ trait AccountControllerBase extends AccountManagementControllerBase {
params.getOrElse("tab", "repositories") match {
// Public Activity
case "activity" =>
_root_.account.html.activity(account,
gitbucket.core.account.html.activity(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getActivitiesByUser(userName, true))
// Members
case "members" if(account.isGroupAccount) => {
val members = getGroupMembers(account.userName)
_root_.account.html.members(account, members.map(_.userName),
gitbucket.core.account.html.members(account, members.map(_.userName),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
}
// Repositories
case _ => {
val members = getGroupMembers(account.userName)
_root_.account.html.repositories(account,
gitbucket.core.account.html.repositories(account,
if(account.isGroupAccount) Nil else getGroupsByUserName(userName),
getVisibleRepositories(context.loginAccount, context.baseUrl, Some(userName)),
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager }))
@@ -129,18 +149,36 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_avatar"){
val userName = params("userName")
getAccountByUserName(userName).flatMap(_.image).map { image =>
contentType = FileUtil.getMimeType(image)
new java.io.File(getUserUploadDir(userName), image)
RawData(FileUtil.getMimeType(image), new java.io.File(getUserUploadDir(userName), image))
} getOrElse {
contentType = "image/png"
Thread.currentThread.getContextClassLoader.getResourceAsStream("noimage.png")
}
}
/**
* 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 =>
account.html.edit(x, flash.get("info"))
html.edit(x, flash.get("info"))
} getOrElse NotFound
})
@@ -164,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))
}
@@ -184,7 +222,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:userName/_ssh")(oneselfOnly {
val userName = params("userName")
getAccountByUserName(userName).map { x =>
account.html.ssh(x, getPublicKeys(x.userName))
html.ssh(x, getPublicKeys(x.userName))
} getOrElse NotFound
})
@@ -201,12 +239,46 @@ 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){
redirect("/")
} else {
account.html.register()
html.register()
}
} else NotFound
}
@@ -220,7 +292,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
get("/groups/new")(usersOnly {
account.html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
html.group(None, List(GroupMember("", context.loginAccount.get.userName, true)))
})
post("/groups/new", newGroupForm)(usersOnly { form =>
@@ -236,7 +308,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
get("/:groupName/_editgroup")(managersOnly {
defining(params("groupName")){ groupName =>
account.html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
html.group(getAccountByUserName(groupName, true), getGroupMembers(groupName))
}
})
@@ -285,7 +357,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
* Show the new repository form.
*/
get("/new")(usersOnly {
account.html.newrepo(getGroupsByUserName(context.loginAccount.get.userName))
html.newrepo(getGroupsByUserName(context.loginAccount.get.userName), context.settings.isCreateRepoOptionPublic)
})
/**
@@ -294,56 +366,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
post("/new", newRepositoryForm)(usersOnly { form =>
LockUtil.lock(s"${form.owner}/${form.name}"){
if(getRepository(form.owner, form.name, context.baseUrl).isEmpty){
val ownerAccount = getAccountByUserName(form.owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(form.name, form.owner, form.description, form.isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(form.owner).foreach { member =>
addCollaborator(form.owner, form.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(form.owner, form.name)
// Create the actual repository
val gitdir = getRepositoryDir(form.owner, form.name)
JGitUtil.initRepository(gitdir)
if(form.createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(form.description.nonEmpty){
form.name + "\n" +
"===============\n" +
"\n" +
form.description.get
} else {
form.name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, form.owner, form.name)
// Record activity
recordCreateRepositoryActivity(form.owner, form.name, loginUserName)
createRepository(form.owner, form.name, form.description, form.isPrivate, form.createReadme)
}
// redirect to the repository
@@ -351,14 +374,82 @@ trait AccountControllerBase extends AccountManagementControllerBase {
}
})
/**
* Create user repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/user/repos")(usersOnly {
val owner = context.loginAccount.get.userName
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${owner}/${data.name}") {
if(getRepository(owner, data.name, context.baseUrl).isEmpty){
createRepository(owner, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(owner, data.name, context.baseUrl).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(owner).get)))
} else {
ApiError(
"A repository with this name already exists on this account",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
/**
* Create group repository
* https://developer.github.com/v3/repos/#create
*/
post("/api/v3/orgs/:org/repos")(managersOnly {
val groupName = params("org")
(for {
data <- extractFromJsonBody[CreateARepository] if data.isValid
} yield {
LockUtil.lock(s"${groupName}/${data.name}") {
if(getRepository(groupName, data.name, context.baseUrl).isEmpty){
createRepository(groupName, data.name, data.description, data.`private`, data.auto_init)
val repository = getRepository(groupName, data.name, context.baseUrl).get
JsonFormat(ApiRepository(repository, ApiUser(getAccountByUserName(groupName).get)))
} else {
ApiError(
"A repository with this name already exists for this group",
Some("https://developer.github.com/v3/repos/#create")
)
}
}
}) getOrElse NotFound
})
get("/:owner/:repository/fork")(readableUsersOnly { repository =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val groups = getGroupsByUserName(loginUserName)
groups match {
case _: List[String] =>
val managerPermissions = groups.map { group =>
val members = getGroupMembers(group)
context.loginAccount.exists(x => members.exists { member => member.userName == x.userName && member.isManager })
}
helper.html.forkrepository(
repository,
(groups zip managerPermissions).toMap
)
case _ => redirect(s"/${loginUserName}")
}
})
LockUtil.lock(s"${loginUserName}/${repository.name}"){
if(repository.owner == loginUserName || getRepository(loginAccount.userName, repository.name, baseUrl).isDefined){
post("/:owner/:repository/fork", accountForm)(readableUsersOnly { (form, repository) =>
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
val accountName = form.accountName
LockUtil.lock(s"${accountName}/${repository.name}"){
if(getRepository(accountName, repository.name, baseUrl).isDefined ||
(accountName != loginUserName && !getGroupsByUserName(loginUserName).contains(accountName))){
// redirect to the repository if repository already exists
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
} else {
// Insert to the database at first
val originUserName = repository.repository.originUserName.getOrElse(repository.owner)
@@ -366,7 +457,7 @@ trait AccountControllerBase extends AccountManagementControllerBase {
createRepository(
repositoryName = repository.name,
userName = loginUserName,
userName = accountName,
description = repository.repository.description,
isPrivate = repository.repository.isPrivate,
originRepositoryName = Some(originRepositoryName),
@@ -375,27 +466,88 @@ trait AccountControllerBase extends AccountManagementControllerBase {
parentUserName = Some(repository.owner)
)
// Add collaborators for group repository
val ownerAccount = getAccountByUserName(accountName).get
if(ownerAccount.isGroupAccount){
getGroupMembers(accountName).foreach { member =>
addCollaborator(accountName, repository.name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(loginUserName, repository.name)
insertDefaultLabels(accountName, repository.name)
// clone repository actually
JGitUtil.cloneRepository(
getRepositoryDir(repository.owner, repository.name),
getRepositoryDir(loginUserName, repository.name))
getRepositoryDir(accountName, repository.name))
// Create Wiki repository
JGitUtil.cloneRepository(
getWikiRepositoryDir(repository.owner, repository.name),
getWikiRepositoryDir(loginUserName, repository.name))
getWikiRepositoryDir(accountName, repository.name))
// Record activity
recordForkActivity(repository.owner, repository.name, loginUserName)
recordForkActivity(repository.owner, repository.name, loginUserName, accountName)
// redirect to the repository
redirect(s"/${loginUserName}/${repository.name}")
redirect(s"/${accountName}/${repository.name}")
}
}
})
private def createRepository(owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) {
val ownerAccount = getAccountByUserName(owner).get
val loginAccount = context.loginAccount.get
val loginUserName = loginAccount.userName
// Insert to the database at first
createRepository(name, owner, description, isPrivate)
// Add collaborators for group repository
if(ownerAccount.isGroupAccount){
getGroupMembers(owner).foreach { member =>
addCollaborator(owner, name, member.userName)
}
}
// Insert default labels
insertDefaultLabels(owner, name)
// Create the actual repository
val gitdir = getRepositoryDir(owner, name)
JGitUtil.initRepository(gitdir)
if(createReadme){
using(Git.open(gitdir)){ git =>
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headId = git.getRepository.resolve(Constants.HEAD + "^{commit}")
val content = if(description.nonEmpty){
name + "\n" +
"===============\n" +
"\n" +
description.get
} else {
name + "\n" +
"===============\n"
}
builder.add(JGitUtil.createDirCacheEntry("README.md", FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))))
builder.finish()
JGitUtil.createNewCommit(git, inserter, headId, builder.getDirCache.writeTree(inserter),
Constants.HEAD, loginAccount.fullName, loginAccount.mailAddress, "Initial commit")
}
}
// Create Wiki repository
createWikiRepository(loginAccount, owner, name)
// Record activity
recordCreateRepositoryActivity(owner, name, loginUserName)
}
private def insertDefaultLabels(userName: String, repositoryName: String): Unit = {
createLabel(userName, repositoryName, "bug", "fc2929")
createLabel(userName, repositoryName, "duplicate", "cccccc")
@@ -431,4 +583,13 @@ trait AccountControllerBase extends AccountManagementControllerBase {
case None => Some("Key is invalid.")
}
}
private def validAccountName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] = {
getAccountByUserName(value) match {
case Some(_) => None
case None => Some("Invalid Group/User Account.")
}
}
}
}

View File

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

View File

@@ -1,19 +1,25 @@
package app
package gitbucket.core.controller
import gitbucket.core.api.ApiError
import gitbucket.core.model.Account
import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import gitbucket.core.util.Implicits._
import gitbucket.core.util._
import _root_.util.Directory._
import _root_.util.Implicits._
import _root_.util.ControlUtil._
import _root_.util.{StringUtil, FileUtil, Validations, Keys}
import org.scalatra._
import org.scalatra.json._
import org.json4s._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import model._
import service.{SystemSettingsService, AccountService}
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,13 +112,19 @@ 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(html.error("Not Found"))
org.scalatra.NotFound(gitbucket.core.html.error("Not Found"))
}
protected def Unauthorized()(implicit context: app.Context) =
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("/"))
@@ -134,6 +149,27 @@ abstract class ControllerBase extends ScalatraFilter
if (path.startsWith("http")) path
else baseUrl + super.url(path, params, false, false, false)
/**
* Use this method to response the raw data against XSS.
*/
protected def RawData[T](contentType: String, rawData: T): T = {
if(contentType.split(";").head.trim.toLowerCase.startsWith("text/html")){
this.contentType = "text/plain"
} else {
this.contentType = contentType
}
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)
}
}
/**
@@ -145,6 +181,13 @@ case class Context(settings: SystemSettingsService.SystemSettings, loginAccount:
val currentPath = request.getRequestURI.substring(request.getContextPath.length)
val baseUrl = settings.baseUrl(request)
val host = new java.net.URL(baseUrl).getHost
val platform = request.getHeader("User-Agent") match {
case null => null
case agent if agent.contains("Mac") => "mac"
case agent if agent.contains("Linux") => "linux"
case agent if agent.contains("Win") => "windows"
case _ => null
}
/**
* Get object from cache.

View File

@@ -0,0 +1,138 @@
package gitbucket.core.controller
import gitbucket.core.dashboard.html
import gitbucket.core.service.{RepositoryService, PullRequestService, AccountService, IssuesService}
import gitbucket.core.util.{StringUtil, Keys, UsersAuthenticator}
import gitbucket.core.util.Implicits._
import gitbucket.core.service.IssuesService._
class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase {
self: IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator =>
get("/dashboard/issues")(usersOnly {
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
case _ => searchIssues("created_by")
}
} getOrElse {
searchIssues("created_by")
}
})
get("/dashboard/issues/assigned")(usersOnly {
searchIssues("assigned")
})
get("/dashboard/issues/created_by")(usersOnly {
searchIssues("created_by")
})
get("/dashboard/issues/mentioned")(usersOnly {
searchIssues("mentioned")
})
get("/dashboard/pulls")(usersOnly {
val q = request.getParameter("q")
val account = context.loginAccount.get
Option(q).map { q =>
val condition = IssueSearchCondition(q, Map[String, Int]())
q match {
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
case _ => searchPullRequests("created_by")
}
} getOrElse {
searchPullRequests("created_by")
}
})
get("/dashboard/pulls/created_by")(usersOnly {
searchPullRequests("created_by")
})
get("/dashboard/pulls/assigned")(usersOnly {
searchPullRequests("assigned")
})
get("/dashboard/pulls/mentioned")(usersOnly {
searchPullRequests("mentioned")
})
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
val condition = session.putAndGet(key, if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, Map[String, Int]())
}
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
filter match {
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
}
}
private def searchIssues(filter: String) = {
import IssuesService._
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val page = IssueSearchCondition.page(request)
html.issues(
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page,
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter,
getGroupNames(userName))
}
private def searchPullRequests(filter: String) = {
import IssuesService._
import PullRequestService._
val userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName)
val page = IssueSearchCondition.page(request)
html.pulls(
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page,
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
filter match {
case "assigned" => condition.copy(assigned = Some(userName))
case "mentioned" => condition.copy(mentioned = Some(userName))
case _ => condition.copy(author = Some(userName))
},
filter,
getGroupNames(userName))
}
}

View File

@@ -1,8 +1,8 @@
package app
package gitbucket.core.controller
import util.{Keys, FileUtil}
import util.ControlUtil._
import util.Directory._
import gitbucket.core.util.{Keys, FileUtil}
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Directory._
import org.scalatra._
import org.scalatra.servlet.{MultipartConfig, FileUploadSupport, FileItem}
import org.apache.commons.io.FileUtils
@@ -17,22 +17,22 @@ class FileUploadController extends ScalatraServlet with FileUploadSupport {
configureMultipartHandling(MultipartConfig(maxFileSize = Some(3 * 1024 * 1024)))
post("/image"){
execute { (file, fileId) =>
execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(getTemporaryDir(session.getId), fileId), file.get)
session += Keys.Session.Upload(fileId) -> file.name
}
}, FileUtil.isImage)
}
post("/image/:owner/:repository"){
execute { (file, fileId) =>
post("/file/:owner/:repository"){
execute({ (file, fileId) =>
FileUtils.writeByteArrayToFile(new java.io.File(
getAttachedDir(params("owner"), params("repository")),
fileId + "." + FileUtil.getExtension(file.getName)), file.get)
}
}, FileUtil.isUploadableType)
}
private def execute(f: (FileItem, String) => Unit) = fileParams.get("file") match {
case Some(file) if(FileUtil.isImage(file.name)) =>
private def execute(f: (FileItem, String) => Unit, mimeTypeChcker: (String) => Boolean) = fileParams.get("file") match {
case Some(file) if(mimeTypeChcker(file.name)) =>
defining(FileUtil.generateFileId){ fileId =>
f(file, fileId)

View File

@@ -1,13 +1,20 @@
package app
package gitbucket.core.controller
import gitbucket.core.api._
import gitbucket.core.helper.xml
import gitbucket.core.html
import gitbucket.core.model.Account
import gitbucket.core.service.{RepositoryService, ActivityService, AccountService}
import gitbucket.core.util.Implicits._
import gitbucket.core.util.{LDAPUtil, Keys, UsersAuthenticator}
import util._
import util.Implicits._
import service._
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 =>
@@ -61,13 +68,13 @@ trait IndexControllerBase extends ControllerBase {
get("/activities.atom"){
contentType = "application/atom+xml; type=feed"
helper.xml.feed(getRecentActivities())
xml.feed(getRecentActivities())
}
/**
* Set account information into HttpSession and redirect.
*/
private def signin(account: model.Account) = {
private def signin(account: Account) = {
session.setAttribute(Keys.Session.LoginAccount, account)
updateLastLoginDate(account.userName)
@@ -92,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)
)
})
@@ -103,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,26 +1,30 @@
package app
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.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 service._
import IssuesService._
import util._
import util.Implicits._
import util.ControlUtil._
import org.scalatra.Ok
import model.Issue
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])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String])
@@ -32,10 +36,12 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply)
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text()))
)(IssueEditForm.apply)
"content" -> trim(optional(text()))
)(x => x)
val commentForm = mapping(
"issueId" -> label("Issue Id", number()),
@@ -47,22 +53,19 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text()))
)(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly {
searchIssues("all", _)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _)
get("/:owner/:repository/issues")(referrersOnly { repository =>
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:pr"))){
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
} else {
searchIssues(repository)
}
})
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) =>
getIssue(owner, name, issueId) map {
issues.html.issue(
html.issue(
_,
getComments(owner, name, issueId.toInt),
getIssueLabels(owner, name, issueId.toInt),
@@ -75,9 +78,21 @@ 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, issue) => ApiComment(issueComment, RepositoryName(repository), issueId, ApiUser(user), issue.isPullRequest) })
}).getOrElse(NotFound)
})
get("/:owner/:repository/issues/new")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
issues.html.create(
html.create(
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestones(owner, name),
getLabels(owner, name),
@@ -111,28 +126,46 @@ 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}")
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, form.title, form.content)
updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
createReferComment(owner, name, issue.copy(title = title), title)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
@@ -147,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), issue.isPullRequest))
}) 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}/${
@@ -179,14 +226,14 @@ trait IssuesControllerBase extends ControllerBase {
getIssue(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect {
case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName)
case t if t == "html" => html.editissue(
x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true)
"content" -> Markdown.toHtml(x.content getOrElse "No description given.",
repository, false, true, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
))
}
} else Unauthorized
@@ -197,30 +244,36 @@ trait IssuesControllerBase extends ControllerBase {
getComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => issues.html.editcomment(
case t if t == "html" => html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true)
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/issues/new/label")(collaboratorsOnly { repository =>
val labelNames = params("labelNames").split(",")
val labels = getLabels(repository.owner, repository.name).filter(x => labelNames.contains(x.labelName))
html.labellist(labels)
})
ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId =>
registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository =>
defining(params("id").toInt){ issueId =>
deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt)
issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
}
})
@@ -234,15 +287,17 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false)
gitbucket.core.issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound
} getOrElse Ok()
})
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action =>
executeBatch(repository) {
handleComment(_, None, repository)( _ => action)
action match {
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
case _ => // TODO BadRequest
}
}
})
@@ -277,8 +332,8 @@ trait IssuesControllerBase extends ControllerBase {
(Directory.getAttachedDir(repository.owner, repository.name) match {
case dir if(dir.exists && dir.isDirectory) =>
dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file =>
contentType = FileUtil.getMimeType(file.getName)
file
response.setHeader("Content-Disposition", f"""inline; filename=${file.getName}""")
RawData(FileUtil.getMimeType(file.getName), file)
}
case _ => None
}) getOrElse NotFound
@@ -287,19 +342,26 @@ trait IssuesControllerBase extends ControllerBase {
val assignedUserName = (key: String) => params.get(key) filter (_.trim != "")
val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt)
private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean =
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues")
params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
}
}
// TODO Same method exists in PullRequestController. Should it moved to IssueService?
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")
}
}
}
}
@@ -308,35 +370,34 @@ trait IssuesControllerBase extends ControllerBase {
* @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]]
*/
private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo)
(getAction: model.Issue => Option[String] =
(getAction: Issue => Option[String] =
p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = {
defining(repository.owner, repository.name){ case (owner, name) =>
val userName = context.loginAccount.get.userName
getIssue(owner, name, issueId.toString) map { issue =>
getIssue(owner, name, issueId.toString) flatMap { issue =>
val (action, recordActivity) =
getAction(issue)
.collect {
case "close" => true -> (Some("close") ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") ->
Some(recordReopenIssueActivity _))
}
case "close" if(!issue.closed) => true ->
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" if(issue.closed) => false ->
(Some("reopen") -> Some(recordReopenIssueActivity _))
}
.map { case (closed, t) =>
updateClosed(owner, name, issueId, closed)
t
}
updateClosed(owner, name, issueId, closed)
t
}
.getOrElse(None -> None)
val commentId = content
.map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") )
.getOrElse ( action.get.capitalize -> action.get )
match {
case (content, action) => createComment(owner, name, userName, issueId, content, action)
val commentId = (content, action) match {
case (None, None) => None
case (None, Some(action)) => Some(createComment(owner, name, userName, issueId, action.capitalize, action))
case (Some(content), _) => Some(createComment(owner, name, userName, issueId, content, action.map(_+ "_comment").getOrElse("comment")))
}
// record activity
// record comment activity if comment is entered
content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _)
@@ -348,56 +409,76 @@ 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}")
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}")
}
}
}
issue -> commentId
commentId.map( issue -> _ )
}
}
}
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
if(request.hasQueryString){
val q = request.getParameter("q")
if(q == null || q.trim.isEmpty){
IssueSearchCondition(request)
} else {
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
}
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
html.list(
"issues",
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page,
(getCollaborators(owner, repoName) :+ owner).sorted,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
(getCollaborators(owner, repoName) :+ owner).sorted
} else {
getCollaborators(owner, repoName)
},
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition,
filter,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}
}

View File

@@ -0,0 +1,83 @@
package gitbucket.core.controller
import gitbucket.core.issues.labels.html
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, LabelsService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import org.scalatra.i18n.Messages
import org.scalatra.Ok
class LabelsController extends LabelsControllerBase
with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase {
self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String)
val labelForm = mapping(
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply)
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
html.edit(None, repository)
})
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
html.edit(Some(label), repository)
} getOrElse NotFound()
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
Ok()
})
/**
* Constraint for the identifier such as user name, repository name or page name.
*/
private def labelName: Constraint = new Constraint(){
override def validate(name: String, value: String, messages: Messages): Option[String] =
if(value.contains(',')){
Some(s"${name} contains invalid character.")
} else if(value.startsWith("_") || value.startsWith("-")){
Some(s"${name} starts with invalid character.")
} else {
None
}
}
}

View File

@@ -1,11 +1,11 @@
package app
package gitbucket.core.controller
import gitbucket.core.issues.milestones.html
import gitbucket.core.service.{RepositoryService, MilestonesService, AccountService}
import gitbucket.core.util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import gitbucket.core.util.Implicits._
import jp.sf.amateras.scalatra.forms._
import service._
import util.{CollaboratorsAuthenticator, ReferrerAuthenticator}
import util.Implicits._
class MilestonesController extends MilestonesControllerBase
with MilestonesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
@@ -23,7 +23,7 @@ trait MilestonesControllerBase extends ControllerBase {
)(MilestoneForm.apply)
get("/:owner/:repository/issues/milestones")(referrersOnly { repository =>
issues.milestones.html.list(
html.list(
params.getOrElse("state", "open"),
getMilestonesWithIssueCount(repository.owner, repository.name),
repository,
@@ -31,7 +31,7 @@ trait MilestonesControllerBase extends ControllerBase {
})
get("/:owner/:repository/issues/milestones/new")(collaboratorsOnly {
issues.milestones.html.edit(None, _)
html.edit(None, _)
})
post("/:owner/:repository/issues/milestones/new", milestoneForm)(collaboratorsOnly { (form, repository) =>
@@ -41,7 +41,7 @@ trait MilestonesControllerBase extends ControllerBase {
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(collaboratorsOnly { repository =>
params("milestoneId").toIntOpt.map{ milestoneId =>
issues.milestones.html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
html.edit(getMilestone(repository.owner, repository.name, milestoneId), repository)
} getOrElse NotFound
})

View File

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

View File

@@ -0,0 +1,531 @@
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.service.CommitStatusService
import gitbucket.core.service.MergeService
import gitbucket.core.service.IssuesService._
import gitbucket.core.service.PullRequestService._
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 scala.collection.JavaConverters._
class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
with CommitsService with ActivityService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService
trait PullRequestsControllerBase extends ControllerBase {
self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService
with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReferrerAuthenticator with CollaboratorsAuthenticator
with CommitStatusService with MergeService =>
private val logger = LoggerFactory.getLogger(classOf[PullRequestsControllerBase])
val pullRequestForm = mapping(
"title" -> trim(label("Title" , text(required, maxlength(100)))),
"content" -> trim(label("Content", optional(text()))),
"targetUserName" -> trim(text(required, maxlength(100))),
"targetBranch" -> trim(text(required, maxlength(100))),
"requestUserName" -> trim(text(required, maxlength(100))),
"requestRepositoryName" -> trim(text(required, maxlength(100))),
"requestBranch" -> trim(text(required, maxlength(100))),
"commitIdFrom" -> trim(text(required, maxlength(40))),
"commitIdTo" -> trim(text(required, maxlength(40))),
"assignedUserName" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())),
"labelNames" -> trim(optional(text()))
)(PullRequestForm.apply)
val mergeForm = mapping(
"message" -> trim(label("Message", text(required)))
)(MergeForm.apply)
case class PullRequestForm(
title: String,
content: Option[String],
targetUserName: String,
targetBranch: String,
requestUserName: String,
requestRepositoryName: String,
requestBranch: String,
commitIdFrom: String,
commitIdTo: String,
assignedUserName: Option[String],
milestoneId: Option[Int],
labelNames: Option[String]
)
case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository =>
val q = request.getParameter("q")
if(Option(q).exists(_.contains("is:issue"))){
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
} else {
searchPullRequests(None, repository)
}
})
/**
* 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
val name = repository.name
getPullRequest(owner, name, issueId) map { case(issue, pullreq) =>
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))
.sortWith((a, b) => a.registeredDate before b.registeredDate),
getIssueLabels(owner, name, issueId),
(getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted,
getMilestonesWithIssueCount(owner, name),
getLabels(owner, name),
commits,
diffs,
hasWritePermission(owner, name, context.loginAccount),
repository)
}
}
} 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(
hasConfrict,
hasProblem,
issue,
pullreq,
statuses,
repository,
getRepository(pullreq.requestUserName, pullreq.requestRepositoryName, context.baseUrl).get)
}
} getOrElse NotFound
})
get("/:owner/:repository/pull/:id/delete/*")(collaboratorsOnly { repository =>
params("id").toIntOpt.map { issueId =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
createComment(repository.owner, repository.name, userName, issueId, branchName, "delete_branch")
redirect(s"/${repository.owner}/${repository.name}/pull/${issueId}")
} getOrElse NotFound
})
post("/:owner/:repository/pull/:id/merge", mergeForm)(collaboratorsOnly { (form, repository) =>
params("id").toIntOpt.flatMap { issueId =>
val owner = repository.owner
val name = repository.name
LockUtil.lock(s"${owner}/${name}"){
getPullRequest(owner, name, issueId).map { case (issue, pullreq) =>
using(Git.open(getRepositoryDir(owner, name))) { git =>
// mark issue as merged and close.
val loginAccount = context.loginAccount.get
createComment(owner, name, loginAccount.userName, issueId, form.message, "merge")
createComment(owner, name, loginAccount.userName, issueId, "Close", "close")
updateClosed(owner, name, issueId, true)
// record activity
recordMergeActivity(owner, name, loginAccount.userName, issueId, form.message)
// 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)
// close issue by content of pull request
val defaultBranch = getRepository(owner, name, context.baseUrl).get.repository.defaultBranch
if(pullreq.branch == defaultBranch){
commits.flatten.foreach { commit =>
closeIssuesFromMessage(commit.fullMessage, loginAccount.userName, owner, name)
}
issue.content match {
case Some(content) => closeIssuesFromMessage(content, loginAccount.userName, owner, name)
case _ =>
}
closeIssuesFromMessage(form.message, loginAccount.userName, owner, name)
}
updatePullRequests(owner, name, pullreq.branch)
// call web hook
callPullRequestWebHook("closed", repository, issueId, context.baseUrl, context.loginAccount.get)
// notifications
Notifier().toNotify(repository, issue, "merge"){
Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
}
}
} getOrElse NotFound
})
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 =>
using(
Git.open(getRepositoryDir(originUserName, originRepositoryName)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ (oldGit, newGit) =>
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}")
}
} getOrElse NotFound
}
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}...${headBranch.getOrElse(defaultBranch)}")
} getOrElse {
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}")
}
}
}
}
})
get("/:owner/:repository/compare/*...*")(referrersOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, originId) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, forkedId) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner) {
// Self repository
Some(forkedRepository.name)
} else if(forkedRepository.repository.originUserName.isEmpty){
// when ForkedRepository is the original repository
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
} else if(Some(originOwner) == forkedRepository.repository.originUserName){
// Original repository
forkedRepository.repository.originRepositoryName
} else {
// 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 {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val (oldId, newId) =
if(originRepository.branchList.contains(originId) && forkedRepository.branchList.contains(forkedId)){
// Branch name
val rootId = JGitUtil.getForkedCommitId(oldGit, newGit,
originRepository.owner, originRepository.name, originId,
forkedRepository.owner, forkedRepository.name, forkedId)
(Option(oldGit.getRepository.resolve(rootId)), Option(newGit.getRepository.resolve(forkedId)))
} else {
// Commit id
(Option(oldGit.getRepository.resolve(originId)), Option(newGit.getRepository.resolve(forkedId)))
}
(oldId, newId) match {
case (Some(oldId), Some(newId)) => {
val (commits, diffs) = getRequestCompareInfo(
originRepository.owner, originRepository.name, oldId.getName,
forkedRepository.owner, forkedRepository.name, newId.getName)
html.compare(
commits,
diffs,
(forkedRepository.repository.originUserName, forkedRepository.repository.originRepositoryName) match {
case (Some(userName), Some(repositoryName)) => (userName, repositoryName) :: getForkedRepositories(userName, repositoryName)
case _ => (forkedRepository.owner, forkedRepository.name) :: getForkedRepositories(forkedRepository.owner, forkedRepository.name)
},
commits.flatten.map(commit => getCommitComments(forkedRepository.owner, forkedRepository.name, commit.id, false)).flatten.toList,
originId,
forkedId,
oldId.getName,
newId.getName,
forkedRepository,
originRepository,
forkedRepository,
hasWritePermission(originRepository.owner, originRepository.name, context.loginAccount),
(getCollaborators(originRepository.owner, originRepository.name) ::: (if(getAccountByUserName(originRepository.owner).get.isGroupAccount) Nil else List(originRepository.owner))).sorted,
getMilestones(originRepository.owner, originRepository.name),
getLabels(originRepository.owner, originRepository.name)
)
}
case (oldId, newId) =>
redirect(s"/${forkedRepository.owner}/${forkedRepository.name}/compare/" +
s"${originOwner}:${oldId.map(_ => originId).getOrElse(originRepository.repository.defaultBranch)}..." +
s"${forkedOwner}:${newId.map(_ => forkedId).getOrElse(forkedRepository.repository.defaultBranch)}")
}
}
}) getOrElse NotFound
})
ajaxGet("/:owner/:repository/compare/*...*/mergecheck")(collaboratorsOnly { forkedRepository =>
val Seq(origin, forked) = multiParams("splat")
val (originOwner, tmpOriginBranch) = parseCompareIdentifie(origin, forkedRepository.owner)
val (forkedOwner, tmpForkedBranch) = parseCompareIdentifie(forked, forkedRepository.owner)
(for(
originRepositoryName <- if(originOwner == forkedOwner){
Some(forkedRepository.name)
} else {
forkedRepository.repository.originRepositoryName.orElse {
getForkedRepositories(forkedRepository.owner, forkedRepository.name).find(_._1 == originOwner).map(_._2)
}
};
originRepository <- getRepository(originOwner, originRepositoryName, context.baseUrl)
) yield {
using(
Git.open(getRepositoryDir(originRepository.owner, originRepository.name)),
Git.open(getRepositoryDir(forkedRepository.owner, forkedRepository.name))
){ case (oldGit, newGit) =>
val originBranch = JGitUtil.getDefaultBranch(oldGit, originRepository, tmpOriginBranch).get._2
val forkedBranch = JGitUtil.getDefaultBranch(newGit, forkedRepository, tmpForkedBranch).get._2
val conflict = LockUtil.lock(s"${originRepository.owner}/${originRepository.name}"){
checkConflict(originRepository.owner, originRepository.name, originBranch,
forkedRepository.owner, forkedRepository.name, forkedBranch)
}
html.mergecheck(conflict)
}
}) getOrElse NotFound
})
post("/:owner/:repository/pulls/new", pullRequestForm)(referrersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
val writable = hasWritePermission(owner, name, context.loginAccount)
val loginUserName = context.loginAccount.get.userName
val issueId = createIssue(
owner = repository.owner,
repository = repository.name,
loginUser = loginUserName,
title = form.title,
content = form.content,
assignedUserName = if(writable) form.assignedUserName else None,
milestoneId = if(writable) form.milestoneId else None,
isPullRequest = true)
createPullRequest(
originUserName = repository.owner,
originRepositoryName = repository.name,
issueId = issueId,
originBranch = form.targetBranch,
requestUserName = form.requestUserName,
requestRepositoryName = form.requestRepositoryName,
requestBranch = form.requestBranch,
commitIdFrom = form.commitIdFrom,
commitIdTo = form.commitIdTo)
// insert labels
if(writable){
form.labelNames.map { value =>
val labels = getLabels(owner, name)
value.split(",").foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(repository.owner, repository.name, issueId, label.labelId)
}
}
}
}
// fetch requested branch
fetchAsPullRequest(owner, name, form.requestUserName, form.requestRepositoryName, form.requestBranch, issueId)
// record activity
recordPullRequestActivity(owner, name, loginUserName, issueId, form.title)
// call web hook
callPullRequestWebHook("opened", repository, issueId, context.baseUrl, context.loginAccount.get)
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, issue, form.content.getOrElse("")){
Notifier.msgPullRequest(s"${context.baseUrl}/${owner}/${name}/pull/${issueId}")
}
}
redirect(s"/${owner}/${name}/pull/${issueId}")
}
})
// TODO Same method exists in IssueController. Should it moved to IssueService?
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){
// 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")
}
}
}
}
/**
* Parses branch identifier and extracts owner and branch name as tuple.
*
* - "owner:branch" to ("owner", "branch")
* - "branch" to ("defaultOwner", "branch")
*/
private def parseCompareIdentifie(value: String, defaultOwner: String): (String, String) =
if(value.contains(':')){
val array = value.split(":")
(array(0), array(1))
} else {
(defaultOwner, value)
}
private def getRequestCompareInfo(userName: String, repositoryName: String, branch: String,
requestUserName: String, requestRepositoryName: String, requestCommitId: String): (Seq[Seq[CommitInfo]], Seq[DiffInfo]) =
using(
Git.open(getRepositoryDir(userName, repositoryName)),
Git.open(getRepositoryDir(requestUserName, requestRepositoryName))
){ (oldGit, newGit) =>
val oldId = oldGit.getRepository.resolve(branch)
val newId = newGit.getRepository.resolve(requestCommitId)
val commits = newGit.log.addRange(oldId, newId).call.iterator.asScala.map { revCommit =>
new CommitInfo(revCommit)
}.toList.splitWith { (commit1, commit2) =>
helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}
val diffs = JGitUtil.getDiffs(newGit, oldId.getName, newId.getName, true)
(commits, diffs)
}
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) =>
val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName)
// retrieve search condition
val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request)
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
)
gitbucket.core.issues.html.list(
"pulls",
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page,
if(!getAccountByUserName(owner).exists(_.isGroupAccount)){
(getCollaborators(owner, repoName) :+ owner).sorted
} else {
getCollaborators(owner, repoName)
},
getMilestones(owner, repoName),
getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition,
repository,
hasWritePermission(owner, repoName, context.loginAccount))
}
}

View File

@@ -1,18 +1,21 @@
package app
package gitbucket.core.controller
import service._
import util.Directory._
import util.ControlUtil._
import util.Implicits._
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
import util.JGitUtil.CommitInfo
import gitbucket.core.settings.html
import gitbucket.core.model.WebHook
import gitbucket.core.service.{RepositoryService, AccountService, WebHookService}
import gitbucket.core.service.WebHookService._
import gitbucket.core.util._
import gitbucket.core.util.JGitUtil._
import gitbucket.core.util.ControlUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.Directory._
import jp.sf.amateras.scalatra.forms._
import org.apache.commons.io.FileUtils
import org.scalatra.i18n.Messages
import service.WebHookService.WebHookPayload
import util.JGitUtil.CommitInfo
import util.ControlUtil._
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.lib.Constants
import org.eclipse.jgit.lib.ObjectId
class RepositorySettingsController extends RepositorySettingsControllerBase
with RepositoryService with AccountService with WebHookService
@@ -64,18 +67,19 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the Options page.
*/
get("/:owner/:repository/settings/options")(ownerOnly {
settings.html.options(_, flash.get("info"))
html.options(_, flash.get("info"))
})
/**
* Save the repository options.
*/
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
saveRepositoryOptions(
repository.owner,
repository.name,
form.description,
if(repository.branchList.isEmpty) "master" else form.defaultBranch,
defaultBranch,
repository.repository.parentUserName.map { _ =>
repository.repository.isPrivate
} getOrElse form.isPrivate
@@ -93,6 +97,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
}
}
// Change repository HEAD
using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git =>
git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch)
}
flash += "info" -> "Repository settings has been updated."
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
})
@@ -101,7 +109,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the Collaborators page.
*/
get("/:owner/:repository/settings/collaborators")(ownerOnly { repository =>
settings.html.collaborators(
html.collaborators(
getCollaborators(repository.owner, repository.name),
getAccountByUserName(repository.owner).get.isGroupAccount,
repository)
@@ -131,7 +139,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the web hook page.
*/
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
})
/**
@@ -153,23 +161,23 @@ trait RepositorySettingsControllerBase extends ControllerBase {
/**
* Send the test request to registered web hook URLs.
*/
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
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(_))
.setMaxCount(4)
.call.iterator.asScala.map(new CommitInfo(_)).toList
getWebHookURLs(repository.owner, repository.name) match {
case webHookURLs if(webHookURLs.nonEmpty) =>
for(ownerAccount <- getAccountByUserName(repository.owner)){
callWebHook(repository.owner, repository.name, webHookURLs,
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
}
case _ =>
getAccountByUserName(repository.owner).foreach { ownerAccount =>
callWebHook("push",
List(WebHook(repository.owner, repository.name, form.url)),
WebHookPushPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, (if(commits.isEmpty){Nil}else{commits.tail}), ownerAccount,
oldId = commits.lastOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()),
newId = commits.headOption.map(_.id).map(ObjectId.fromString).getOrElse(ObjectId.zeroId()))
)
}
flash += "url" -> form.url
flash += "info" -> "Test payload deployed!"
}
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
@@ -179,7 +187,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
* Display the danger zone.
*/
get("/:owner/:repository/settings/danger")(ownerOnly {
settings.html.danger(_)
html.danger(_)
})
/**
@@ -269,4 +277,4 @@ trait RepositorySettingsControllerBase extends ControllerBase {
}
}
}
}
}

View File

@@ -0,0 +1,725 @@
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, CommitState}
import gitbucket.core.service.CommitStatusService
import gitbucket.core.service.WebHookService._
import gitbucket.core.view
import gitbucket.core.view.helpers
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.dircache.DirCache
import org.eclipse.jgit.errors.MissingObjectException
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 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 CommitStatusService
with WebHookPullRequestService =>
ArchiveCommand.registerFormat("zip", new ZipFormat)
ArchiveCommand.registerFormat("tar.gz", new TgzFormat)
case class EditorForm(
branch: String,
path: String,
content: String,
message: Option[String],
charset: String,
lineSeparator: String,
newFileName: String,
oldFileName: Option[String]
)
case class DeleteForm(
branch: String,
path: String,
message: Option[String],
fileName: String
)
case class CommentForm(
fileName: Option[String],
oldLineNumber: Option[Int],
newLineNumber: Option[Int],
content: String,
issueId: Option[Int]
)
val editorForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"content" -> trim(label("Content", text(required))),
"message" -> trim(label("Message", optional(text()))),
"charset" -> trim(label("Charset", text(required))),
"lineSeparator" -> trim(label("Line Separator", text(required))),
"newFileName" -> trim(label("Filename", text(required))),
"oldFileName" -> trim(label("Old filename", optional(text())))
)(EditorForm.apply)
val deleteForm = mapping(
"branch" -> trim(label("Branch", text(required))),
"path" -> trim(label("Path", text())),
"message" -> trim(label("Message", optional(text()))),
"fileName" -> trim(label("Filename", text(required)))
)(DeleteForm.apply)
val commentForm = mapping(
"fileName" -> trim(label("Filename", optional(text()))),
"oldLineNumber" -> trim(label("Old line number", optional(number()))),
"newLineNumber" -> trim(label("New line number", optional(number()))),
"content" -> trim(label("Content", text(required))),
"issueId" -> trim(label("Issue Id", optional(number())))
)(CommentForm.apply)
/**
* Returns converted HTML from Markdown for preview.
*/
post("/:owner/:repository/_preview")(referrersOnly { repository =>
contentType = "text/html"
helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean,
params("enableRefsLink").toBoolean,
params("enableTaskList").toBoolean,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
/**
* Displays the file list of the repository root and the default branch.
*/
get("/:owner/:repository")(referrersOnly {
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.
*/
get("/:owner/:repository/tree/*")(referrersOnly { repository =>
val (id, path) = splitPath(repository, multiParams("splat").head)
if(path.isEmpty){
fileList(repository, id)
} else {
fileList(repository, id, path)
}
})
/**
* Displays the commit list of the specified resource.
*/
get("/:owner/:repository/commits/*")(referrersOnly { repository =>
val (branchName, path) = splitPath(repository, multiParams("splat").head)
val page = params.get("page").flatMap(_.toIntOpt).getOrElse(1)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.getCommitLog(git, branchName, page, 30, path) match {
case Right((logs, hasNext)) =>
html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound
}
}
})
/**
* 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,
None, JGitUtil.ContentInfo("text", None, Some("UTF-8")))
})
get("/:owner/:repository/edit/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
html.editor(branch, repository, paths.take(paths.size - 1).toList, Some(paths.last),
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
get("/:owner/:repository/remove/*")(collaboratorsOnly { repository =>
val (branch, path) = splitPath(repository, multiParams("splat").head)
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
getPathObjectId(git, path, revCommit).map { objectId =>
val paths = path.split("/")
html.delete(branch, repository, paths.take(paths.size - 1).toList, paths.last,
JGitUtil.getContentInfo(git, path, objectId))
} getOrElse NotFound
}
})
post("/:owner/:repository/create", editorForm)(collaboratorsOnly { (form, repository) =>
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) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
}")
})
post("/:owner/:repository/update", editorForm)(collaboratorsOnly { (form, repository) =>
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) urlEncode(form.newFileName) else s"${form.path}/${urlEncode(form.newFileName)}"
}")
})
post("/:owner/:repository/remove", deleteForm)(collaboratorsOnly { (form, repository) =>
commitFile(repository, form.branch, form.path, None, Some(form.fileName), "", "",
form.message.getOrElse(s"Delete ${form.fileName}"))
redirect(s"/${repository.owner}/${repository.name}/tree/${form.branch}${if(form.path.length == 0) "" else form.path}")
})
/**
* Displays the file content of the specified branch or commit.
*/
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))
getPathObjectId(git, path, revCommit).map { objectId =>
if(raw){
// Download
JGitUtil.getObjectLoaderFromId(git, objectId){ loader =>
//RawData("application/octet-stream", bytes)
contentType = "application/octet-stream"
response.setContentLength(loader.getSize.toInt)
loader.copyTo(response.getOutputStream)
()
} getOrElse NotFound
} else {
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.
*/
get("/:owner/:repository/commit/:id")(referrersOnly { repository =>
val id = params("id")
try {
using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git =>
defining(JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(id))) { revCommit =>
JGitUtil.getDiffs(git, id) match {
case (diffs, oldCommitId) =>
html.commit(id, new JGitUtil.CommitInfo(revCommit),
JGitUtil.getBranchesOfCommit(git, revCommit.getName),
JGitUtil.getTagsOfCommit(git, revCommit.getName),
getCommitComments(repository.owner, repository.name, id, false),
repository, diffs, oldCommitId, hasWritePermission(repository.owner, repository.name, context.loginAccount))
}
}
}
} catch {
case e:MissingObjectException => NotFound
}
})
post("/:owner/:repository/commit/:id/comment/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName, form.content,
form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
redirect(s"/${repository.owner}/${repository.name}/commit/${id}")
})
ajaxGet("/:owner/:repository/commit/:id/comment/_form")(readableUsersOnly { repository =>
val id = params("id")
val fileName = params.get("fileName")
val oldLineNumber = params.get("oldLineNumber") map (_.toInt)
val newLineNumber = params.get("newLineNumber") map (_.toInt)
val issueId = params.get("issueId") map (_.toInt)
html.commentform(
commitId = id,
fileName, oldLineNumber, newLineNumber, issueId,
hasWritePermission = hasWritePermission(repository.owner, repository.name, context.loginAccount),
repository = repository
)
})
ajaxPost("/:owner/:repository/commit/:id/comment/_data/new", commentForm)(readableUsersOnly { (form, repository) =>
val id = params("id")
val commentId = createCommitComment(repository.owner, repository.name, id, context.loginAccount.get.userName,
form.content, form.fileName, form.oldLineNumber, form.newLineNumber, form.issueId.isDefined)
form.issueId match {
case Some(issueId) => recordCommentPullRequestActivity(repository.owner, repository.name, context.loginAccount.get.userName, issueId, form.content)
case None => recordCommentCommitActivity(repository.owner, repository.name, context.loginAccount.get.userName, id, form.content)
}
helper.html.commitcomment(getCommitComment(repository.owner, repository.name, commentId.toString).get,
hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
ajaxGet("/:owner/:repository/commit_comments/_data/:id")(readableUsersOnly { repository =>
getCommitComment(repository.owner, repository.name, params("id")) map { x =>
if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){
params.get("dataType") collect {
case t if t == "html" => html.editcomment(
x.content, x.commentId, x.userName, x.repositoryName)
} getOrElse {
contentType = formats("json")
org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
))
}
} else Unauthorized
} getOrElse NotFound
})
ajaxPost("/:owner/:repository/commit_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
updateCommitComment(comment.commentId, form.content)
redirect(s"/${owner}/${name}/commit_comments/_data/${comment.commentId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/commit_comments/delete/:id")(readableUsersOnly { repository =>
defining(repository.owner, repository.name){ case (owner, name) =>
getCommitComment(owner, name, params("id")).map { comment =>
if(isEditable(owner, name, comment.commentedUserName)){
Ok(deleteCommitComment(comment.commentId))
} else Unauthorized
} getOrElse NotFound
}
})
/**
* Displays branches.
*/
get("/:owner/:repository/branches")(referrersOnly { repository =>
val branches = JGitUtil.getBranches(
owner = repository.owner,
name = repository.name,
defaultBranch = repository.repository.defaultBranch,
origin = repository.repository.originUserName.isEmpty
)
.sortBy(br => (br.mergeInfo.isEmpty, br.commitTime))
.map(br => br -> getPullRequestByRequestCommit(repository.owner, repository.name, repository.repository.defaultBranch, br.name, br.commitId))
.reverse
html.branches(branches, hasWritePermission(repository.owner, repository.name, context.loginAccount), repository)
})
/**
* Creates a branch.
*/
post("/:owner/:repository/branches")(collaboratorsOnly { repository =>
val newBranchName = params.getOrElse("new", halt(400))
val fromBranchName = params.getOrElse("from", halt(400))
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
JGitUtil.createBranch(git, fromBranchName, newBranchName)
} match {
case Right(message) =>
flash += "info" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}")
case Left(message) =>
flash += "error" -> message
redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}")
}
})
/**
* Deletes branch.
*/
get("/:owner/:repository/delete/*")(collaboratorsOnly { repository =>
val branchName = multiParams("splat").head
val userName = context.loginAccount.get.userName
if(repository.repository.defaultBranch != branchName){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
git.branchDelete().setForce(true).setBranchNames(branchName).call()
recordDeleteBranchActivity(repository.owner, repository.name, userName, branchName)
}
}
redirect(s"/${repository.owner}/${repository.name}/branches")
})
/**
* Displays tags.
*/
get("/:owner/:repository/tags")(referrersOnly {
html.tags(_)
})
/**
* Download repository contents as an archive.
*/
get("/:owner/:repository/archive/*")(referrersOnly { repository =>
multiParams("splat").head match {
case name if name.endsWith(".zip") =>
archiveRepository(name, ".zip", repository)
case name if name.endsWith(".tar.gz") =>
archiveRepository(name, ".tar.gz", repository)
case _ => BadRequest
}
})
get("/:owner/:repository/network/members")(referrersOnly { repository =>
html.forked(
getRepository(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name),
context.baseUrl),
getForkedRepositories(
repository.repository.originUserName.getOrElse(repository.owner),
repository.repository.originRepositoryName.getOrElse(repository.name)),
repository)
})
/**
* Displays the file find of branch.
*/
get("/:owner/:repository/find/*")(referrersOnly { repository =>
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val ref = multiParams("splat").head
JGitUtil.getTreeId(git, ref).map{ treeId =>
html.find(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
} orElse repository.tags.collectFirst {
case tag if(path == tag.name || path.startsWith(tag.name + "/")) => tag.name
} getOrElse path.split("/")(0)
(id, path.substring(id.length).stripPrefix("/"))
}
private val readmeFiles = PluginRegistry().renderableExtensions.map { extension =>
s"readme.${extension}"
} ++ Seq("readme.txt", "readme")
/**
* Provides HTML of the file list.
*
* @param repository the repository information
* @param revstr the branch name or commit id(optional)
* @param path the directory path (optional)
* @return HTML of the file list
*/
private def fileList(repository: RepositoryService.RepositoryInfo, revstr: String = "", path: String = ".") = {
if(repository.commitCount == 0){
html.guide(repository, hasWritePermission(repository.owner, repository.name, context.loginAccount))
} else {
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
// get specified commit
JGitUtil.getDefaultBranch(git, repository, revstr).map { case (objectId, revision) =>
defining(JGitUtil.getRevCommitFromId(git, objectId)) { revCommit =>
val lastModifiedCommit = if(path == ".") revCommit else JGitUtil.getLastModifiedCommit(git, revCommit, path)
// get files
val files = JGitUtil.getFileList(git, revision, path)
val parentPath = if (path == ".") Nil else path.split("/").toList
// process README.md or README.markdown
val readme = files.find { file =>
readmeFiles.contains(file.name.toLowerCase)
}.map { file =>
val path = (file.name :: parentPath.reverse).reverse
path -> StringUtil.convertFromByteArray(JGitUtil.getContentFromId(
Git.open(getRepositoryDir(repository.owner, repository.name)), file.id, true).get)
}
html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path
context.loginAccount match {
case None => List()
case account: Option[Account] => getGroupsByUserName(account.get.userName)
}, // 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
}
}
}
private def commitFile(repository: RepositoryService.RepositoryInfo,
branch: String, path: String, newFileName: Option[String], oldFileName: Option[String],
content: String, charset: String, message: String) = {
val newPath = newFileName.map { newFileName => if(path.length == 0) newFileName else s"${path}/${newFileName}" }
val oldPath = oldFileName.map { oldFileName => if(path.length == 0) oldFileName else s"${path}/${oldFileName}" }
LockUtil.lock(s"${repository.owner}/${repository.name}"){
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val loginAccount = context.loginAccount.get
val builder = DirCache.newInCore.builder()
val inserter = git.getRepository.newObjectInserter()
val headName = s"refs/heads/${branch}"
val headTip = git.getRepository.resolve(headName)
JGitUtil.processTree(git, headTip){ (path, tree) =>
if(!newPath.exists(_ == path) && !oldPath.exists(_ == path)){
builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
}
}
newPath.foreach { newPath =>
builder.add(JGitUtil.createDirCacheEntry(newPath, FileMode.REGULAR_FILE,
inserter.insert(Constants.OBJ_BLOB, content.getBytes(charset))))
}
builder.finish()
val commitId = JGitUtil.createNewCommit(git, inserter, headTip, builder.getDirCache.writeTree(inserter),
headName, loginAccount.fullName, loginAccount.mailAddress, message)
inserter.flush()
inserter.release()
// update refs
val refUpdate = git.getRepository.updateRef(headName)
refUpdate.setNewObjectId(commitId)
refUpdate.setForceUpdate(false)
refUpdate.setRefLogIdent(new PersonIdent(loginAccount.fullName, loginAccount.mailAddress))
//refUpdate.setRefLogMessage("merged", true)
refUpdate.update()
// update pull request
updatePullRequests(repository.owner, repository.name, branch)
// record activity
recordPushActivity(repository.owner, repository.name, loginAccount.userName, branch,
List(new CommitInfo(JGitUtil.getRevCommitFromId(git, commitId))))
// close issue by commit message
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))
callWebHookOf(repository.owner, repository.name, "push") {
getAccountByUserName(repository.owner).map{ ownerAccount =>
WebHookPushPayload(git, loginAccount, headName, repository, List(commit), ownerAccount,
oldId = headTip, newId = commitId)
}
}
}
}
}
private def getPathObjectId(git: Git, path: String, revCommit: RevCommit): Option[ObjectId] = {
@scala.annotation.tailrec
def _getPathObjectId(path: String, walk: TreeWalk): Option[ObjectId] = walk.next match {
case true if(walk.getPathString == path) => Some(walk.getObjectId(0))
case true => _getPathObjectId(path, walk)
case false => None
}
using(new TreeWalk(git.getRepository)){ treeWalk =>
treeWalk.addTree(revCommit.getTree)
treeWalk.setRecursive(true)
_getPathObjectId(path, treeWalk)
}
}
private def archiveRepository(name: String, suffix: String, repository: RepositoryService.RepositoryInfo): Unit = {
val revision = name.stripSuffix(suffix)
val workDir = getDownloadWorkDir(repository.owner, repository.name, session.getId)
if(workDir.exists) {
FileUtils.deleteDirectory(workDir)
}
workDir.mkdirs
val filename = repository.name + "-" +
(if(revision.length == 40) revision.substring(0, 10) else revision).replace('/', '_') + suffix
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(revision))
contentType = "application/octet-stream"
response.setHeader("Content-Disposition", s"attachment; filename=${filename}")
response.setBufferSize(1024 * 1024);
git.archive
.setFormat(suffix.tail)
.setTree(revCommit.getTree)
.setOutputStream(response.getOutputStream)
.call()
Unit
}
}
private def isEditable(owner: String, repository: String, author: String)(implicit context: Context): Boolean =
hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName
}

View File

@@ -1,9 +1,10 @@
package app
package gitbucket.core.controller
import util._
import gitbucket.core.search.html
import gitbucket.core.service._
import gitbucket.core.util.{StringUtil, ControlUtil, ReferrerAuthenticator, Implicits}
import ControlUtil._
import Implicits._
import service._
import jp.sf.amateras.scalatra.forms._
class SearchController extends SearchControllerBase
@@ -34,12 +35,12 @@ trait SearchControllerBase extends ControllerBase { self: RepositoryService
}
target.toLowerCase match {
case "issue" => search.html.issues(
case "issue" => html.issues(
searchIssues(repository.owner, repository.name, query),
countFiles(repository.owner, repository.name, query),
query, page, repository)
case _ => search.html.code(
case _ => html.code(
searchFiles(repository.owner, repository.name, query),
countIssues(repository.owner, repository.name, query),
query, page, repository)

View File

@@ -0,0 +1,87 @@
package gitbucket.core.controller
import gitbucket.core.admin.html
import gitbucket.core.service.{AccountService, SystemSettingsService}
import gitbucket.core.util.AdminAuthenticator
import gitbucket.core.ssh.SshServer
import SystemSettingsService._
import jp.sf.amateras.scalatra.forms._
class SystemSettingsController extends SystemSettingsControllerBase
with AccountService with AdminAuthenticator
trait SystemSettingsControllerBase extends ControllerBase {
self: AccountService with AdminAuthenticator =>
private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
"allowAnonymousAccess" -> trim(label("Anonymous access", boolean())),
"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()))),
"useSMTP" -> trim(label("SMTP", boolean())),
"smtp" -> optionalIfNotChecked("useSMTP", mapping(
"host" -> trim(label("SMTP Host", text(required))),
"port" -> trim(label("SMTP Port", optional(number()))),
"user" -> trim(label("SMTP User", optional(text()))),
"password" -> trim(label("SMTP Password", optional(text()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"fromAddress" -> trim(label("FROM Address", optional(text()))),
"fromName" -> trim(label("FROM Name", optional(text())))
)(Smtp.apply)),
"ldapAuthentication" -> trim(label("LDAP", boolean())),
"ldap" -> optionalIfNotChecked("ldapAuthentication", mapping(
"host" -> trim(label("LDAP host", text(required))),
"port" -> trim(label("LDAP port", optional(number()))),
"bindDN" -> trim(label("Bind DN", optional(text()))),
"bindPassword" -> trim(label("Bind Password", optional(text()))),
"baseDN" -> trim(label("Base DN", text(required))),
"userNameAttribute" -> trim(label("User name attribute", text(required))),
"additionalFilterCondition"-> trim(label("Additional filter condition", optional(text()))),
"fullNameAttribute" -> trim(label("Full name attribute", optional(text()))),
"mailAttribute" -> trim(label("Mail address attribute", optional(text()))),
"tls" -> trim(label("Enable TLS", optional(boolean()))),
"ssl" -> trim(label("Enable SSL", optional(boolean()))),
"keystore" -> trim(label("Keystore", optional(text())))
)(Ldap.apply))
)(SystemSettings.apply).verifying { settings =>
if(settings.ssh && settings.baseUrl.isEmpty){
Seq("baseUrl" -> "Base URL is required if SSH access is enabled.")
} else Nil
}
private val pluginForm = mapping(
"pluginId" -> list(trim(label("", text())))
)(PluginForm.apply)
case class PluginForm(pluginIds: List[String])
get("/admin/system")(adminOnly {
html.system(flash.get("info"))
})
post("/admin/system", form)(adminOnly { form =>
saveSystemSettings(form)
if(form.ssh && SshServer.isActive && context.settings.sshPort != form.sshPort){
SshServer.stop()
}
if(form.ssh && !SshServer.isActive && form.baseUrl.isDefined){
SshServer.start(
form.sshPort.getOrElse(SystemSettingsService.DefaultSshPort),
form.baseUrl.get)
} else if(!form.ssh && SshServer.isActive){
SshServer.stop()
}
flash += "info" -> "System settings has been updated."
redirect("/admin/system")
})
}

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